From 21f31edca7a89e4dfdfa22ad2b341dddefa5cd5a Mon Sep 17 00:00:00 2001 From: Khor Shu Heng Date: Mon, 24 Jul 2023 10:41:57 +0800 Subject: [PATCH 01/12] feat: use Keto 0.11 for authorization Signed-off-by: Khor Shu Heng --- Makefile | 2 +- api/api/api_test.go | 5 +- api/api/projects_api.go | 39 +- api/api/router.go | 25 +- api/config/config.go | 9 +- api/config/config_test.go | 23 +- api/config/testdata/config-2.yaml | 4 +- api/middleware/authorization.go | 76 ++-- api/middleware/authorization_test.go | 45 +++ api/pkg/authz/enforcer/builder.go | 54 +-- api/pkg/authz/enforcer/cache.go | 79 +--- api/pkg/authz/enforcer/cache_test.go | 168 ++------ api/pkg/authz/enforcer/enforcer.go | 464 ++++++++++------------- api/pkg/authz/enforcer/enforcer_test.go | 424 ++++++++++----------- api/pkg/authz/enforcer/mocks/enforcer.go | 156 ++++---- api/pkg/authz/enforcer/types/policy.go | 9 - api/pkg/authz/enforcer/types/role.go | 7 - api/service/projects_service.go | 167 ++++---- api/service/projects_service_test.go | 295 +++++++------- config-keto-dev.yaml | 15 + docker-compose.yaml | 12 +- go.mod | 26 +- go.sum | 131 +------ 23 files changed, 966 insertions(+), 1269 deletions(-) create mode 100644 api/middleware/authorization_test.go delete mode 100644 api/pkg/authz/enforcer/types/policy.go delete mode 100644 api/pkg/authz/enforcer/types/role.go create mode 100644 config-keto-dev.yaml diff --git a/Makefile b/Makefile index 12cfee71..7287b0bd 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ all: setup init-dep lint test clean build run setup: @echo "> Setting up tools..." @test -x $(shell go env GOPATH)/bin/golangci-lint || \ - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/v1.48.0/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.48.0 + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/v1.53.3/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.53.3 .PHONY: init-dep init-dep: init-dep-ui init-dep-api diff --git a/api/api/api_test.go b/api/api/api_test.go index a4d76955..3037b1a8 100644 --- a/api/api/api_test.go +++ b/api/api/api_test.go @@ -1,6 +1,7 @@ package api import ( + "context" "fmt" "net/http" "strings" @@ -75,12 +76,12 @@ func (s *APITestSuite) SetupTest() { s.Require().NoError(err, "Failed to create app context") // create project and otherProject - s.mainProject, err = appCtx.ProjectsService.CreateProject(&models.Project{ + s.mainProject, err = appCtx.ProjectsService.CreateProject(context.Background(), &models.Project{ Name: "test-project", }) s.Require().NoError(err, "Failed to create project") - s.otherProject, err = appCtx.ProjectsService.CreateProject(&models.Project{ + s.otherProject, err = appCtx.ProjectsService.CreateProject(context.Background(), &models.Project{ Name: "other-project", }) s.Require().NoError(err, "Failed to create other project") diff --git a/api/api/projects_api.go b/api/api/projects_api.go index 7c1b91e4..4dfb8d24 100644 --- a/api/api/projects_api.go +++ b/api/api/projects_api.go @@ -8,7 +8,6 @@ import ( "github.com/caraml-dev/mlp/api/log" "github.com/caraml-dev/mlp/api/models" - "github.com/caraml-dev/mlp/api/pkg/authz/enforcer" apperror "github.com/caraml-dev/mlp/api/pkg/errors" ) @@ -17,14 +16,12 @@ type ProjectsController struct { } func (c *ProjectsController) ListProjects(r *http.Request, vars map[string]string, _ interface{}) *Response { - projects, err := c.ProjectsService.ListProjects(vars["name"]) + projects, err := c.ProjectsService.ListProjects(r.Context(), vars["name"], vars["user"]) if err != nil { log.Errorf("error fetching projects: %s", err) return FromError(err) } - user := vars["user"] - projects, err = c.filterAuthorizedProjects(user, projects, enforcer.ActionRead) if err != nil { return InternalServerError(err.Error()) } @@ -57,7 +54,7 @@ func (c *ProjectsController) CreateProject(r *http.Request, vars map[string]stri user := vars["user"] project.Administrators = addRequester(user, project.Administrators) - project, err = c.ProjectsService.CreateProject(project) + project, err = c.ProjectsService.CreateProject(r.Context(), project) if err != nil { log.Errorf("error creating project %s: %s", project.Name, err) return FromError(err) @@ -85,7 +82,7 @@ func (c *ProjectsController) UpdateProject(r *http.Request, vars map[string]stri project.Team = newProject.Team project.Stream = newProject.Stream project.Labels = newProject.Labels - project, err = c.ProjectsService.UpdateProject(project) + project, err = c.ProjectsService.UpdateProject(r.Context(), project) if err != nil { log.Errorf("error updating project %s: %s", project.Name, err) return FromError(err) @@ -105,36 +102,6 @@ func (c *ProjectsController) GetProject(r *http.Request, vars map[string]string, return Ok(project) } -func (c *ProjectsController) filterAuthorizedProjects( - user string, - projects []*models.Project, - action string, -) ([]*models.Project, error) { - if c.AuthorizationEnabled { - projectIds := make([]string, 0) - allowedProjects := make([]*models.Project, 0) - projectMap := make(map[string]*models.Project) - for _, project := range projects { - projectID := fmt.Sprintf("projects:%s", project.ID) - projectIds = append(projectIds, projectID) - projectMap[projectID] = project - } - - allowedProjectIds, err := c.Enforcer.FilterAuthorizedResource(user, projectIds, action) - if err != nil { - return nil, err - } - - for _, projectID := range allowedProjectIds { - allowedProjects = append(allowedProjects, projectMap[projectID]) - } - - return allowedProjects, nil - } - - return projects, nil -} - func (c *ProjectsController) Routes() []Route { return []Route{ { diff --git a/api/api/router.go b/api/api/router.go index 2ba20974..cde94394 100644 --- a/api/api/router.go +++ b/api/api/router.go @@ -33,14 +33,16 @@ type AppContext struct { SecretStorageService service.SecretStorageService DefaultSecretStorage *models.SecretStorage - AuthorizationEnabled bool - Enforcer enforcer.Enforcer + AuthorizationEnabled bool + UseAuthorizationMiddleware bool + Enforcer enforcer.Enforcer } func NewAppContext(db *gorm.DB, cfg *config.Config) (ctx *AppContext, err error) { var authEnforcer enforcer.Enforcer if cfg.Authorization.Enabled { - enforcerCfg := enforcer.NewEnforcerBuilder().URL(cfg.Authorization.KetoServerURL).Product("mlp") + enforcerCfg := enforcer.NewEnforcerBuilder() + enforcerCfg.KetoEndpoints(cfg.Authorization.KetoRemoteRead, cfg.Authorization.KetoRemoteWrite) if cfg.Authorization.Caching.Enabled { enforcerCfg = enforcerCfg.WithCaching( cfg.Authorization.Caching.KeyExpirySeconds, @@ -94,13 +96,14 @@ func NewAppContext(db *gorm.DB, cfg *config.Config) (ctx *AppContext, err error) projectRepository, storageClientRegistry, defaultSecretStorage) return &AppContext{ - ApplicationService: applicationService, - ProjectsService: projectsService, - SecretService: secretService, - SecretStorageService: secretStorageService, - AuthorizationEnabled: cfg.Authorization.Enabled, - Enforcer: authEnforcer, - DefaultSecretStorage: defaultSecretStorage, + ApplicationService: applicationService, + ProjectsService: projectsService, + SecretService: secretService, + SecretStorageService: secretStorageService, + AuthorizationEnabled: cfg.Authorization.Enabled, + UseAuthorizationMiddleware: cfg.Authorization.UseMiddleware, + Enforcer: authEnforcer, + DefaultSecretStorage: defaultSecretStorage, }, nil } @@ -180,7 +183,7 @@ func NewRouter(appCtx *AppContext, controllers []Controller) *mux.Router { router := mux.NewRouter().StrictSlash(true) validator := validation.NewValidator() - if appCtx.AuthorizationEnabled { + if appCtx.AuthorizationEnabled && appCtx.UseAuthorizationMiddleware { authzMiddleware := middleware.NewAuthorizer(appCtx.Enforcer) router.Use(authzMiddleware.AuthorizationMiddleware) } diff --git a/api/config/config.go b/api/config/config.go index 97e25e5c..f78994ef 100644 --- a/api/config/config.go +++ b/api/config/config.go @@ -87,9 +87,11 @@ type DatabaseConfig struct { } type AuthorizationConfig struct { - Enabled bool - KetoServerURL string `validate:"required_if=Enabled True"` - Caching *InMemoryCacheConfig `validate:"required_if=Enabled True"` + Enabled bool + KetoRemoteRead string `validate:"required_if=Enabled True"` + KetoRemoteWrite string `validate:"required_if=Enabled True"` + Caching *InMemoryCacheConfig `validate:"required_if=Enabled True"` + UseMiddleware bool } type InMemoryCacheConfig struct { @@ -199,6 +201,7 @@ var defaultConfig = &Config{ KeyExpirySeconds: 600, CacheCleanUpIntervalSeconds: 900, }, + UseMiddleware: false, }, Database: &DatabaseConfig{ Host: "localhost", diff --git a/api/config/config_test.go b/api/config/config_test.go index def4dd49..7a23cb20 100644 --- a/api/config/config_test.go +++ b/api/config/config_test.go @@ -198,13 +198,15 @@ func TestLoad(t *testing.T) { }, }, Authorization: &config.AuthorizationConfig{ - Enabled: true, - KetoServerURL: "http://localhost:4466", + Enabled: true, + KetoRemoteRead: "http://localhost:4466", + KetoRemoteWrite: "http://localhost:4467", Caching: &config.InMemoryCacheConfig{ Enabled: true, KeyExpirySeconds: 1000, CacheCleanUpIntervalSeconds: 2000, }, + UseMiddleware: true, }, Database: &config.DatabaseConfig{ Host: "localhost", @@ -328,8 +330,9 @@ func TestValidate(t *testing.T) { Port: 8080, Environment: "dev", Authorization: &config.AuthorizationConfig{ - Enabled: true, - KetoServerURL: "http://keto.mlp", + Enabled: true, + KetoRemoteRead: "http://keto.mlp", + KetoRemoteWrite: "http://keto.mlp", Caching: &config.InMemoryCacheConfig{ KeyExpirySeconds: 600, CacheCleanUpIntervalSeconds: 900, @@ -379,7 +382,8 @@ func TestValidate(t *testing.T) { Port: 8080, Environment: "dev", Authorization: &config.AuthorizationConfig{ - Enabled: true, + Enabled: true, + KetoRemoteWrite: "localhost:4467", Caching: &config.InMemoryCacheConfig{ KeyExpirySeconds: 600, CacheCleanUpIntervalSeconds: 900, @@ -413,8 +417,8 @@ func TestValidate(t *testing.T) { }, error: errors.New( "failed to validate configuration: " + - "Key: 'Config.Authorization.KetoServerURL' " + - "Error:Field validation for 'KetoServerURL' failed on the 'required_if' tag", + "Key: 'Config.Authorization.KetoRemoteRead' " + + "Error:Field validation for 'KetoRemoteRead' failed on the 'required_if' tag", ), }, "missing authz cache key expiry | failure": { @@ -423,8 +427,9 @@ func TestValidate(t *testing.T) { Port: 8080, Environment: "dev", Authorization: &config.AuthorizationConfig{ - Enabled: true, - KetoServerURL: "http://abc", + Enabled: true, + KetoRemoteRead: "http://abc", + KetoRemoteWrite: "http://abc", Caching: &config.InMemoryCacheConfig{ Enabled: true, }, diff --git a/api/config/testdata/config-2.yaml b/api/config/testdata/config-2.yaml index e9caa595..635b6f02 100644 --- a/api/config/testdata/config-2.yaml +++ b/api/config/testdata/config-2.yaml @@ -17,11 +17,13 @@ applications: authorization: enabled: true - ketoServerURL: http://localhost:4466 + ketoRemoteRead: http://localhost:4466 + ketoRemoteWrite: http://localhost:4467 caching: enabled: true keyExpirySeconds: 1000 cacheCleanUpIntervalSeconds: 2000 + useMiddleware: true ui: clockworkUIHomepage: http://clockwork.dev diff --git a/api/middleware/authorization.go b/api/middleware/authorization.go index cb7e9f00..88d3806b 100644 --- a/api/middleware/authorization.go +++ b/api/middleware/authorization.go @@ -6,45 +6,40 @@ import ( "net/http" "strings" + "golang.org/x/exp/slices" + "github.com/caraml-dev/mlp/api/pkg/authz/enforcer" ) +type Authorizer struct { + authEnforcer enforcer.Enforcer +} + func NewAuthorizer(enforcer enforcer.Enforcer) *Authorizer { return &Authorizer{authEnforcer: enforcer} } -type Authorizer struct { - authEnforcer enforcer.Enforcer +type Operation struct { + RequestPath string + RequestMethod []string } -var methodMapping = map[string]string{ - http.MethodGet: enforcer.ActionRead, - http.MethodHead: enforcer.ActionRead, - http.MethodPost: enforcer.ActionCreate, - http.MethodPut: enforcer.ActionUpdate, - http.MethodPatch: enforcer.ActionUpdate, - http.MethodDelete: enforcer.ActionDelete, - http.MethodConnect: enforcer.ActionRead, - http.MethodOptions: enforcer.ActionRead, - http.MethodTrace: enforcer.ActionRead, +var publicOperations = []Operation{ + {"/projects", []string{http.MethodGet, http.MethodPost}}, + {"/applications", []string{http.MethodGet}}, } +// AuthorizationMiddleware is a middleware that checks if the request is authorized. func (a *Authorizer) AuthorizationMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - - resource, err := a.getResource(r) - if err != nil { - jsonError( - w, - fmt.Sprintf("Error while checking authorization: %s", err), - http.StatusInternalServerError) + if !a.RequireAuthorization(r.URL.Path, r.Method) { + next.ServeHTTP(w, r) return } - - action := methodToAction(r.Method) + permission := a.GetPermission(r.URL.Path, r.Method) user := r.Header.Get("User-Email") - allowed, err := a.authEnforcer.Enforce(user, resource, action) + allowed, err := a.authEnforcer.IsUserGrantedPermission(r.Context(), user, permission) if err != nil { jsonError( w, @@ -52,10 +47,10 @@ func (a *Authorizer) AuthorizationMiddleware(next http.Handler) http.Handler { http.StatusInternalServerError) return } - if !*allowed { + if !allowed { jsonError( w, - fmt.Sprintf("%s is not authorized to execute %s on %s", user, action, resource), + fmt.Sprintf("%s does not have the permission:%s ", user, permission), http.StatusUnauthorized) return } @@ -64,23 +59,34 @@ func (a *Authorizer) AuthorizationMiddleware(next http.Handler) http.Handler { }) } -func (a *Authorizer) getResource(r *http.Request) (string, error) { - parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/"), "/") +// RequireAuthorization returns true if the request requires authorization. +func (a *Authorizer) RequireAuthorization(requestPath string, requestMethod string) bool { + if requestMethod == http.MethodOptions { + return false + } + + for _, operation := range publicOperations { + if operation.RequestPath == requestPath && slices.Contains(operation.RequestMethod, requestMethod) { + return false + } + } + return true +} + +// GetPermission returns the permission required to authorized a request. +// It's assumed that permission to request is a one to one mapping. +func (a *Authorizer) GetPermission(requestPath string, requestMethod string) string { + parts := strings.Split(strings.TrimPrefix(requestPath, "/"), "/") // Current paths registered in MLP are of the following format: + // - /projects // - /applications // - /projects/{project_id}/** - // Given this, we only care about the permissions up-to 2 levels deep. The rationale is that - // if a user has READ/WRITE permissions on /projects/{project_id}, they would also have the same - // permissions on all its sub-resources. Thus, trimming the resource identifier to aid quicker - // authz matching and to efficiently make use of the in-memory authz cache, if enabled. + // Only project sub-resources endpoint require permission. If a user has READ/WRITE permissions + // on /projects/{project_id}, they would also have the same permissions on all its sub-resources. if len(parts) > 1 { parts = parts[:2] } - return strings.Join(parts, ":"), nil -} - -func methodToAction(method string) string { - return methodMapping[method] + return fmt.Sprintf("mlp.%s.%s", strings.Join(parts, "."), strings.ToLower(requestMethod)) } func jsonError(w http.ResponseWriter, msg string, status int) { diff --git a/api/middleware/authorization_test.go b/api/middleware/authorization_test.go new file mode 100644 index 00000000..950ce508 --- /dev/null +++ b/api/middleware/authorization_test.go @@ -0,0 +1,45 @@ +package middleware + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + enforcerMock "github.com/caraml-dev/mlp/api/pkg/authz/enforcer/mocks" +) + +func TestAuthorizer_RequireAuthorization(t *testing.T) { + authorizer := NewAuthorizer(&enforcerMock.Enforcer{}) + tests := []struct { + name string + path string + method string + expected bool + }{ + {"All authenticated users can list projects", "/projects", "GET", false}, + {"All authenticated users can create new project", "/projects", "POST", false}, + {"All authenticated users can list applications", "/applications", "GET", false}, + {"Only authorized users can update project", "/projects/100", "PATCH", true}, + {"Options http request does not require authorization", "/projects/100", "OPTIONS", false}, + {"Only authorized users can access project sub resources", "/projects/100/secrets", "GET", true}, + } + for _, tt := range tests { + assert.Equal(t, tt.expected, authorizer.RequireAuthorization(tt.path, tt.method)) + } +} + +func TestAuthorizer_GetPermission(t *testing.T) { + authorizer := NewAuthorizer(&enforcerMock.Enforcer{}) + tests := []struct { + name string + path string + method string + expected string + }{ + {"project permission", "/projects/1003", "GET", "mlp.projects.1003.get"}, + {"project sub-resource permission", "/projects/1003/secrets", "GET", "mlp.projects.1003.get"}, + } + for _, tt := range tests { + assert.Equal(t, tt.expected, authorizer.GetPermission(tt.path, tt.method)) + } +} diff --git a/api/pkg/authz/enforcer/builder.go b/api/pkg/authz/enforcer/builder.go index d4c04560..2e030c92 100644 --- a/api/pkg/authz/enforcer/builder.go +++ b/api/pkg/authz/enforcer/builder.go @@ -1,57 +1,31 @@ package enforcer -import ( - "time" -) - // Builder builder of enforcer.Enforcer type Builder struct { - url string - product string - flavor Flavor - timeout time.Duration - cacheConfig *CacheConfig + ketoRemoteRead string + ketoRemoteWrite string + cacheConfig *CacheConfig } const ( - // DefaultURL default Keto server URL - DefaultURL = "http://localhost:4466" - // DefaultFlavor default Keto flavor to be used - DefaultFlavor = FlavorGlob - // DefaultTimeout maximum call duration to Keto Server before considered as timeout - DefaultTimeout = 5 * time.Second + // DefaultKetoRemoteRead default Keto remote read endpoint + DefaultKetoRemoteRead = "http://localhost:4466" + // DefaultKetoRemoteWrite default Keto remote write endpoint + DefaultKetoRemoteWrite = "http://localhost:4467" ) // NewEnforcerBuilder create new enforcer builder with all default parameters func NewEnforcerBuilder() *Builder { return &Builder{ - url: DefaultURL, - flavor: DefaultFlavor, - timeout: DefaultTimeout, + ketoRemoteRead: DefaultKetoRemoteRead, + ketoRemoteWrite: DefaultKetoRemoteWrite, } } -// Product set product name -func (b *Builder) Product(product string) *Builder { - b.product = product - return b -} - -// URL set Keto URL -func (b *Builder) URL(url string) *Builder { - b.url = url - return b -} - -// Flavor set Keto flavor -func (b *Builder) Flavor(flavor Flavor) *Builder { - b.flavor = flavor - return b -} - -// Timeout set timeout -func (b *Builder) Timeout(timeout time.Duration) *Builder { - b.timeout = timeout +// KetoEndpoints set Keto remote read and write endpoint +func (b *Builder) KetoEndpoints(ketoRemoteRead string, ketoRemoteWrite string) *Builder { + b.ketoRemoteRead = ketoRemoteRead + b.ketoRemoteWrite = ketoRemoteWrite return b } @@ -65,5 +39,5 @@ func (b *Builder) WithCaching(keyExpirySeconds int, cacheCleanUpIntervalSeconds // Build build an enforcer.Enforcer instance func (b *Builder) Build() (Enforcer, error) { - return newEnforcer(b.url, b.product, b.flavor, b.timeout, b.cacheConfig) + return newEnforcer(b.ketoRemoteRead, b.ketoRemoteWrite, b.cacheConfig) } diff --git a/api/pkg/authz/enforcer/cache.go b/api/pkg/authz/enforcer/cache.go index 6b277e73..bf22c046 100644 --- a/api/pkg/authz/enforcer/cache.go +++ b/api/pkg/authz/enforcer/cache.go @@ -2,26 +2,13 @@ package enforcer import ( "fmt" - "strconv" - "sync" "time" - "github.com/ory/keto-client-go/models" cache "github.com/patrickmn/go-cache" ) type InMemoryCache struct { store *cache.Cache - - mapLock sync.Mutex - - // Instead of caching the full names of the subject, resource and action, ids will be - // generated (using an incrementing counter) for each unique value. This will aid in - // generating smaller cache keys. - // The following maps store the mapping of the name -> internal id. - actionMap map[string]string - resourceMap map[string]string - subjectMap map[string]string } func newInMemoryCache(keyExpirySeconds int, cacheCleanUpIntervalSeconds int) *InMemoryCache { @@ -30,14 +17,13 @@ func newInMemoryCache(keyExpirySeconds int, cacheCleanUpIntervalSeconds int) *In time.Duration(keyExpirySeconds)*time.Second, time.Duration(cacheCleanUpIntervalSeconds)*time.Second, ), - actionMap: map[string]string{}, - resourceMap: map[string]string{}, - subjectMap: map[string]string{}, } } -func (c *InMemoryCache) LookUpPermission(input models.OryAccessControlPolicyAllowedInput) (*bool, bool) { - if cachedValue, ok := c.store.Get(c.buildCacheKey(input)); ok { +// LookUpUserPermissions returns the cached permission check result for a user / permission pair. +// The returned value indicates whether the result is cached. +func (c *InMemoryCache) LookUpUserPermissions(user string, permission string) (*bool, bool) { + if cachedValue, ok := c.store.Get(c.buildCacheKey(user, permission)); ok { if allowed, ok := cachedValue.(*bool); ok { return allowed, true } @@ -45,58 +31,11 @@ func (c *InMemoryCache) LookUpPermission(input models.OryAccessControlPolicyAllo return nil, false } -func (c *InMemoryCache) StorePermission(input models.OryAccessControlPolicyAllowedInput, isAllowed *bool) { - c.store.Set(c.buildCacheKey(input), isAllowed, cache.DefaultExpiration) +// StoreUserPermissions stores the permission check result for a user / permission pair. +func (c *InMemoryCache) StoreUserPermissions(user string, permission string, result bool) { + c.store.Set(c.buildCacheKey(user, permission), &result, cache.DefaultExpiration) } -func (c *InMemoryCache) buildCacheKey(input models.OryAccessControlPolicyAllowedInput) string { - return fmt.Sprintf("%s:%s:%s", - c.getActionID(input.Action), - c.getResourceID(input.Resource), - c.getSubjectID(input.Subject), - ) -} - -func (c *InMemoryCache) getSubjectID(name string) string { - c.mapLock.Lock() - defer c.mapLock.Unlock() - - if val, ok := c.subjectMap[name]; ok { - return val - } - newID := strconv.Itoa(countMapKeys(c.subjectMap) + 1) - c.subjectMap[name] = newID - return newID -} - -func (c *InMemoryCache) getResourceID(name string) string { - c.mapLock.Lock() - defer c.mapLock.Unlock() - - if val, ok := c.resourceMap[name]; ok { - return val - } - newID := strconv.Itoa(countMapKeys(c.resourceMap) + 1) - c.resourceMap[name] = newID - return newID -} - -func (c *InMemoryCache) getActionID(name string) string { - c.mapLock.Lock() - defer c.mapLock.Unlock() - - if val, ok := c.actionMap[name]; ok { - return val - } - newID := strconv.Itoa(countMapKeys(c.actionMap) + 1) - c.actionMap[name] = newID - return newID -} - -func countMapKeys(m map[string]string) int { - count := 0 - for range m { - count++ - } - return count +func (c *InMemoryCache) buildCacheKey(user string, permission string) string { + return fmt.Sprintf("%s-%s", user, permission) } diff --git a/api/pkg/authz/enforcer/cache_test.go b/api/pkg/authz/enforcer/cache_test.go index 9b6a3f5a..3072f13b 100644 --- a/api/pkg/authz/enforcer/cache_test.go +++ b/api/pkg/authz/enforcer/cache_test.go @@ -3,171 +3,49 @@ package enforcer import ( "testing" - "github.com/ory/keto-client-go/models" "github.com/stretchr/testify/assert" ) -func TestBuildCacheKey(t *testing.T) { - tests := map[string]struct { - cache *InMemoryCache - input models.OryAccessControlPolicyAllowedInput - expected string - }{ - "first item": { - cache: &InMemoryCache{ - actionMap: map[string]string{}, - resourceMap: map[string]string{}, - subjectMap: map[string]string{}, - }, - input: models.OryAccessControlPolicyAllowedInput{ - Action: "abc", - Resource: "def", - Subject: "xyz", - }, - expected: "1:1:1", - }, - "all items exist": { - cache: &InMemoryCache{ - actionMap: map[string]string{ - "abc": "2", - }, - resourceMap: map[string]string{ - "def": "4", - }, - subjectMap: map[string]string{ - "xyz": "10", - }, - }, - input: models.OryAccessControlPolicyAllowedInput{ - Action: "abc", - Resource: "def", - Subject: "xyz", - }, - expected: "2:4:10", - }, - "some items exist": { - cache: &InMemoryCache{ - actionMap: map[string]string{ - "abc": "0", - }, - resourceMap: map[string]string{ - "def": "4", - }, - subjectMap: map[string]string{ - "xyz": "0", - }, - }, - input: models.OryAccessControlPolicyAllowedInput{ - Action: "abc", - Resource: "abc", - Subject: "xyz", - }, - expected: "0:2:0", // Resource ID (missing) will be generated using the map key count - }, - } - - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - got := tt.cache.buildCacheKey(tt.input) - assert.Equal(t, tt.expected, got) - }) - } -} - -func TestLookUp(t *testing.T) { +func TestInMemoryCache_LookUpUserPermissions(t *testing.T) { + cache := newInMemoryCache(600, 600) + cache.StoreUserPermissions("user1@email.com", "mlp.projects.1.get", true) + cache.StoreUserPermissions("user1@email.com", "mlp.projects.1.post", false) trueValue, falseValue := true, false - type seedData struct { - item models.OryAccessControlPolicyAllowedInput - isAllowed *bool - } - tests := map[string]struct { - seedData []seedData - input models.OryAccessControlPolicyAllowedInput + user string + permission string expectedVal *bool expectedFound bool }{ - "item exists | true value": { - seedData: []seedData{ - { - item: models.OryAccessControlPolicyAllowedInput{ - Action: "abc", - Resource: "def", - Subject: "xyz", - }, - isAllowed: &trueValue, - }, - { - item: models.OryAccessControlPolicyAllowedInput{ - Action: "def", - Resource: "ghi", - Subject: "xyz", - }, - isAllowed: &falseValue, - }, - }, - input: models.OryAccessControlPolicyAllowedInput{ - Action: "abc", - Resource: "def", - Subject: "xyz", - }, + "cache hit | both user and permission match, result is true": { + user: "user1@email.com", + permission: "mlp.projects.1.get", expectedVal: &trueValue, expectedFound: true, }, - "item exists | false value": { - seedData: []seedData{ - { - item: models.OryAccessControlPolicyAllowedInput{ - Action: "abc", - Resource: "def", - Subject: "xyz", - }, - isAllowed: &trueValue, - }, - { - item: models.OryAccessControlPolicyAllowedInput{ - Action: "def", - Resource: "ghi", - Subject: "xyz", - }, - isAllowed: &falseValue, - }, - }, - input: models.OryAccessControlPolicyAllowedInput{ - Action: "def", - Resource: "ghi", - Subject: "xyz", - }, + "cache hit | both user and permission match, result is false": { + user: "user1@email.com", + permission: "mlp.projects.1.post", expectedVal: &falseValue, expectedFound: true, }, - "item does not exist": { - seedData: []seedData{ - { - item: models.OryAccessControlPolicyAllowedInput{ - Action: "abc", - Resource: "def", - Subject: "xyz", - }, - isAllowed: &trueValue, - }, - }, - input: models.OryAccessControlPolicyAllowedInput{ - Action: "def", - Resource: "ghi", - Subject: "xyz", - }, + "cache miss | user matches but permission doesn't": { + user: "user1@email.com", + permission: "mlp.projects.1.delete", + expectedVal: nil, + expectedFound: false, + }, + "cache miss | permission matches but user doesn't": { + user: "user2@email.com", + permission: "mlp.projects.1.get", + expectedVal: nil, expectedFound: false, }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { - cache := newInMemoryCache(60, 60) - for _, item := range tt.seedData { - cache.StorePermission(item.item, item.isAllowed) - } - cachedVal, found := cache.LookUpPermission(tt.input) + cachedVal, found := cache.LookUpUserPermissions(tt.user, tt.permission) assert.Equal(t, tt.expectedVal, cachedVal) assert.Equal(t, tt.expectedFound, found) }) diff --git a/api/pkg/authz/enforcer/enforcer.go b/api/pkg/authz/enforcer/enforcer.go index 131c4664..af6016f6 100644 --- a/api/pkg/authz/enforcer/enforcer.go +++ b/api/pkg/authz/enforcer/enforcer.go @@ -1,46 +1,28 @@ package enforcer import ( + "context" "fmt" - "net/url" - "regexp" - "sort" - "strings" "sync" - "time" - "github.com/ory/keto-client-go/client" - "github.com/ory/keto-client-go/client/engines" - "github.com/ory/keto-client-go/models" - - "github.com/caraml-dev/mlp/api/pkg/authz/enforcer/types" - "github.com/caraml-dev/mlp/api/util" -) - -const ( - // ActionCreate action to create a resource - ActionCreate = "actions:create" - // ActionRead action to read a resource - ActionRead = "actions:read" - // ActionUpdate action to update a resource - ActionUpdate = "actions:update" - // ActionDelete action to delete a resource - ActionDelete = "actions:delete" - // ActionAll all action - ActionAll = "actions:**" + ory "github.com/ory/keto-client-go" + "golang.org/x/exp/slices" + "golang.org/x/sync/errgroup" ) -// Flavor flavor type -type Flavor string - -const ( - // FlavorExact keto flavor using "exact" semantics - FlavorExact Flavor = "exact" - // FlavorGlob keto flavor using "glob" pattern matching - FlavorGlob Flavor = "glob" - // FlavorRegex keto flavor using "regex" pattern matching - FlavorRegex Flavor = "regex" -) +// Enforcer interface to enforce authorization +type Enforcer interface { + // IsUserGrantedPermission check whether user has the required permission, both directly and indirectly + IsUserGrantedPermission(ctx context.Context, user string, permission string) (bool, error) + // GetUserRoles get all roles directly associated with a user + GetUserRoles(ctx context.Context, user string) ([]string, error) + // GetRolePermissions get all permissions directly associated with a role + GetRolePermissions(ctx context.Context, role string) ([]string, error) + // GetRoleMembers get all members for a role + GetRoleMembers(ctx context.Context, role string) ([]string, error) + // UpdateAuthorization update authorization rules in batches + UpdateAuthorization(ctx context.Context, updateRequest AuthorizationUpdateRequest) error +} // CacheConfig holds the configuration for the in-memory cache, if enabled type CacheConfig struct { @@ -51,59 +33,34 @@ type CacheConfig struct { // MaxKeyExpirySeconds is the max allowed value for the KeyExpirySeconds. const MaxKeyExpirySeconds = 600 -// Enforcer thin client providing interface for authorizing users -type Enforcer interface { - // Enforce check whether user is authorized to do certain action against a resource - Enforce(user string, resource string, action string) (*bool, error) - // FilterAuthorizedResource filter and return list of authorized resource for certain user - FilterAuthorizedResource(user string, resources []string, action string) ([]string, error) - // GetRole get role with name - GetRole(roleName string) (*types.Role, error) - // GetPolicy get policy with name - GetPolicy(policyName string) (*types.Policy, error) - // UpsertRole create or update a role containing member as specified by users argument - UpsertRole(roleName string, users []string) (*types.Role, error) - // UpsertPolicy create or update a policy to allow subjects do actions against the specified resources - UpsertPolicy( - policyName string, - roles []string, - users []string, - resources []string, - actions []string, - ) (*types.Policy, error) -} - type enforcer struct { - cache *InMemoryCache - ketoClient *engines.Client - product string - flavor Flavor - timeout time.Duration + cache *InMemoryCache + ketoReadClient *ory.APIClient + ketoWriteClient *ory.APIClient } func newEnforcer( - hostURL string, - productName string, - flavor Flavor, - timeout time.Duration, + ketoRemoteRead string, + ketoRemoteWrite string, cacheConfig *CacheConfig, ) (*enforcer, error) { - u, err := url.ParseRequestURI(hostURL) - if err != nil { - return nil, err + readConfiguration := ory.NewConfiguration() + readConfiguration.Servers = []ory.ServerConfiguration{ + { + URL: ketoRemoteRead, + }, + } + writeConfiguration := ory.NewConfiguration() + writeConfiguration.Servers = []ory.ServerConfiguration{ + { + URL: ketoRemoteWrite, + }, } - client := client.NewHTTPClientWithConfig(nil, &client.TransportConfig{ - Host: u.Host, - BasePath: u.Path, - Schemes: []string{u.Scheme}, - }) - enforcer := &enforcer{ - ketoClient: client.Engines, - product: productName, - flavor: flavor, - timeout: timeout, + ketoReadClient: ory.NewAPIClient(readConfiguration), + ketoWriteClient: ory.NewAPIClient(writeConfiguration), } + if cacheConfig != nil { if cacheConfig.KeyExpirySeconds > MaxKeyExpirySeconds { return nil, fmt.Errorf("Configured KeyExpirySeconds is larger than the max permitted value of %d", @@ -114,244 +71,207 @@ func newEnforcer( return enforcer, nil } -// Enforce check whether user is authorized to do action against a resource -func (e *enforcer) Enforce(user string, resource string, action string) (*bool, error) { - user = e.formatUser(user) - resource = e.formatResource(resource) - - return e.isAllowed(user, resource, action) -} - -// GetRole get role with name -func (e *enforcer) GetRole(roleName string) (*types.Role, error) { - fmtRole := e.formatRole(roleName) - - params := &engines.GetOryAccessControlPolicyRoleParams{ - Flavor: string(e.flavor), - ID: fmtRole, +func (e *enforcer) IsUserGrantedPermission(ctx context.Context, user string, permission string) (bool, error) { + if e.isCacheEnabled() { + if isAllowed, found := e.cache.LookUpUserPermissions(user, permission); found { + return *isAllowed, nil + } } - res, err := e.ketoClient.GetOryAccessControlPolicyRole(params.WithTimeout(e.timeout)) + checkPermissionResult, _, err := e.ketoReadClient.PermissionApi.CheckPermission(ctx). + Namespace("Permission"). + Object(permission). + Relation("granted"). + SubjectSetNamespace("Subject"). + SubjectSetObject(user). + SubjectSetRelation(""). + Execute() if err != nil { - return nil, err + return false, err + } + userHasPermission := checkPermissionResult.Allowed + if e.isCacheEnabled() { + e.cache.StoreUserPermissions(user, permission, userHasPermission) } - return &types.Role{ - ID: res.GetPayload().ID, - Members: res.GetPayload().Members, - }, nil + return userHasPermission, nil } -// GetPolicy get policy with name -func (e *enforcer) GetPolicy(policyName string) (*types.Policy, error) { - fmtPolicy := e.formatPolicy(policyName) - params := &engines.GetOryAccessControlPolicyParams{ - Flavor: string(e.flavor), - ID: fmtPolicy, - } - res, err := e.ketoClient.GetOryAccessControlPolicy(params.WithTimeout(e.timeout)) +func (e *enforcer) GetUserRoles(ctx context.Context, user string) ([]string, error) { + roleRelationships, _, err := e.ketoReadClient.RelationshipApi.GetRelationships(ctx). + Namespace("Role"). + Relation("member"). + SubjectSetNamespace("Subject"). + SubjectSetRelation(""). + SubjectSetObject(user).Execute() if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get user roles: %w", err) + } + roles := make([]string, 0) + for _, tuple := range roleRelationships.RelationTuples { + roles = append(roles, tuple.Object) } - payload := res.GetPayload() - return &types.Policy{ - ID: payload.ID, - Actions: payload.Actions, - Resources: payload.Resources, - Subjects: payload.Subjects, - }, nil -} - -// FilterAuthorizedResource filter and return list of authorized resource for certain user -func (e *enforcer) FilterAuthorizedResource(user string, resources []string, action string) ([]string, error) { - user = e.formatUser(user) - var wg sync.WaitGroup - allowedResourcesConcurrent := util.ConcurrentSlice{} - errorsConcurrent := util.ConcurrentSlice{} - for _, resource := range resources { - wg.Add(1) - go func(u string, r string, a string) { - defer wg.Done() - r = e.formatResource(r) + return roles, nil +} - allowed, err := e.isAllowed(u, r, a) - if err != nil { - errorsConcurrent.Append(err) - return - } - if *allowed { - allowedResourcesConcurrent.Append(e.stripResourcePrefix(r)) - } - }(user, resource, action) +func (e *enforcer) GetRolePermissions(ctx context.Context, role string) ([]string, error) { + permissionRelationships, _, err := e.ketoReadClient.RelationshipApi.GetRelationships(ctx). + Namespace("Permission"). + Relation("granted"). + SubjectSetNamespace("Role"). + SubjectSetRelation("member"). + SubjectSetObject(role).Execute() + if err != nil { + return nil, err } - wg.Wait() - - errors := errorsConcurrent.GetItems() - if len(errors) > 0 { - return nil, errors[0].(error) + permissions := make([]string, 0) + for _, tuple := range permissionRelationships.RelationTuples { + permissions = append(permissions, tuple.Object) } - allowedResources := make([]string, 0) - for _, item := range allowedResourcesConcurrent.GetItems() { - allowedResources = append(allowedResources, item.(string)) - } - sort.Strings(allowedResources) - return allowedResources, nil + return permissions, nil } -// UpsertRole create or update a role containing member as specified by users argument -func (e *enforcer) UpsertRole(roleName string, users []string) (*types.Role, error) { - fmtRoleName := e.formatRole(roleName) - fmtUser := make([]string, 0) - for _, user := range users { - fmtUser = append(fmtUser, e.formatUser(user)) - } - - input := &models.OryAccessControlPolicyRole{ - ID: fmtRoleName, - Members: fmtUser, - } - params := &engines.UpsertOryAccessControlPolicyRoleParams{ - Body: input, - Flavor: string(e.flavor), - } - res, err := e.ketoClient.UpsertOryAccessControlPolicyRole(params.WithTimeout(e.timeout)) +func (e *enforcer) GetRoleMembers(ctx context.Context, role string) ([]string, error) { + expandedRole, _, err := e.ketoReadClient.PermissionApi.ExpandPermissions(ctx). + Namespace("Role"). + Relation("member"). + Object(role). + MaxDepth(2).Execute() if err != nil { return nil, err } - return &types.Role{ - ID: res.GetPayload().ID, - Members: res.GetPayload().Members, - }, nil -} - -// CreatePolicy create a policy to allow subject do an operation against the specified resource -func (e *enforcer) UpsertPolicy( - policyName string, - roles []string, - users []string, - resources []string, - actions []string, -) (*types.Policy, error) { - fmtPolicy := e.formatPolicy(policyName) - - fmtResources := make([]string, 0) - for _, res := range resources { - fmtResources = append(fmtResources, e.formatResource(res)) + members := make([]string, 0) + for _, child := range expandedRole.GetChildren() { + members = append(members, child.Tuple.SubjectSet.Object) } - fmtRoles := make([]string, 0) - for _, role := range roles { - fmtRoles = append(fmtRoles, e.formatRole(role)) - } + return members, nil +} - fmtUsers := make([]string, 0) - for _, user := range users { - fmtUsers = append(fmtUsers, e.formatUser(user)) +func newRolePermissionPatch(action string, permission string, role string) ory.RelationshipPatch { + return ory.RelationshipPatch{ + Action: &action, + RelationTuple: &ory.Relationship{ + Namespace: "Permission", + Object: permission, + Relation: "granted", + SubjectSet: ory.NewSubjectSet("Role", role, "member"), + }, } +} - input := &models.OryAccessControlPolicy{ - Actions: actions, - Effect: "allow", - ID: fmtPolicy, - Resources: fmtResources, - Subjects: append(fmtRoles, fmtUsers...), - } - params := &engines.UpsertOryAccessControlPolicyParams{ - Body: input, - Flavor: string(e.flavor), - } - res, err := e.ketoClient.UpsertOryAccessControlPolicy(params.WithTimeout(e.timeout)) - if err != nil { - return nil, err +func newRoleMemberPatch(action string, role string, member string) ory.RelationshipPatch { + return ory.RelationshipPatch{ + Action: &action, + RelationTuple: &ory.Relationship{ + Namespace: "Role", + Object: role, + Relation: "member", + SubjectSet: ory.NewSubjectSet("Subject", member, ""), + }, } - - payload := res.GetPayload() - - return &types.Policy{ - ID: payload.ID, - Subjects: payload.Subjects, - Resources: payload.Resources, - Actions: payload.Actions, - }, nil } -func (e *enforcer) isAllowed(user string, resource string, action string) (*bool, error) { - input := &models.OryAccessControlPolicyAllowedInput{ - Action: action, - Subject: user, - Resource: resource, +func (e *enforcer) UpdateAuthorization(ctx context.Context, updateRequest AuthorizationUpdateRequest) error { + var existingRolePermissions sync.Map + var existingRoleMembers sync.Map + getRelationsWorkersGroup := new(errgroup.Group) + for role := range updateRequest.RolePermissions { + updatedRole := role + getRelationsWorkersGroup.Go(func() error { + permissions, err := e.GetRolePermissions(ctx, updatedRole) + if err != nil { + return err + } + existingRolePermissions.Store(updatedRole, permissions) + return nil + }) } - params := &engines.DoOryAccessControlPoliciesAllowParams{ - Body: input, - Flavor: string(e.flavor), + for role := range updateRequest.RoleMembers { + updatedRole := role + getRelationsWorkersGroup.Go(func() error { + members, err := e.GetRoleMembers(ctx, updatedRole) + if err != nil { + return err + } + existingRoleMembers.Store(updatedRole, members) + return nil + }) + } + err := getRelationsWorkersGroup.Wait() + if err != nil { + return err } + patches := make([]ory.RelationshipPatch, 0) + existingRolePermissions.Range(func(key, value interface{}) bool { + role := key.(string) + permissions := value.([]string) + for _, permission := range permissions { + if !slices.Contains(updateRequest.RolePermissions[role], permission) { + patches = append(patches, newRolePermissionPatch("delete", permission, role)) + } + } + return true + }) - // If cache is set up, check there first - if e.isCacheEnabled() { - if isAllowed, found := e.cache.LookUpPermission(*input); found { - return isAllowed, nil + for role, permissions := range updateRequest.RolePermissions { + for _, permission := range permissions { + result, found := existingRolePermissions.Load(role) + existingPermissions := result.([]string) + if found && !slices.Contains(existingPermissions, permission) { + patches = append(patches, newRolePermissionPatch("insert", permission, role)) + } } } - res, err := e.ketoClient.DoOryAccessControlPoliciesAllow(params.WithTimeout(e.timeout)) - if err != nil { - switch d := err.(type) { - case *engines.DoOryAccessControlPoliciesAllowForbidden: - isAllowed := d.GetPayload().Allowed - // Save to cache and return - if e.isCacheEnabled() { - e.cache.StorePermission(*input, isAllowed) + existingRoleMembers.Range(func(key, value interface{}) bool { + role := key.(string) + members := value.([]string) + for _, member := range members { + if !slices.Contains(updateRequest.RoleMembers[role], member) { + patches = append(patches, newRoleMemberPatch("delete", role, member)) } - return isAllowed, nil - default: - return nil, err } - } + return true + }) - isAllowed := res.GetPayload().Allowed - // Save to cache and return - if e.isCacheEnabled() { - e.cache.StorePermission(*input, isAllowed) + for role, members := range updateRequest.RoleMembers { + for _, member := range members { + result, found := existingRoleMembers.Load(role) + existingMembers := result.([]string) + if found && !slices.Contains(existingMembers, member) { + patches = append(patches, newRoleMemberPatch("insert", role, member)) + } + } } - return isAllowed, nil -} -func (e *enforcer) formatUser(user string) string { - match, _ := regexp.MatchString("users:.*", user) - if match { - return user - } - return fmt.Sprintf("users:%s", user) + _, err = e.ketoWriteClient.RelationshipApi.PatchRelationships(ctx).RelationshipPatch(patches).Execute() + return err } -func (e *enforcer) formatResource(resource string) string { - match, _ := regexp.MatchString(fmt.Sprintf("resources:%s:.*", e.product), resource) - if match { - return resource - } - return fmt.Sprintf("resources:%s:%s", e.product, resource) +func (e *enforcer) isCacheEnabled() bool { + return e.cache != nil } -func (e *enforcer) formatRole(role string) string { - match, _ := regexp.MatchString(fmt.Sprintf("roles:%s:.*", e.product), role) - if match { - return role +func NewAuthorizationUpdateRequest() AuthorizationUpdateRequest { + return AuthorizationUpdateRequest{ + RolePermissions: make(map[string][]string), + RoleMembers: make(map[string][]string), } - return fmt.Sprintf("roles:%s:%s", e.product, role) } -func (e *enforcer) formatPolicy(policy string) string { - match, _ := regexp.MatchString(fmt.Sprintf("policies:%s:.*", e.product), policy) - if match { - return policy - } - return fmt.Sprintf("policies:%s:%s", e.product, policy) +type AuthorizationUpdateRequest struct { + RolePermissions map[string][]string + RoleMembers map[string][]string } -func (e *enforcer) stripResourcePrefix(resource string) string { - return strings.Replace(resource, fmt.Sprintf("resources:%s:", e.product), "", 1) +func (a AuthorizationUpdateRequest) UpdateRolePermissions(role string, + permissions []string) AuthorizationUpdateRequest { + a.RolePermissions[role] = permissions + return a } -func (e *enforcer) isCacheEnabled() bool { - return e.cache != nil +func (a AuthorizationUpdateRequest) UpdateRoleMembers(role string, members []string) AuthorizationUpdateRequest { + a.RoleMembers[role] = members + return a } diff --git a/api/pkg/authz/enforcer/enforcer_test.go b/api/pkg/authz/enforcer/enforcer_test.go index 5a0a351a..cfd6df53 100644 --- a/api/pkg/authz/enforcer/enforcer_test.go +++ b/api/pkg/authz/enforcer/enforcer_test.go @@ -1,98 +1,45 @@ package enforcer import ( - "os" + "context" + "fmt" + "sort" "testing" - "time" - "github.com/stretchr/testify/assert" - - "github.com/caraml-dev/mlp/api/pkg/authz/enforcer/types" + ory "github.com/ory/keto-client-go" + "github.com/stretchr/testify/require" ) -// These tests are run with real keto instance running on localhost:4466 -// Execute test using `make test` to spin up the necessary instances const ( - ProductName = "test" + ketoRemoteRead = "http://localhost:4466" + ketoRemoteWrite = "http://localhost:4467" ) -var KetoURL = getEnvOrDefault("KETO_URL", "http://localhost:4466") - -var BootstrapRoles = []types.Role{ - { - ID: "bootrap-role-1", - Members: []string{"user-1@example.com", "user-2@example.com"}, - }, - { - ID: "bootrap-role-2", - Members: []string{"user-3@example.com", "user-4@example.com"}, - }, -} - -// testPolicy is a structure that holds the input data for creating a policy -type testPolicy struct { - ID string - Roles []string - Users []string - Resources []string - Actions []string -} - -var BootstrapPolicy = []testPolicy{ - { - "bootstrap-policy-1", - []string{BootstrapRoles[0].ID}, - []string{}, - []string{"pages:1"}, - []string{ActionRead}, - }, - { - "bootstrap-policy-2", - []string{BootstrapRoles[1].ID}, - []string{}, - []string{"pages:**"}, - []string{ActionAll}, - }, - { - "bootstrap-policy-3", - []string{}, - []string{"users:**"}, - []string{"pages:10"}, - []string{ActionRead}, - }, -} - +// These tests are run with real keto instance running on localhost, with port 4466 and 4467 for read and write +// respectively. Execute test using `make test` to spin up the necessary instances func TestNewEnforcer(t *testing.T) { tests := map[string]struct { - hostURL string - productName string - flavor Flavor - timeout time.Duration - cacheConfig *CacheConfig + ketoRemoteRead string + ketoRemoteWrite string + cacheConfig *CacheConfig expectedError string }{ "success | no cache": { - hostURL: "http://authz.com", - productName: "mlp", - flavor: FlavorExact, - timeout: time.Second, + ketoRemoteRead: "http://localhost:4466", + ketoRemoteWrite: "http://localhost:4467", }, "success | with cache": { - hostURL: "http://authz.com", - productName: "mlp", - flavor: FlavorExact, - timeout: time.Second, + ketoRemoteRead: "http://localhost:4466", + ketoRemoteWrite: "http://localhost:4467", cacheConfig: &CacheConfig{ KeyExpirySeconds: 30, CacheCleanUpIntervalSeconds: 60, }, }, "failure | large cache expiry": { - hostURL: "http://authz.com", - productName: "mlp", - flavor: FlavorExact, - timeout: time.Second, + ketoRemoteRead: "http://localhost:4466", + ketoRemoteWrite: "http://localhost:4467", cacheConfig: &CacheConfig{ KeyExpirySeconds: 3000, CacheCleanUpIntervalSeconds: 60, @@ -103,208 +50,257 @@ func TestNewEnforcer(t *testing.T) { for name, tt := range tests { t.Run(name, func(t *testing.T) { - _, err := newEnforcer(tt.hostURL, tt.productName, tt.flavor, tt.timeout, tt.cacheConfig) + builder := NewEnforcerBuilder(). + KetoEndpoints(tt.ketoRemoteRead, tt.ketoRemoteWrite) + if tt.cacheConfig != nil { + builder.WithCaching(tt.cacheConfig.KeyExpirySeconds, tt.cacheConfig.CacheCleanUpIntervalSeconds) + } + _, err := builder.Build() if tt.expectedError != "" { - assert.EqualError(t, err, tt.expectedError) + require.EqualError(t, err, tt.expectedError) } else { - assert.NoError(t, err) + require.NoError(t, err) } }) } } -func TestEnforcer_Enforce(t *testing.T) { - enforcer, err := NewEnforcerBuilder().URL(KetoURL).Product(ProductName).Build() - assert.NoError(t, err) +func TestEnforcer_HasPermission(t *testing.T) { + ketoEnforcer, err := NewEnforcerBuilder().Build() + require.NoError(t, err) + readClient := newKetoClient(ketoRemoteRead) + writeClient := newKetoClient(ketoRemoteWrite) + clearRelations(readClient, writeClient) - err = initializeBootstrapRoles(enforcer, BootstrapRoles) - assert.NoError(t, err) - err = initializeBootstrapPolicies(enforcer, BootstrapPolicy) - assert.NoError(t, err) + newRoleAndPermissionsRequest := NewAuthorizationUpdateRequest() + newRoleAndPermissionsRequest.UpdateRolePermissions("page.1.admin", []string{"page.1.get", "page.1.put"}) + newRoleAndPermissionsRequest.UpdateRoleMembers("page.1.admin", []string{"user-1@example.com"}) + err = ketoEnforcer.UpdateAuthorization(context.Background(), newRoleAndPermissionsRequest) + require.NoError(t, err) tests := []struct { - name string - user string - resource string - action string - result bool + name string + permission string + user string + result bool }{ { - "allow: user-1 request read pages:1", + "allow: user-1 request read page.1", + "page.1.get", "user-1@example.com", - "pages:1", - ActionRead, true, }, { - "allow: user-3 request update pages:10", + "reject: user-3 request update page.1", + "page.1.put", "user-3@example.com", - "pages:10", - ActionUpdate, - true, + false, }, { - "allow: user-10 request read pages:10", - "user-10@example.com", - "pages:10", - ActionRead, - true, + "reject: user-1 request read unknown page number", + "page.99.put", + "user-1@example.com", + false, + }, + { + "reject: unknown user request read page 1", + "page.1.put", + "anonymous@example.com", + false, }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res, err := ketoEnforcer.IsUserGrantedPermission(context.Background(), tt.user, tt.permission) + require.NoError(t, err) + require.Equal(t, tt.result, res, "invalid enforce result") + }) + } + updateRoleAndPermissionsRequest := NewAuthorizationUpdateRequest() + updateRoleAndPermissionsRequest.UpdateRolePermissions("page.1.admin", []string{"page.1.get", "page.1.delete"}) + updateRoleAndPermissionsRequest.UpdateRoleMembers("page.1.admin", []string{"admin-1@example.com"}) + err = ketoEnforcer.UpdateAuthorization(context.Background(), updateRoleAndPermissionsRequest) + testsAfterUpdate := []struct { + name string + permission string + user string + result bool + }{ { - "reject: user-10 request update pages:10", - "user-10@example.com", - "pages:10", - ActionUpdate, + "reject after update: user-1 request read page.1", + "page.1.get", + "user-1@example.com", false, }, { - "reject: user-2 request read pages:2", - "user-2@example.com", - "pages:2", - ActionRead, + "allow after update: admin-1 request delete page.1", + "page.1.delete", + "admin-1@example.com", + true, + }, + { + "reject after update: admin-1 request update page.1", + "page.1.put", + "admin-1@example.com", false, }, } - for _, tt := range tests { + for _, tt := range testsAfterUpdate { t.Run(tt.name, func(t *testing.T) { - res, err := enforcer.Enforce(tt.user, tt.resource, tt.action) - assert.NoError(t, err) - assert.Equal(t, tt.result, *res, "invalid enforce result") + res, err := ketoEnforcer.IsUserGrantedPermission(context.Background(), tt.user, tt.permission) + require.NoError(t, err) + require.Equal(t, tt.result, res, "invalid enforce result") }) } -} -func TestEnforcer_FilterAuthorizedResource(t *testing.T) { - enforcer, err := NewEnforcerBuilder().URL(KetoURL).Product(ProductName).Build() - assert.NoError(t, err) + require.NoError(t, err) +} - err = initializeBootstrapRoles(enforcer, BootstrapRoles) - assert.NoError(t, err) - err = initializeBootstrapPolicies(enforcer, BootstrapPolicy) - assert.NoError(t, err) +func TestEnforcer_GetUserRoles(t *testing.T) { + ketoEnforcer, err := NewEnforcerBuilder().Build() + require.NoError(t, err) + readClient := newKetoClient(ketoRemoteRead) + writeClient := newKetoClient(ketoRemoteWrite) + clearRelations(readClient, writeClient) + updateRequest := NewAuthorizationUpdateRequest() + for i := 1; i < 4; i++ { + updateRequest.UpdateRoleMembers(fmt.Sprintf("pages.%d.reader", i), []string{"user-1@example.com"}) + } + err = ketoEnforcer.UpdateAuthorization(context.Background(), updateRequest) + require.NoError(t, err) tests := []struct { - name string - user string - resources []string - action string - expectedResources []string + name string + user string + expectedRoles []string }{ { - "user-1 only able to read pages:1", + "user-1 is reader for page 1, 2 and 3", "user-1@example.com", []string{ - "pages:1", - "pages:2", - "pages:3", - }, - ActionRead, - []string{ - "pages:1", + "pages.1.reader", + "pages.2.reader", + "pages.3.reader", }, }, { - "user-3 able to update all pages", - "user-3@example.com", - []string{ - "pages:1", - "pages:2", - "pages:3", - }, - ActionUpdate, - []string{ - "pages:1", - "pages:2", - "pages:3", - }, + "unknown user has no roles", + "anonymous@example.com", + []string{}, }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res, err := ketoEnforcer.GetUserRoles(context.Background(), tt.user) + require.NoError(t, err) + sort.Strings(tt.expectedRoles) + sort.Strings(res) + require.Equal(t, tt.expectedRoles, res) + }) + } +} + +func TestEnforcer_GetRolePermissions(t *testing.T) { + ketoEnforcer, err := NewEnforcerBuilder().Build() + require.NoError(t, err) + readClient := newKetoClient(ketoRemoteRead) + writeClient := newKetoClient(ketoRemoteWrite) + clearRelations(readClient, writeClient) + updateRequest := NewAuthorizationUpdateRequest() + updateRequest.UpdateRolePermissions("pages.1.reader", []string{"pages.1.get", "pages.1.post"}) + err = ketoEnforcer.UpdateAuthorization(context.Background(), updateRequest) + require.NoError(t, err) + tests := []struct { + name string + role string + expectedPermissions []string + }{ { - "user-10 able to read pages:10", - "user-10@example.com", + "pages.1.reader role can read and update page 1", + "pages.1.reader", []string{ - "pages:1", - "pages:10", - }, - ActionRead, - []string{ - "pages:10", + "pages.1.get", + "pages.1.post", }, }, + { + "unknown user has no roles", + "anonymous@example.com", + []string{}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - res, err := enforcer.FilterAuthorizedResource(tt.user, tt.resources, tt.action) - assert.NoError(t, err) - assert.Equal(t, tt.expectedResources, res) + res, err := ketoEnforcer.GetRolePermissions(context.Background(), tt.role) + require.NoError(t, err) + sort.Strings(tt.expectedPermissions) + sort.Strings(res) + require.Equal(t, tt.expectedPermissions, res) }) } } -func TestEnforcer_GetRole(t *testing.T) { - enforcer, err := NewEnforcerBuilder().URL(KetoURL).Product(ProductName).Timeout(5 * time.Second).Build() - assert.NoError(t, err) - - err = initializeBootstrapRoles(enforcer, BootstrapRoles) - assert.NoError(t, err) - err = initializeBootstrapPolicies(enforcer, BootstrapPolicy) - assert.NoError(t, err) - - role, err := enforcer.GetRole(BootstrapRoles[0].ID) - assert.NoError(t, err) - - assert.Equal(t, "roles:test:bootrap-role-1", role.ID) - assert.Equal(t, []string{"users:user-1@example.com", "users:user-2@example.com"}, role.Members) - - _, err = enforcer.GetRole("unknown-role") - assert.Error(t, err) -} - -func TestEnforcer_GetPolicy(t *testing.T) { - enforcer, err := NewEnforcerBuilder().URL(KetoURL).Product(ProductName).Timeout(5 * time.Second).Build() - assert.NoError(t, err) - - err = initializeBootstrapRoles(enforcer, BootstrapRoles) - assert.NoError(t, err) - err = initializeBootstrapPolicies(enforcer, BootstrapPolicy) - assert.NoError(t, err) - - policy, err := enforcer.GetPolicy(BootstrapPolicy[0].ID) - assert.NoError(t, err) - - assert.Equal(t, "policies:test:bootstrap-policy-1", policy.ID) - assert.Equal(t, []string{"roles:test:bootrap-role-1"}, policy.Subjects) - assert.Equal(t, []string{"resources:test:pages:1"}, policy.Resources) - assert.Equal(t, []string{ActionRead}, policy.Actions) - - _, err = enforcer.GetPolicy("unknown-policy") - assert.Error(t, err) +func TestEnforcer_GetRoleMembers(t *testing.T) { + ketoEnforcer, err := NewEnforcerBuilder().Build() + require.NoError(t, err) + readClient := newKetoClient(ketoRemoteRead) + writeClient := newKetoClient(ketoRemoteWrite) + clearRelations(readClient, writeClient) + updateRequest := NewAuthorizationUpdateRequest() + updateRequest.UpdateRolePermissions("pages.1.reader", []string{"pages.1.get", "pages.1.post"}) + err = ketoEnforcer.UpdateAuthorization(context.Background(), updateRequest) + require.NoError(t, err) + tests := []struct { + name string + role string + expectedPermissions []string + }{ + { + "pages.1.reader role can read and update page 1", + "pages.1.reader", + []string{ + "pages.1.get", + "pages.1.post", + }, + }, + { + "unknown user has no roles", + "anonymous@example.com", + []string{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res, err := ketoEnforcer.GetRolePermissions(context.Background(), tt.role) + require.NoError(t, err) + sort.Strings(tt.expectedPermissions) + sort.Strings(res) + require.Equal(t, tt.expectedPermissions, res) + }) + } } -func initializeBootstrapPolicies(e Enforcer, policies []testPolicy) error { - for _, policy := range policies { - _, err := e.UpsertPolicy(policy.ID, policy.Roles, policy.Users, policy.Resources, policy.Actions) - if err != nil { - return err - } +func newKetoClient(endpoint string) *ory.APIClient { + cfg := ory.NewConfiguration() + cfg.Servers = ory.ServerConfigurations{ + { + URL: endpoint, + }, } - return nil + return ory.NewAPIClient(cfg) } - -func initializeBootstrapRoles(e Enforcer, roles []types.Role) error { - for _, role := range roles { - _, err := e.UpsertRole(role.ID, role.Members) +func clearRelations(readClient *ory.APIClient, writeClient *ory.APIClient) { + ctx := context.Background() + resp, _, err := readClient.RelationshipApi.ListRelationshipNamespaces(ctx).Execute() + if err != nil { + panic(err) + } + for _, namespace := range resp.GetNamespaces() { + _, err := writeClient.RelationshipApi.DeleteRelationships(context.Background()).Namespace(namespace.GetName()). + Execute() if err != nil { - return err + panic(err) } } - return nil -} - -func getEnvOrDefault(env string, defaultValue string) string { - val, ok := os.LookupEnv(env) - if !ok { - return defaultValue - } - return val } diff --git a/api/pkg/authz/enforcer/mocks/enforcer.go b/api/pkg/authz/enforcer/mocks/enforcer.go index ab49f544..1074d4c7 100644 --- a/api/pkg/authz/enforcer/mocks/enforcer.go +++ b/api/pkg/authz/enforcer/mocks/enforcer.go @@ -1,11 +1,13 @@ -// Code generated by mockery v2.2.1. DO NOT EDIT. +// Code generated by mockery v2.32.0. DO NOT EDIT. package mocks import ( + context "context" + mock "github.com/stretchr/testify/mock" - types "github.com/caraml-dev/mlp/api/pkg/authz/enforcer/types" + enforcer "github.com/caraml-dev/mlp/api/pkg/authz/enforcer" ) // Enforcer is an autogenerated mock type for the Enforcer type @@ -13,22 +15,25 @@ type Enforcer struct { mock.Mock } -// Enforce provides a mock function with given fields: user, resource, action -func (_m *Enforcer) Enforce(user string, resource string, action string) (*bool, error) { - ret := _m.Called(user, resource, action) +// GetRoleMembers provides a mock function with given fields: ctx, role +func (_m *Enforcer) GetRoleMembers(ctx context.Context, role string) ([]string, error) { + ret := _m.Called(ctx, role) - var r0 *bool - if rf, ok := ret.Get(0).(func(string, string, string) *bool); ok { - r0 = rf(user, resource, action) + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]string, error)); ok { + return rf(ctx, role) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []string); ok { + r0 = rf(ctx, role) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*bool) + r0 = ret.Get(0).([]string) } } - var r1 error - if rf, ok := ret.Get(1).(func(string, string, string) error); ok { - r1 = rf(user, resource, action) + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, role) } else { r1 = ret.Error(1) } @@ -36,22 +41,25 @@ func (_m *Enforcer) Enforce(user string, resource string, action string) (*bool, return r0, r1 } -// FilterAuthorizedResource provides a mock function with given fields: user, resources, action -func (_m *Enforcer) FilterAuthorizedResource(user string, resources []string, action string) ([]string, error) { - ret := _m.Called(user, resources, action) +// GetRolePermissions provides a mock function with given fields: ctx, role +func (_m *Enforcer) GetRolePermissions(ctx context.Context, role string) ([]string, error) { + ret := _m.Called(ctx, role) var r0 []string - if rf, ok := ret.Get(0).(func(string, []string, string) []string); ok { - r0 = rf(user, resources, action) + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]string, error)); ok { + return rf(ctx, role) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []string); ok { + r0 = rf(ctx, role) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]string) } } - var r1 error - if rf, ok := ret.Get(1).(func(string, []string, string) error); ok { - r1 = rf(user, resources, action) + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, role) } else { r1 = ret.Error(1) } @@ -59,22 +67,25 @@ func (_m *Enforcer) FilterAuthorizedResource(user string, resources []string, ac return r0, r1 } -// GetPolicy provides a mock function with given fields: policyName -func (_m *Enforcer) GetPolicy(policyName string) (*types.Policy, error) { - ret := _m.Called(policyName) +// GetUserPermissions provides a mock function with given fields: ctx, user +func (_m *Enforcer) GetUserPermissions(ctx context.Context, user string) ([]string, error) { + ret := _m.Called(ctx, user) - var r0 *types.Policy - if rf, ok := ret.Get(0).(func(string) *types.Policy); ok { - r0 = rf(policyName) + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]string, error)); ok { + return rf(ctx, user) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []string); ok { + r0 = rf(ctx, user) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Policy) + r0 = ret.Get(0).([]string) } } - var r1 error - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(policyName) + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, user) } else { r1 = ret.Error(1) } @@ -82,22 +93,25 @@ func (_m *Enforcer) GetPolicy(policyName string) (*types.Policy, error) { return r0, r1 } -// GetRole provides a mock function with given fields: roleName -func (_m *Enforcer) GetRole(roleName string) (*types.Role, error) { - ret := _m.Called(roleName) +// GetUserRoles provides a mock function with given fields: ctx, user +func (_m *Enforcer) GetUserRoles(ctx context.Context, user string) ([]string, error) { + ret := _m.Called(ctx, user) - var r0 *types.Role - if rf, ok := ret.Get(0).(func(string) *types.Role); ok { - r0 = rf(roleName) + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]string, error)); ok { + return rf(ctx, user) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []string); ok { + r0 = rf(ctx, user) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Role) + r0 = ret.Get(0).([]string) } } - var r1 error - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(roleName) + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, user) } else { r1 = ret.Error(1) } @@ -105,22 +119,23 @@ func (_m *Enforcer) GetRole(roleName string) (*types.Role, error) { return r0, r1 } -// UpsertPolicy provides a mock function with given fields: policyName, roles, users, resources, actions -func (_m *Enforcer) UpsertPolicy(policyName string, roles []string, users []string, resources []string, actions []string) (*types.Policy, error) { - ret := _m.Called(policyName, roles, users, resources, actions) +// IsUserGrantedPermission provides a mock function with given fields: ctx, user, permission +func (_m *Enforcer) IsUserGrantedPermission(ctx context.Context, user string, permission string) (bool, error) { + ret := _m.Called(ctx, user, permission) - var r0 *types.Policy - if rf, ok := ret.Get(0).(func(string, []string, []string, []string, []string) *types.Policy); ok { - r0 = rf(policyName, roles, users, resources, actions) + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (bool, error)); ok { + return rf(ctx, user, permission) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) bool); ok { + r0 = rf(ctx, user, permission) } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Policy) - } + r0 = ret.Get(0).(bool) } - var r1 error - if rf, ok := ret.Get(1).(func(string, []string, []string, []string, []string) error); ok { - r1 = rf(policyName, roles, users, resources, actions) + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, user, permission) } else { r1 = ret.Error(1) } @@ -128,25 +143,30 @@ func (_m *Enforcer) UpsertPolicy(policyName string, roles []string, users []stri return r0, r1 } -// UpsertRole provides a mock function with given fields: roleName, users -func (_m *Enforcer) UpsertRole(roleName string, users []string) (*types.Role, error) { - ret := _m.Called(roleName, users) +// UpdateAuthorization provides a mock function with given fields: ctx, updateRequest +func (_m *Enforcer) UpdateAuthorization(ctx context.Context, updateRequest enforcer.AuthorizationUpdateRequest) error { + ret := _m.Called(ctx, updateRequest) - var r0 *types.Role - if rf, ok := ret.Get(0).(func(string, []string) *types.Role); ok { - r0 = rf(roleName, users) + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, enforcer.AuthorizationUpdateRequest) error); ok { + r0 = rf(ctx, updateRequest) } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Role) - } + r0 = ret.Error(0) } - var r1 error - if rf, ok := ret.Get(1).(func(string, []string) error); ok { - r1 = rf(roleName, users) - } else { - r1 = ret.Error(1) - } + return r0 +} - return r0, r1 +// NewEnforcer creates a new instance of Enforcer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewEnforcer(t interface { + mock.TestingT + Cleanup(func()) +}) *Enforcer { + mock := &Enforcer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock } diff --git a/api/pkg/authz/enforcer/types/policy.go b/api/pkg/authz/enforcer/types/policy.go deleted file mode 100644 index ba2addcb..00000000 --- a/api/pkg/authz/enforcer/types/policy.go +++ /dev/null @@ -1,9 +0,0 @@ -package types - -// Policy authorization policy -type Policy struct { - ID string - Subjects []string - Resources []string - Actions []string -} diff --git a/api/pkg/authz/enforcer/types/role.go b/api/pkg/authz/enforcer/types/role.go deleted file mode 100644 index cff20f38..00000000 --- a/api/pkg/authz/enforcer/types/role.go +++ /dev/null @@ -1,7 +0,0 @@ -package types - -// Role authorization role -type Role struct { - ID string - Members []string -} diff --git a/api/service/projects_service.go b/api/service/projects_service.go index ff688df7..1e42b106 100644 --- a/api/service/projects_service.go +++ b/api/service/projects_service.go @@ -1,9 +1,12 @@ package service import ( + "context" "fmt" "strings" + "golang.org/x/exp/slices" + "github.com/pkg/errors" "github.com/caraml-dev/mlp/api/repository" @@ -13,9 +16,9 @@ import ( ) type ProjectsService interface { - ListProjects(name string) ([]*models.Project, error) - CreateProject(project *models.Project) (*models.Project, error) - UpdateProject(project *models.Project) (*models.Project, error) + ListProjects(ctx context.Context, name string, user string) ([]*models.Project, error) + CreateProject(ctx context.Context, project *models.Project) (*models.Project, error) + UpdateProject(ctx context.Context, project *models.Project) (*models.Project, error) FindByID(projectID models.ID) (*models.Project, error) FindByName(projectName string) (*models.Project, error) } @@ -28,11 +31,6 @@ var reservedProjectName = map[string]bool{ "knative-monitoring": true, } -const ( - ProjectSubResources = "projects:%s:**" - ProjectResources = "projects:%s" -) - func NewProjectsService( mlflowURL string, projectRepository repository.ProjectRepository, @@ -57,7 +55,7 @@ type projectsService struct { authEnabled bool } -func (service *projectsService) CreateProject(project *models.Project) (*models.Project, error) { +func (service *projectsService) CreateProject(ctx context.Context, project *models.Project) (*models.Project, error) { if _, ok := reservedProjectName[project.Name]; ok { return nil, fmt.Errorf("unable to use reserved project name: %s", project.Name) } @@ -72,7 +70,7 @@ func (service *projectsService) CreateProject(project *models.Project) (*models. } if service.authEnabled { - err = service.upsertAuthorizationPolicy(project) + err = service.updateAuthorizationPolicy(ctx, project) if err != nil { return nil, fmt.Errorf("error while creating authorization policy for project %s", project.Name) } @@ -81,13 +79,21 @@ func (service *projectsService) CreateProject(project *models.Project) (*models. return project, nil } -func (service *projectsService) ListProjects(name string) (projects []*models.Project, err error) { - return service.projectRepository.ListProjects(name) +func (service *projectsService) ListProjects(ctx context.Context, name string, user string) (projects []*models.Project, + err error) { + allProjects, err := service.projectRepository.ListProjects(name) + if err != nil { + return nil, err + } + if service.authEnabled { + return service.filterAuthorizedProjects(ctx, allProjects, user) + } + return allProjects, nil } -func (service *projectsService) UpdateProject(project *models.Project) (*models.Project, error) { +func (service *projectsService) UpdateProject(ctx context.Context, project *models.Project) (*models.Project, error) { if service.authEnabled { - err := service.upsertAuthorizationPolicy(project) + err := service.updateAuthorizationPolicy(ctx, project) if err != nil { return nil, fmt.Errorf("error while updating authorization policy for project %s", project.Name) } @@ -112,72 +118,91 @@ func (service *projectsService) save(project *models.Project) (*models.Project, return service.projectRepository.Save(project) } -func (service *projectsService) upsertAuthorizationPolicy(project *models.Project) error { - // create administrators policy - adminRole, err := service.upsertAdministratorsRole(project) - if err != nil { - return err - } - err = service.upsertAdministratorsPolicy(adminRole, project) - if err != nil { - return err +func projectReaderRole(project *models.Project) string { + return fmt.Sprintf("mlp.projects.%d.reader", project.ID) +} + +func projectAdminRole(project *models.Project) string { + return fmt.Sprintf("mlp.projects.%d.administrator", project.ID) +} + +func rolesWithReadOnlyAccess(project *models.Project) []string { + predefinedRoles := []string{ + "mlp.projects.reader", } + return append(predefinedRoles, projectReaderRole(project)) +} - // create readers policy - readersRole, err := service.upsertReadersRole(project) - if err != nil { - return err +func rolesWithAdminAccess(project *models.Project) []string { + predefinedRoles := []string{ + "mlp.administrator", } - err = service.upsertReadersPolicy(readersRole, project) - if err != nil { - return err + return append(predefinedRoles, projectAdminRole(project)) +} + +func readPermissions(project *models.Project) []string { + permissions := make([]string, 0) + for _, method := range []string{"get"} { + permissions = append(permissions, fmt.Sprintf("mlp.projects.%d.%s", project.ID, method)) } + return permissions +} - return nil +func adminPermissions(project *models.Project) []string { + permissions := make([]string, 0) + for _, method := range []string{"get", "put", "post", "patch", "delete"} { + permissions = append(permissions, fmt.Sprintf("mlp.projects.%d.%s", project.ID, method)) + } + return permissions } -func (service *projectsService) upsertReadersRole(project *models.Project) (string, error) { - roleName := fmt.Sprintf("%s-%s", project.Name, "readers") - role, err := service.authEnforcer.UpsertRole(roleName, project.Readers) - if err != nil { - return "", err +func (service *projectsService) updateAuthorizationPolicy(ctx context.Context, project *models.Project) error { + updateRequest := enforcer.NewAuthorizationUpdateRequest() + for _, role := range rolesWithReadOnlyAccess(project) { + updateRequest.UpdateRolePermissions(role, readPermissions(project)) + } + if project.Administrators != nil { + updateRequest.UpdateRoleMembers(projectAdminRole(project), project.Administrators) + } else { + updateRequest.UpdateRoleMembers(projectAdminRole(project), []string{}) } - return role.ID, nil + + for _, role := range rolesWithAdminAccess(project) { + updateRequest.UpdateRolePermissions(role, adminPermissions(project)) + } + if project.Readers != nil { + updateRequest.UpdateRoleMembers(projectReaderRole(project), project.Readers) + } else { + updateRequest.UpdateRoleMembers(projectReaderRole(project), []string{}) + } + + return service.authEnforcer.UpdateAuthorization(ctx, updateRequest) } -func (service *projectsService) upsertAdministratorsRole(project *models.Project) (string, error) { - roleName := fmt.Sprintf("%s-%s", project.Name, "administrators") - policy, err := service.authEnforcer.UpsertRole(roleName, project.Administrators) +func (service *projectsService) filterAuthorizedProjects(ctx context.Context, projects []*models.Project, + user string) ([]*models.Project, error) { + if user == "" { + return nil, fmt.Errorf("authorization is enabled but user is not provided") + } + + roles, err := service.authEnforcer.GetUserRoles(ctx, user) if err != nil { - return "", err - } - return policy.ID, nil -} - -func (service *projectsService) upsertAdministratorsPolicy(role string, project *models.Project) error { - subResources := fmt.Sprintf(ProjectSubResources, project.ID) - resource := fmt.Sprintf(ProjectResources, project.ID) - nameResource := fmt.Sprintf(ProjectResources, project.Name) - policyName := fmt.Sprintf("%s-administrators-policy", project.Name) - _, err := service.authEnforcer.UpsertPolicy( - policyName, - []string{role}, - []string{}, - []string{resource, subResources, nameResource}, - []string{enforcer.ActionAll}) - return err -} - -func (service *projectsService) upsertReadersPolicy(role string, project *models.Project) error { - subResources := fmt.Sprintf(ProjectSubResources, project.ID) - resource := fmt.Sprintf(ProjectResources, project.ID) - nameResource := fmt.Sprintf(ProjectResources, project.Name) - policyName := fmt.Sprintf("%s-readers-policy", project.Name) - _, err := service.authEnforcer.UpsertPolicy( - policyName, - []string{role}, - []string{}, - []string{resource, subResources, nameResource}, - []string{enforcer.ActionRead}) - return err + return nil, err + } + for _, role := range roles { + if slices.Contains([]string{"mlp.administrator", "mlp.projects.reader"}, role) { + return projects, nil + } + } + if err != nil { + return nil, err + } + authorizedProjects := make([]*models.Project, 0) + for _, project := range projects { + if (project.Administrators != nil && slices.Contains(project.Administrators, user)) || + (project.Readers != nil && slices.Contains(project.Readers, user)) { + authorizedProjects = append(authorizedProjects, project) + } + } + return authorizedProjects, nil } diff --git a/api/service/projects_service_test.go b/api/service/projects_service_test.go index 5522089b..b6c9c132 100644 --- a/api/service/projects_service_test.go +++ b/api/service/projects_service_test.go @@ -1,29 +1,33 @@ package service import ( - "fmt" + "context" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" "github.com/caraml-dev/mlp/api/pkg/authz/enforcer" - enforcerMock "github.com/caraml-dev/mlp/api/pkg/authz/enforcer/mocks" - "github.com/caraml-dev/mlp/api/pkg/authz/enforcer/types" + + "github.com/stretchr/testify/assert" "github.com/caraml-dev/mlp/api/models" "github.com/caraml-dev/mlp/api/repository/mocks" + + enforcerMock "github.com/caraml-dev/mlp/api/pkg/authz/enforcer/mocks" ) const MLFlowTrackingURL = "http://localhost:5555" func TestProjectsService_CreateProject(t *testing.T) { tests := []struct { - name string - arg *models.Project - authEnabled bool - expResult *models.Project - wantError bool - wantErrorMsg string + name string + arg *models.Project + authEnabled bool + expResult *models.Project + expAuthUpdate *enforcer.AuthorizationUpdateRequest + wantError bool + wantErrorMsg string }{ { "success: auth enabled", @@ -41,6 +45,20 @@ func TestProjectsService_CreateProject(t *testing.T) { Administrators: []string{"user@email.com"}, Readers: nil, }, + &enforcer.AuthorizationUpdateRequest{ + RolePermissions: map[string][]string{ + "mlp.administrator": {"mlp.projects.1.get", "mlp.projects.1.put", "mlp.projects.1.post", + "mlp.projects.1.patch", "mlp.projects.1.delete"}, + "mlp.projects.reader": {"mlp.projects.1.get"}, + "mlp.projects.1.reader": {"mlp.projects.1.get"}, + "mlp.projects.1.administrator": {"mlp.projects.1.get", "mlp.projects.1.put", "mlp.projects.1.post", + "mlp.projects.1.patch", "mlp.projects.1.delete"}, + }, + RoleMembers: map[string][]string{ + "mlp.projects.1.reader": {}, + "mlp.projects.1.administrator": {"user@email.com"}, + }, + }, false, "", }, @@ -60,6 +78,7 @@ func TestProjectsService_CreateProject(t *testing.T) { Administrators: []string{"user@email.com"}, Readers: nil, }, + nil, false, "", }, @@ -73,6 +92,7 @@ func TestProjectsService_CreateProject(t *testing.T) { }, false, nil, + nil, true, "unable to use reserved project name: infrastructure", }, @@ -82,112 +102,89 @@ func TestProjectsService_CreateProject(t *testing.T) { storage := &mocks.ProjectRepository{} storage.On("Save", tt.expResult).Return(tt.expResult, nil) - projectResource := fmt.Sprintf(ProjectResources, tt.arg.ID) - projectSubResource := fmt.Sprintf(ProjectSubResources, tt.arg.ID) - projectNameResource := fmt.Sprintf(ProjectResources, tt.arg.Name) - authEnforcer := &enforcerMock.Enforcer{} - if tt.authEnabled { - authEnforcer.On( - "UpsertRole", - fmt.Sprintf("%s-administrators", tt.arg.Name), - []string(tt.arg.Administrators), - ).Return(&types.Role{ - ID: "admin-role", - Members: tt.arg.Administrators, - }, nil) + projectsService, err := NewProjectsService(MLFlowTrackingURL, storage, authEnforcer, tt.authEnabled) + require.NoError(t, err) - authEnforcer.On( - "UpsertRole", - fmt.Sprintf("%s-readers", tt.arg.Name), - []string(tt.arg.Readers), - ).Return(&types.Role{ - ID: "reader-role", - Members: tt.arg.Readers, - }, nil) - authEnforcer.On( - "UpsertPolicy", - fmt.Sprintf("%s-administrators-policy", tt.arg.Name), - []string{"admin-role"}, - []string{}, - []string{projectResource, projectSubResource, projectNameResource}, - []string{enforcer.ActionAll}, - ).Return(&types.Policy{ - ID: "admin-policy", - Subjects: []string{"admin-role"}, - Resources: []string{projectResource, projectSubResource, projectNameResource}, - Actions: []string{enforcer.ActionAll}, - }, nil) - authEnforcer.On( - "UpsertPolicy", - fmt.Sprintf("%s-readers-policy", tt.arg.Name), - []string{"reader-role"}, - []string{}, - []string{projectResource, projectSubResource, projectNameResource}, - []string{enforcer.ActionRead}, - ).Return(&types.Policy{ - ID: "reader-policy", - Subjects: []string{"readers-role"}, - Resources: []string{projectResource, projectSubResource, projectNameResource}, - Actions: []string{enforcer.ActionRead}, - }, nil) + if tt.expAuthUpdate != nil { + authEnforcer.On("UpdateAuthorization", mock.Anything, *tt.expAuthUpdate).Return(nil) } - projectsService, err := NewProjectsService(MLFlowTrackingURL, storage, authEnforcer, tt.authEnabled) - assert.NoError(t, err) - - res, err := projectsService.CreateProject(tt.arg) + res, err := projectsService.CreateProject(context.Background(), tt.arg) if tt.wantError { - assert.Error(t, err) - assert.Equal(t, tt.wantErrorMsg, err.Error()) + require.Error(t, err) + require.Equal(t, tt.wantErrorMsg, err.Error()) return } - assert.NoError(t, err) - assert.Equal(t, tt.expResult, res) + require.NoError(t, err) + require.Equal(t, tt.expResult, res) storage.AssertExpectations(t) - authEnforcer.AssertExpectations(t) + + if tt.expAuthUpdate != nil { + authEnforcer.AssertExpectations(t) + } }) } } func TestProjectsService_UpdateProject(t *testing.T) { tests := []struct { - name string - arg *models.Project - authEnabled bool - expResult *models.Project + name string + arg *models.Project + authEnabled bool + expResult *models.Project + expAuthUpdate *enforcer.AuthorizationUpdateRequest }{ { "success: auth enabled", &models.Project{ + ID: 1, Name: "my-project", Administrators: []string{"user@email.com"}, Readers: nil, }, true, &models.Project{ + ID: 1, Name: "my-project", MLFlowTrackingURL: MLFlowTrackingURL, Administrators: []string{"user@email.com"}, Readers: nil, }, + &enforcer.AuthorizationUpdateRequest{ + RolePermissions: map[string][]string{ + "mlp.administrator": {"mlp.projects.1.get", "mlp.projects.1.put", "mlp.projects.1.post", + "mlp.projects.1.patch", "mlp.projects.1.delete"}, + "mlp.projects.reader": {"mlp.projects.1.get"}, + "mlp.projects.1.reader": {"mlp.projects.1.get"}, + "mlp.projects.1.administrator": {"mlp.projects.1.get", "mlp.projects.1.put", "mlp.projects.1.post", + "mlp.projects.1.patch", "mlp.projects.1.delete"}, + }, + RoleMembers: map[string][]string{ + "mlp.projects.1.reader": {}, + "mlp.projects.1.administrator": {"user@email.com"}, + }, + }, }, { "success: auth disabled", &models.Project{ + ID: 1, Name: "my-project", Administrators: []string{"user@email.com"}, Readers: nil, }, false, &models.Project{ + ID: 1, Name: "my-project", MLFlowTrackingURL: MLFlowTrackingURL, Administrators: []string{"user@email.com"}, Readers: nil, }, + nil, }, } for _, tt := range tests { @@ -196,62 +193,16 @@ func TestProjectsService_UpdateProject(t *testing.T) { storage.On("Save", tt.expResult).Return(tt.expResult, nil) authEnforcer := &enforcerMock.Enforcer{} - if tt.authEnabled { - - projectResource := fmt.Sprintf(ProjectResources, tt.arg.ID) - projectSubResource := fmt.Sprintf(ProjectSubResources, tt.arg.ID) - projectNameResource := fmt.Sprintf(ProjectResources, tt.arg.Name) - - authEnforcer.On( - "UpsertRole", - fmt.Sprintf("%s-administrators", tt.arg.Name), - []string(tt.arg.Administrators), - ).Return(&types.Role{ - ID: "admin-role", - Members: tt.arg.Administrators, - }, nil) - authEnforcer.On( - "UpsertRole", - fmt.Sprintf("%s-readers", tt.arg.Name), - []string(tt.arg.Readers), - ).Return(&types.Role{ - ID: "reader-role", - Members: tt.arg.Readers, - }, nil) - authEnforcer.On( - "UpsertPolicy", - fmt.Sprintf("%s-administrators-policy", tt.arg.Name), - []string{"admin-role"}, - []string{}, - []string{projectResource, projectSubResource, projectNameResource}, - []string{enforcer.ActionAll}, - ).Return(&types.Policy{ - ID: "admin-policy", - Subjects: []string{"admin-role"}, - Resources: []string{projectResource, projectSubResource, projectNameResource}, - Actions: []string{enforcer.ActionAll}, - }, nil) - authEnforcer.On( - "UpsertPolicy", - fmt.Sprintf("%s-readers-policy", tt.arg.Name), - []string{"reader-role"}, - []string{}, - []string{projectResource, projectSubResource, projectNameResource}, - []string{enforcer.ActionRead}, - ).Return(&types.Policy{ - ID: "reader-policy", - Subjects: []string{"readers-role"}, - Resources: []string{projectResource, projectSubResource, projectNameResource}, - Actions: []string{enforcer.ActionRead}, - }, nil) + if tt.expAuthUpdate != nil { + authEnforcer.On("UpdateAuthorization", mock.Anything, *tt.expAuthUpdate).Return(nil) } projectsService, err := NewProjectsService(MLFlowTrackingURL, storage, authEnforcer, tt.authEnabled) assert.NoError(t, err) - res, err := projectsService.UpdateProject(tt.arg) - assert.NoError(t, err) - assert.Equal(t, tt.expResult, res) + res, err := projectsService.UpdateProject(context.Background(), tt.arg) + require.NoError(t, err) + require.Equal(t, tt.expResult, res) storage.AssertExpectations(t) authEnforcer.AssertExpectations(t) @@ -260,28 +211,100 @@ func TestProjectsService_UpdateProject(t *testing.T) { } func TestProjectsService_ListProjects(t *testing.T) { - projectFilter := "my-project" + project1 := &models.Project{ + ID: 1, + Name: "project-1", + MLFlowTrackingURL: MLFlowTrackingURL, + Administrators: []string{"admin-1@email.com"}, + Readers: []string{"reader-1@email.com"}, + } + project2 := &models.Project{ + ID: 2, + Name: "project-2", + MLFlowTrackingURL: MLFlowTrackingURL, + Administrators: []string{"admin-2@email.com"}, + Readers: []string{"reader-2@email.com"}, + } + allProjects := []*models.Project{project1, project2} + storage := &mocks.ProjectRepository{} + storage.On("ListProjects", "project-").Return(allProjects, nil) - exp := []*models.Project{ + tests := []struct { + name string + projectFilter string + authEnabled bool + expResult []*models.Project + user string + userRoles []string + }{ { - Name: "my-project", - MLFlowTrackingURL: MLFlowTrackingURL, - Administrators: []string{"user@email.com"}, - Readers: nil, + "filter only by project name when auth is disabled", + "project-", + false, + allProjects, + "anonymous@email.com", + nil, + }, + { + "filter by permission and project name when auth is enabled", + "project-", + true, + []*models.Project{}, + "anonymous-user@email.com", + []string{}, + }, + { + "allow project admin to read project, regardless of user roles return by enforcer", + "project-", + true, + []*models.Project{project1}, + "admin-1@email.com", + []string{"some roles"}, + }, + { + "allow project reader to read project, regardless of user roles return by enforcer", + "project-", + true, + []*models.Project{project2}, + "reader-2@email.com", + []string{"some roles"}, + }, + { + "allow mlp administrators to read all projects", + "project-", + true, + allProjects, + "mlp-admin@email.com", + []string{"mlp.administrator"}, + }, + { + "allow project readers to read all projects", + "project-", + true, + allProjects, + "project-reader@email.com", + []string{"mlp.projects.reader"}, }, } - storage := &mocks.ProjectRepository{} - storage.On("ListProjects", projectFilter).Return(exp, nil) - authEnforcer := &enforcerMock.Enforcer{} - projectsService, err := NewProjectsService(MLFlowTrackingURL, storage, authEnforcer, false) - assert.NoError(t, err) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + authEnforcer := &enforcerMock.Enforcer{} + if tt.authEnabled { + authEnforcer.On("GetUserRoles", mock.Anything, tt.user).Return(tt.userRoles, nil) + } - res, err := projectsService.ListProjects(projectFilter) - assert.NoError(t, err) - assert.Equal(t, exp, res) + projectsService, err := NewProjectsService(MLFlowTrackingURL, storage, authEnforcer, tt.authEnabled) + assert.NoError(t, err) - storage.AssertExpectations(t) + res, err := projectsService.ListProjects(context.Background(), "project-", tt.user) + assert.NoError(t, err) + assert.Equal(t, tt.expResult, res) + + storage.AssertExpectations(t) + authEnforcer.AssertExpectations(t) + }) + } } func TestProjectsService_FindById(t *testing.T) { diff --git a/config-keto-dev.yaml b/config-keto-dev.yaml new file mode 100644 index 00000000..e4c52fea --- /dev/null +++ b/config-keto-dev.yaml @@ -0,0 +1,15 @@ +namespaces: + - id: 0 + name: Subject + - id: 1 + name: Role + - id: 2 + name: Permission +dsn: memory +serve: + read: + host: 0.0.0.0 + port: 4466 + write: + host: 0.0.0.0 + port: 4467 diff --git a/docker-compose.yaml b/docker-compose.yaml index cea746d2..5aa6ea67 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -23,13 +23,15 @@ services: - POSTGRESQL_DATABASE=mlp keto: - image: oryd/keto:v0.4 + image: oryd/keto:v0.11 ports: - 4466:4466 - environment: - - DSN=memory - command: - - serve + - 4467:4467 + command: serve -c /home/ory/keto.yaml + volumes: + - type: bind + source: ./config-keto-dev.yaml + target: /home/ory/keto.yaml vault: image: hashicorp/vault diff --git a/go.mod b/go.mod index 58851832..6303b456 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/lib/pq v1.3.0 github.com/newrelic/go-agent v3.19.2+incompatible github.com/opentracing/opentracing-go v1.1.0 - github.com/ory/keto-client-go v0.4.4-alpha.1 + github.com/ory/keto-client-go v0.11.0-alpha.0 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.11.1 @@ -35,8 +35,11 @@ require ( github.com/stretchr/testify v1.8.1 github.com/uber/jaeger-client-go v2.16.0+incompatible go.uber.org/zap v1.17.0 - golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 + golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 + golang.org/x/oauth2 v0.5.0 + golang.org/x/sync v0.1.0 google.golang.org/api v0.106.0 + google.golang.org/grpc v1.52.3 gopkg.in/errgo.v2 v2.1.0 gopkg.in/yaml.v2 v2.4.0 k8s.io/client-go v0.26.0 @@ -60,17 +63,6 @@ require ( github.com/fatih/structs v1.1.0 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/go-logr/logr v1.2.3 // indirect - github.com/go-openapi/analysis v0.19.5 // indirect - github.com/go-openapi/errors v0.19.4 // indirect - github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/jsonreference v0.20.0 // indirect - github.com/go-openapi/loads v0.19.4 // indirect - github.com/go-openapi/runtime v0.19.15 // indirect - github.com/go-openapi/spec v0.19.3 // indirect - github.com/go-openapi/strfmt v0.19.5 // indirect - github.com/go-openapi/swag v0.19.14 // indirect - github.com/go-openapi/validate v0.19.7 // indirect - github.com/go-stack/stack v1.8.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -93,11 +85,9 @@ require ( github.com/imdario/mergo v0.3.6 // indirect github.com/imkira/go-interpol v1.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.15.0 // indirect github.com/leodido/go-urn v1.2.1 // indirect - github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect @@ -126,7 +116,6 @@ require ( github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect github.com/yudai/gojsondiff v1.0.0 // indirect github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect - go.mongodb.org/mongo-driver v1.1.2 // indirect go.opencensus.io v0.24.0 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect @@ -138,9 +127,8 @@ require ( golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect - google.golang.org/grpc v1.51.0 // indirect - google.golang.org/protobuf v1.28.1 // indirect + google.golang.org/genproto v0.0.0-20230131230820-1c016267d619 // indirect + google.golang.org/protobuf v1.29.0 // indirect gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 // indirect gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index 9984a013..457d3a85 100644 --- a/go.sum +++ b/go.sum @@ -65,12 +65,8 @@ github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcy github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= -github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -78,7 +74,6 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= @@ -88,7 +83,6 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= @@ -126,10 +120,8 @@ github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/cockroach-go v0.0.0-20190925194419-606b3d062051/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= @@ -175,7 +167,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= @@ -195,8 +186,6 @@ github.com/gavv/httpexpect/v2 v2.15.0/go.mod h1:7myOP3A3VyS4+qnA4cm8DAad8zMN+7zx github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= -github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -210,70 +199,9 @@ github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= -github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= -github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= -github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= -github.com/go-openapi/analysis v0.19.4/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= -github.com/go-openapi/analysis v0.19.5 h1:8b2ZgKfKIUTVQpTb77MoRDIMEIwvDVw40o3aOXdfYzI= -github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= -github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= -github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= -github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= -github.com/go-openapi/errors v0.19.3/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= -github.com/go-openapi/errors v0.19.4 h1:fSGwO1tSYHFu70NKaWJt5Qh0qoBRtCm/mXS1yhf+0W0= -github.com/go-openapi/errors v0.19.4/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= -github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= -github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= -github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= -github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= -github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= -github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= -github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= -github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= -github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= -github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= -github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= -github.com/go-openapi/loads v0.19.3/go.mod h1:YVfqhUCdahYwR3f3iiwQLhicVRvLlU/WO5WPaZvcvSI= -github.com/go-openapi/loads v0.19.4 h1:5I4CCSqoWzT+82bBkNIvmLc0UOsoKKQ4Fz+3VxOB7SY= -github.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk= -github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= -github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= -github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= -github.com/go-openapi/runtime v0.19.11/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo= -github.com/go-openapi/runtime v0.19.15 h1:2GIefxs9Rx1vCDNghRtypRq+ig8KSLrjHbAYI/gCLCM= -github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo= -github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= -github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= -github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= -github.com/go-openapi/spec v0.19.3 h1:0XRyw8kguri6Yw4SxhsQA/atC88yqrk0+G4YhI2wabc= -github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= -github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= -github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= -github.com/go-openapi/strfmt v0.19.2/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= -github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= -github.com/go-openapi/strfmt v0.19.4/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= -github.com/go-openapi/strfmt v0.19.5 h1:0utjKrw+BAh8s57XE9Xz8DUBsVvPmRUB6styvl9wWIM= -github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= -github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= -github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= -github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.7/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= -github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= -github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= -github.com/go-openapi/validate v0.19.3/go.mod h1:90Vh6jjkTn+OT1Eefm0ZixWNFjhtOH7vS9k0lo6zwJo= -github.com/go-openapi/validate v0.19.6/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= -github.com/go-openapi/validate v0.19.7 h1:fR4tP2xc+25pdo5Qvv4v6g+5QKFgNg8nrifTE7V8ibA= -github.com/go-openapi/validate v0.19.7/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= @@ -288,7 +216,6 @@ github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= @@ -388,8 +315,6 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -520,7 +445,6 @@ github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhB github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -550,7 +474,6 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -563,13 +486,7 @@ github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= -github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= @@ -634,7 +551,6 @@ github.com/neo4j-drivers/gobolt v1.7.4/go.mod h1:O9AUbip4Dgre+CD3p40dnMD4a4r52QB github.com/neo4j/neo4j-go-driver v1.7.4/go.mod h1:aPO0vVr+WnhEJne+FgFjfsjzAnssPFLucHgGZ76Zb/U= github.com/newrelic/go-agent v3.19.2+incompatible h1:KnCNZPUqL+zxjAHMXX5uEqzVqzB/skA7qXmwwuigipA= github.com/newrelic/go-agent v3.19.2+incompatible/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -649,13 +565,14 @@ github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zM github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= -github.com/ory/keto-client-go v0.4.4-alpha.1 h1:eQkXvjovJkqFHawMJYZBUa1CdTQicVjsD7vA5cxeQB4= -github.com/ory/keto-client-go v0.4.4-alpha.1/go.mod h1:v13bI95oapu8Qh+i3E0y6mtezkVK6SMNzX2l04+0NOs= +github.com/ory/keto-client-go v0.11.0-alpha.0 h1:CJyKa6DhiYVDDtHa0DR//wkhLH8SXgWIXLZUugOYwH8= +github.com/ory/keto-client-go v0.11.0-alpha.0/go.mod h1:z/TmfbuoIU3DAHiv5a+cTyHXYc5mQDx38Ve8g2kYI00= +github.com/ory/keto/proto v0.11.1-alpha.0 h1:xVpFRnnIAGGvP9lYIUwjSWmrO7qVoLn20bT6NxzYQy4= +github.com/ory/keto/proto v0.11.1-alpha.0/go.mod h1:M9J/kybmyLKRmvvSqYzmRVYx2avY3yDMdUPinsck1q0= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= -github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= @@ -746,8 +663,6 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= -github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/uber-go/atomic v1.4.0 h1:yOuPqEq4ovnhEjpHmfFwsqBXDYbQeT6Nb0bwD6XnD5o= github.com/uber-go/atomic v1.4.0/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= github.com/uber/jaeger-client-go v2.16.0+incompatible h1:Q2Pp6v3QYiocMxomCaJuwQGFt7E53bPYqEgug/AoBtY= @@ -759,7 +674,6 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4= github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= @@ -788,11 +702,7 @@ gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2 go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= -go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.0/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= -go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= -go.mongodb.org/mongo-driver v1.1.2 h1:jxcFYjlkl8xaERsgLo+RNquI0epW6zuy/ZRQs6jnrFA= -go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -817,13 +727,10 @@ go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -845,6 +752,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200213203834-85f925bdd4d0/go.mod h1:IX6Eufr4L0ErOUlzqX/aFlHqsiKZRbV42Kb69e9VsTE= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= +golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -874,14 +783,12 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -891,7 +798,6 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -939,8 +845,8 @@ golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk= -golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s= +golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -953,6 +859,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -963,14 +871,12 @@ golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1062,7 +968,6 @@ golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -1072,8 +977,6 @@ golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= @@ -1229,8 +1132,8 @@ google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEc google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w= -google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230131230820-1c016267d619 h1:p0kMzw6AG0JEzd7Z+kXqOiLhC6gjUQTbtS2zR0Q3DbI= +google.golang.org/genproto v0.0.0-20230131230820-1c016267d619/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -1260,8 +1163,8 @@ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= -google.golang.org/grpc v1.51.0 h1:E1eGv1FTqoLIdnBCZufiSHgKjlqG6fKFf6pPWtMTh8U= -google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= +google.golang.org/grpc v1.52.3 h1:pf7sOysg4LdgBqduXveGKrcEwbStiK2rtfghdzlUYDQ= +google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1276,8 +1179,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.29.0 h1:44S3JjaKmLEE4YIkjzexaP+NzZsudE3Zin5Njn/pYX0= +google.golang.org/protobuf v1.29.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 h1:FVCohIoYO7IJoDDVpV2pdq7SgrMH6wHnuTyrdrxJNoY= gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= @@ -1285,7 +1188,6 @@ gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUy gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= @@ -1313,7 +1215,6 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 050b93184c06ee470762c3a8a922437ec2d9c4c8 Mon Sep 17 00:00:00 2001 From: Khor Shu Heng Date: Tue, 25 Jul 2023 01:09:28 +0800 Subject: [PATCH 02/12] feat: add boostrap and serving command to mlp binary Signed-off-by: Khor Shu Heng --- Dockerfile | 3 +- Makefile | 4 +-- api.Dockerfile | 5 ++-- api/cmd/bootstrap.go | 54 +++++++++++++++++++++++++++++++++++ api/cmd/root.go | 43 ++++++++++++++++++++++++++++ api/cmd/{main.go => serve.go} | 20 +++++++------ api/main.go | 9 ++++++ docker-compose.yaml | 1 + full.Dockerfile | 1 + go.mod | 4 ++- go.sum | 8 ++++-- 11 files changed, 135 insertions(+), 17 deletions(-) create mode 100644 api/cmd/bootstrap.go create mode 100644 api/cmd/root.go rename api/cmd/{main.go => serve.go} (94%) create mode 100644 api/main.go diff --git a/Dockerfile b/Dockerfile index f5d681a0..915b46e9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ COPY api api/ COPY go.mod . COPY go.sum . COPY db-migrations ./db-migrations -RUN go build -o bin/mlp-api ./api/cmd/main.go +RUN go build -o bin/mlp-api ./api/main.go # ============================================================ # Build stage 3: Run the app @@ -28,3 +28,4 @@ COPY --from=go-builder /src/api/bin/mlp-api /usr/bin/mlp COPY --from=go-builder /src/api/db-migrations ./db-migrations ENTRYPOINT ["sh", "-c", "mlp \"$@\"", "--"] +CMD ["serve"] \ No newline at end of file diff --git a/Makefile b/Makefile index 7287b0bd..922d4627 100644 --- a/Makefile +++ b/Makefile @@ -86,7 +86,7 @@ build-ui: clean-ui .PHONY: build-api build-api: clean-bin @echo "> Building API binary ..." - @cd ${API_PATH} && go build -o ../bin/${BIN_NAME} ./cmd/main.go + @cd ${API_PATH} && go build -o ../bin/${BIN_NAME} main.go .PHONY: build-api-image build-api-image: version @@ -111,7 +111,7 @@ build-image: version .PHONY: run run: local-env @echo "> Running application ..." - @go run api/cmd/main.go --config config-dev.yaml + @go run api/main.go serve --config config-dev.yaml .PHONY: start-ui start-ui: diff --git a/api.Dockerfile b/api.Dockerfile index e371b26c..ad0369ca 100644 --- a/api.Dockerfile +++ b/api.Dockerfile @@ -10,7 +10,7 @@ COPY api api/ COPY go.mod . COPY go.sum . -RUN go build -o bin/mlp-api ./api/cmd/main.go +RUN go build -o bin/mlp-api ./api/main.go # Clean image with mlp-api binary FROM alpine:3.16 @@ -18,4 +18,5 @@ FROM alpine:3.16 COPY --from=go-builder /src/api/bin/mlp-api /usr/bin/mlp COPY db-migrations ./db-migrations -ENTRYPOINT ["sh", "-c", "mlp \"$@\"", "--"] \ No newline at end of file +ENTRYPOINT ["sh", "-c", "mlp \"$@\"", "--"] +CMD ["serve"] diff --git a/api/cmd/bootstrap.go b/api/cmd/bootstrap.go new file mode 100644 index 00000000..5e6609ea --- /dev/null +++ b/api/cmd/bootstrap.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "context" + + "github.com/spf13/cobra" + + "github.com/caraml-dev/mlp/api/config" + "github.com/caraml-dev/mlp/api/log" + "github.com/caraml-dev/mlp/api/pkg/authz/enforcer" +) + +type BootstrapOptions struct { + ProjectReaders []string + MLPAdmins []string +} + +var ( + bootstrapOpts = &BootstrapOptions{} + bootstrapCmd = &cobra.Command{ + Use: "bootstrap", + Short: "Start bootstrap job to populate Keto", + Run: func(cmd *cobra.Command, args []string) { + err := startKetoBootstrap(globalConfig, bootstrapOpts) + if err != nil { + log.Panicf("unable to bootstrap keto: %v", err) + } + }, + } +) + +func init() { + bootstrapCmd.Flags().StringSliceVarP(&bootstrapOpts.ProjectReaders, "project-readers", "r", + []string{}, "Comma separated list of project readers") + bootstrapCmd.Flags().StringSliceVar(&bootstrapOpts.MLPAdmins, "mlp-admins", []string{}, + "Comma separated list of MLP admins") +} + +func startKetoBootstrap(globalCfg *config.Config, bootstrapOpts *BootstrapOptions) error { + authEnforcer, err := enforcer.NewEnforcerBuilder(). + KetoEndpoints(globalCfg.Authorization.KetoRemoteRead, globalCfg.Authorization.KetoRemoteWrite). + Build() + if err != nil { + return err + } + updateRequest := enforcer.NewAuthorizationUpdateRequest() + updateRequest.UpdateRoleMembers("mlp.projects.reader", bootstrapOpts.ProjectReaders) + updateRequest.UpdateRoleMembers("mlp.admin", bootstrapOpts.MLPAdmins) + err = authEnforcer.UpdateAuthorization(context.Background(), updateRequest) + if err != nil { + return err + } + return nil +} diff --git a/api/cmd/root.go b/api/cmd/root.go new file mode 100644 index 00000000..861f6f8e --- /dev/null +++ b/api/cmd/root.go @@ -0,0 +1,43 @@ +package cmd + +import ( + "github.com/spf13/cobra" + + "github.com/caraml-dev/mlp/api/config" + "github.com/caraml-dev/mlp/api/log" +) + +var ( + configFiles []string + globalConfig *config.Config + rootCmd = &cobra.Command{ + Use: "mlp", + Short: "CaraML Machine Learning Platform Console", + Long: "CaraML Machine Learning Platform Console, which provides a web UI to interact with different CaraML " + + "services.", + } +) + +func init() { + cobra.OnInitialize(initConfig) + rootCmd.PersistentFlags().StringSliceVarP(&configFiles, "config", "c", []string{}, + "Comma separated list of config files to load. The last config file will take precedence over the "+ + "previous ones.") + rootCmd.AddCommand(serveCmd) + rootCmd.AddCommand(bootstrapCmd) +} + +func initConfig() { + var err error + globalConfig, err = config.LoadAndValidate(configFiles...) + if err != nil { + log.Fatalf("failed initializing config: %v", err) + } +} + +func Execute() { + err := rootCmd.Execute() + if err != nil { + log.Fatalf("failed executing root command: %v", err) + } +} diff --git a/api/cmd/main.go b/api/cmd/serve.go similarity index 94% rename from api/cmd/main.go rename to api/cmd/serve.go index 46725818..f26d1ebe 100644 --- a/api/cmd/main.go +++ b/api/cmd/serve.go @@ -1,4 +1,4 @@ -package main +package cmd import ( "encoding/json" @@ -13,7 +13,7 @@ import ( "github.com/gorilla/mux" "github.com/heptiolabs/healthcheck" "github.com/rs/cors" - flag "github.com/spf13/pflag" + "github.com/spf13/cobra" "github.com/caraml-dev/mlp/api/api" apiV2 "github.com/caraml-dev/mlp/api/api/v2" @@ -23,15 +23,17 @@ import ( "github.com/caraml-dev/mlp/api/pkg/authz/enforcer" ) -func main() { - configFiles := flag.StringSliceP("config", "c", []string{}, "Path to a configuration files") - flag.Parse() - - cfg, err := config.LoadAndValidate(*configFiles...) - if err != nil { - log.Panicf("failed initializing config: %v", err) +var ( + serveCmd = &cobra.Command{ + Use: "serve", + Short: "Start MLP API server", + Run: func(cmd *cobra.Command, args []string) { + startServer(globalConfig) + }, } +) +func startServer(cfg *config.Config) { // init db db, err := database.InitDB(cfg.Database) if err != nil { diff --git a/api/main.go b/api/main.go new file mode 100644 index 00000000..5828f1e3 --- /dev/null +++ b/api/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "github.com/caraml-dev/mlp/api/cmd" +) + +func main() { + cmd.Execute() +} diff --git a/docker-compose.yaml b/docker-compose.yaml index 5aa6ea67..2a0f700b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -12,6 +12,7 @@ services: env_file: .env.development environment: - DATABASE_HOST=postgres + restart: on-failure postgres: image: bitnami/postgresql:14.5.0 diff --git a/full.Dockerfile b/full.Dockerfile index 3ed0ff2c..df4e040c 100644 --- a/full.Dockerfile +++ b/full.Dockerfile @@ -4,3 +4,4 @@ FROM ${MLP_API_IMAGE} COPY ui/build ./ui/build ENTRYPOINT ["sh", "-c", "mlp \"$@\"", "--"] +CMD ["serve"] diff --git a/go.mod b/go.mod index 6303b456..d06d5917 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( github.com/prometheus/client_golang v1.11.1 github.com/prometheus/client_model v0.2.0 github.com/rs/cors v1.7.0 + github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.1 github.com/uber/jaeger-client-go v2.16.0+incompatible @@ -39,7 +40,6 @@ require ( golang.org/x/oauth2 v0.5.0 golang.org/x/sync v0.1.0 google.golang.org/api v0.106.0 - google.golang.org/grpc v1.52.3 gopkg.in/errgo.v2 v2.1.0 gopkg.in/yaml.v2 v2.4.0 k8s.io/client-go v0.26.0 @@ -84,6 +84,7 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/imdario/mergo v0.3.6 // indirect github.com/imkira/go-interpol v1.1.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.15.0 // indirect @@ -128,6 +129,7 @@ require ( golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230131230820-1c016267d619 // indirect + google.golang.org/grpc v1.52.3 // indirect google.golang.org/protobuf v1.29.0 // indirect gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 // indirect gopkg.in/go-playground/assert.v1 v1.2.1 // indirect diff --git a/go.sum b/go.sum index 457d3a85..184b3bca 100644 --- a/go.sum +++ b/go.sum @@ -132,6 +132,7 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= @@ -406,6 +407,8 @@ github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= @@ -567,8 +570,6 @@ github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFSt github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/ory/keto-client-go v0.11.0-alpha.0 h1:CJyKa6DhiYVDDtHa0DR//wkhLH8SXgWIXLZUugOYwH8= github.com/ory/keto-client-go v0.11.0-alpha.0/go.mod h1:z/TmfbuoIU3DAHiv5a+cTyHXYc5mQDx38Ve8g2kYI00= -github.com/ory/keto/proto v0.11.1-alpha.0 h1:xVpFRnnIAGGvP9lYIUwjSWmrO7qVoLn20bT6NxzYQy4= -github.com/ory/keto/proto v0.11.1-alpha.0/go.mod h1:M9J/kybmyLKRmvvSqYzmRVYx2avY3yDMdUPinsck1q0= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= @@ -624,6 +625,7 @@ github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= @@ -641,6 +643,8 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= From 7fef651d25424d5be88fe1543c4e89297e9b6ead Mon Sep 17 00:00:00 2001 From: Khor Shu Heng Date: Tue, 25 Jul 2023 10:44:47 +0800 Subject: [PATCH 03/12] feat: Invoke serve command by default if none is specified --- api/cmd/root.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api/cmd/root.go b/api/cmd/root.go index 861f6f8e..cc839708 100644 --- a/api/cmd/root.go +++ b/api/cmd/root.go @@ -14,7 +14,11 @@ var ( Use: "mlp", Short: "CaraML Machine Learning Platform Console", Long: "CaraML Machine Learning Platform Console, which provides a web UI to interact with different CaraML " + - "services.", + "services. If no subcommand are provided, serve command will be run as default.", + // Run serve command by default if non is specified + Run: func(cmd *cobra.Command, args []string) { + serveCmd.Run(cmd, args) + }, } ) From fa4449df2302977d1c59443fb9df724c55929e43 Mon Sep 17 00:00:00 2001 From: Khor Shu Heng Date: Tue, 25 Jul 2023 13:36:59 +0800 Subject: [PATCH 04/12] fix: add additional comments to authorization update request, and update the method naming Signed-off-by: Khor Shu Heng --- api/cmd/bootstrap.go | 4 ++-- api/pkg/authz/enforcer/enforcer.go | 9 +++++++-- api/pkg/authz/enforcer/enforcer_test.go | 14 +++++++------- api/service/projects_service.go | 12 ++++++------ 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/api/cmd/bootstrap.go b/api/cmd/bootstrap.go index 5e6609ea..aacf8bc7 100644 --- a/api/cmd/bootstrap.go +++ b/api/cmd/bootstrap.go @@ -44,8 +44,8 @@ func startKetoBootstrap(globalCfg *config.Config, bootstrapOpts *BootstrapOption return err } updateRequest := enforcer.NewAuthorizationUpdateRequest() - updateRequest.UpdateRoleMembers("mlp.projects.reader", bootstrapOpts.ProjectReaders) - updateRequest.UpdateRoleMembers("mlp.admin", bootstrapOpts.MLPAdmins) + updateRequest.SetRoleMembers("mlp.projects.reader", bootstrapOpts.ProjectReaders) + updateRequest.SetRoleMembers("mlp.admin", bootstrapOpts.MLPAdmins) err = authEnforcer.UpdateAuthorization(context.Background(), updateRequest) if err != nil { return err diff --git a/api/pkg/authz/enforcer/enforcer.go b/api/pkg/authz/enforcer/enforcer.go index af6016f6..04dcf401 100644 --- a/api/pkg/authz/enforcer/enforcer.go +++ b/api/pkg/authz/enforcer/enforcer.go @@ -253,6 +253,9 @@ func (e *enforcer) isCacheEnabled() bool { return e.cache != nil } +// NewAuthorizationUpdateRequest create a new AuthorizationUpdateRequest. Multiple operations can be chained together +// using the SetRolePermissions and SetRoleMembers methods. No changes will be made until the AuthorizationUpdateRequest +// object is passed to the Enforcer, in which all the previously chained operations will be executed in batch. func NewAuthorizationUpdateRequest() AuthorizationUpdateRequest { return AuthorizationUpdateRequest{ RolePermissions: make(map[string][]string), @@ -265,13 +268,15 @@ type AuthorizationUpdateRequest struct { RoleMembers map[string][]string } -func (a AuthorizationUpdateRequest) UpdateRolePermissions(role string, +// SetRolePermissions set the permissions for a role. If the role already has permissions, they will be replaced. +func (a AuthorizationUpdateRequest) SetRolePermissions(role string, permissions []string) AuthorizationUpdateRequest { a.RolePermissions[role] = permissions return a } -func (a AuthorizationUpdateRequest) UpdateRoleMembers(role string, members []string) AuthorizationUpdateRequest { +// SetRoleMembers set the members for a role. If the role already has members, they will be replaced. +func (a AuthorizationUpdateRequest) SetRoleMembers(role string, members []string) AuthorizationUpdateRequest { a.RoleMembers[role] = members return a } diff --git a/api/pkg/authz/enforcer/enforcer_test.go b/api/pkg/authz/enforcer/enforcer_test.go index cfd6df53..506bfc1a 100644 --- a/api/pkg/authz/enforcer/enforcer_test.go +++ b/api/pkg/authz/enforcer/enforcer_test.go @@ -74,8 +74,8 @@ func TestEnforcer_HasPermission(t *testing.T) { clearRelations(readClient, writeClient) newRoleAndPermissionsRequest := NewAuthorizationUpdateRequest() - newRoleAndPermissionsRequest.UpdateRolePermissions("page.1.admin", []string{"page.1.get", "page.1.put"}) - newRoleAndPermissionsRequest.UpdateRoleMembers("page.1.admin", []string{"user-1@example.com"}) + newRoleAndPermissionsRequest.SetRolePermissions("page.1.admin", []string{"page.1.get", "page.1.put"}) + newRoleAndPermissionsRequest.SetRoleMembers("page.1.admin", []string{"user-1@example.com"}) err = ketoEnforcer.UpdateAuthorization(context.Background(), newRoleAndPermissionsRequest) require.NoError(t, err) @@ -118,8 +118,8 @@ func TestEnforcer_HasPermission(t *testing.T) { }) } updateRoleAndPermissionsRequest := NewAuthorizationUpdateRequest() - updateRoleAndPermissionsRequest.UpdateRolePermissions("page.1.admin", []string{"page.1.get", "page.1.delete"}) - updateRoleAndPermissionsRequest.UpdateRoleMembers("page.1.admin", []string{"admin-1@example.com"}) + updateRoleAndPermissionsRequest.SetRolePermissions("page.1.admin", []string{"page.1.get", "page.1.delete"}) + updateRoleAndPermissionsRequest.SetRoleMembers("page.1.admin", []string{"admin-1@example.com"}) err = ketoEnforcer.UpdateAuthorization(context.Background(), updateRoleAndPermissionsRequest) testsAfterUpdate := []struct { name string @@ -165,7 +165,7 @@ func TestEnforcer_GetUserRoles(t *testing.T) { clearRelations(readClient, writeClient) updateRequest := NewAuthorizationUpdateRequest() for i := 1; i < 4; i++ { - updateRequest.UpdateRoleMembers(fmt.Sprintf("pages.%d.reader", i), []string{"user-1@example.com"}) + updateRequest.SetRoleMembers(fmt.Sprintf("pages.%d.reader", i), []string{"user-1@example.com"}) } err = ketoEnforcer.UpdateAuthorization(context.Background(), updateRequest) require.NoError(t, err) @@ -208,7 +208,7 @@ func TestEnforcer_GetRolePermissions(t *testing.T) { writeClient := newKetoClient(ketoRemoteWrite) clearRelations(readClient, writeClient) updateRequest := NewAuthorizationUpdateRequest() - updateRequest.UpdateRolePermissions("pages.1.reader", []string{"pages.1.get", "pages.1.post"}) + updateRequest.SetRolePermissions("pages.1.reader", []string{"pages.1.get", "pages.1.post"}) err = ketoEnforcer.UpdateAuthorization(context.Background(), updateRequest) require.NoError(t, err) tests := []struct { @@ -248,7 +248,7 @@ func TestEnforcer_GetRoleMembers(t *testing.T) { writeClient := newKetoClient(ketoRemoteWrite) clearRelations(readClient, writeClient) updateRequest := NewAuthorizationUpdateRequest() - updateRequest.UpdateRolePermissions("pages.1.reader", []string{"pages.1.get", "pages.1.post"}) + updateRequest.SetRolePermissions("pages.1.reader", []string{"pages.1.get", "pages.1.post"}) err = ketoEnforcer.UpdateAuthorization(context.Background(), updateRequest) require.NoError(t, err) tests := []struct { diff --git a/api/service/projects_service.go b/api/service/projects_service.go index 1e42b106..dd6f950b 100644 --- a/api/service/projects_service.go +++ b/api/service/projects_service.go @@ -159,21 +159,21 @@ func adminPermissions(project *models.Project) []string { func (service *projectsService) updateAuthorizationPolicy(ctx context.Context, project *models.Project) error { updateRequest := enforcer.NewAuthorizationUpdateRequest() for _, role := range rolesWithReadOnlyAccess(project) { - updateRequest.UpdateRolePermissions(role, readPermissions(project)) + updateRequest.SetRolePermissions(role, readPermissions(project)) } if project.Administrators != nil { - updateRequest.UpdateRoleMembers(projectAdminRole(project), project.Administrators) + updateRequest.SetRoleMembers(projectAdminRole(project), project.Administrators) } else { - updateRequest.UpdateRoleMembers(projectAdminRole(project), []string{}) + updateRequest.SetRoleMembers(projectAdminRole(project), []string{}) } for _, role := range rolesWithAdminAccess(project) { - updateRequest.UpdateRolePermissions(role, adminPermissions(project)) + updateRequest.SetRolePermissions(role, adminPermissions(project)) } if project.Readers != nil { - updateRequest.UpdateRoleMembers(projectReaderRole(project), project.Readers) + updateRequest.SetRoleMembers(projectReaderRole(project), project.Readers) } else { - updateRequest.UpdateRoleMembers(projectReaderRole(project), []string{}) + updateRequest.SetRoleMembers(projectReaderRole(project), []string{}) } return service.authEnforcer.UpdateAuthorization(ctx, updateRequest) From 679408a9490a1e06ee2d282bc838e7b4c4643cab Mon Sep 17 00:00:00 2001 From: Khor Shu Heng Date: Tue, 25 Jul 2023 13:44:49 +0800 Subject: [PATCH 05/12] fix: Use constant string for predefined roles Signed-off-by: Khor Shu Heng --- api/cmd/bootstrap.go | 4 ++-- api/pkg/authz/enforcer/role.go | 6 ++++++ api/service/projects_service.go | 6 +++--- 3 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 api/pkg/authz/enforcer/role.go diff --git a/api/cmd/bootstrap.go b/api/cmd/bootstrap.go index aacf8bc7..c7cafee3 100644 --- a/api/cmd/bootstrap.go +++ b/api/cmd/bootstrap.go @@ -44,8 +44,8 @@ func startKetoBootstrap(globalCfg *config.Config, bootstrapOpts *BootstrapOption return err } updateRequest := enforcer.NewAuthorizationUpdateRequest() - updateRequest.SetRoleMembers("mlp.projects.reader", bootstrapOpts.ProjectReaders) - updateRequest.SetRoleMembers("mlp.admin", bootstrapOpts.MLPAdmins) + updateRequest.SetRoleMembers(enforcer.ProjectReaderRole, bootstrapOpts.ProjectReaders) + updateRequest.SetRoleMembers(enforcer.MLPAdminRole, bootstrapOpts.MLPAdmins) err = authEnforcer.UpdateAuthorization(context.Background(), updateRequest) if err != nil { return err diff --git a/api/pkg/authz/enforcer/role.go b/api/pkg/authz/enforcer/role.go new file mode 100644 index 00000000..065dda78 --- /dev/null +++ b/api/pkg/authz/enforcer/role.go @@ -0,0 +1,6 @@ +package enforcer + +const ( + MLPAdminRole = "mlp.administrator" + ProjectReaderRole = "mlp.projects.reader" +) diff --git a/api/service/projects_service.go b/api/service/projects_service.go index dd6f950b..bfaf565d 100644 --- a/api/service/projects_service.go +++ b/api/service/projects_service.go @@ -128,14 +128,14 @@ func projectAdminRole(project *models.Project) string { func rolesWithReadOnlyAccess(project *models.Project) []string { predefinedRoles := []string{ - "mlp.projects.reader", + enforcer.ProjectReaderRole, } return append(predefinedRoles, projectReaderRole(project)) } func rolesWithAdminAccess(project *models.Project) []string { predefinedRoles := []string{ - "mlp.administrator", + enforcer.MLPAdminRole, } return append(predefinedRoles, projectAdminRole(project)) } @@ -190,7 +190,7 @@ func (service *projectsService) filterAuthorizedProjects(ctx context.Context, pr return nil, err } for _, role := range roles { - if slices.Contains([]string{"mlp.administrator", "mlp.projects.reader"}, role) { + if slices.Contains([]string{enforcer.MLPAdminRole, enforcer.ProjectReaderRole}, role) { return projects, nil } } From b00eff24e1409d9cb89853593b00603f04e0f374 Mon Sep 17 00:00:00 2001 From: Khor Shu Heng Date: Tue, 25 Jul 2023 13:48:01 +0800 Subject: [PATCH 06/12] fix: Use singular form for permission lookup / store Signed-off-by: Khor Shu Heng --- api/pkg/authz/enforcer/cache.go | 8 ++++---- api/pkg/authz/enforcer/cache_test.go | 8 ++++---- api/pkg/authz/enforcer/enforcer.go | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/api/pkg/authz/enforcer/cache.go b/api/pkg/authz/enforcer/cache.go index bf22c046..a7f08da3 100644 --- a/api/pkg/authz/enforcer/cache.go +++ b/api/pkg/authz/enforcer/cache.go @@ -20,9 +20,9 @@ func newInMemoryCache(keyExpirySeconds int, cacheCleanUpIntervalSeconds int) *In } } -// LookUpUserPermissions returns the cached permission check result for a user / permission pair. +// LookUpUserPermission returns the cached permission check result for a user / permission pair. // The returned value indicates whether the result is cached. -func (c *InMemoryCache) LookUpUserPermissions(user string, permission string) (*bool, bool) { +func (c *InMemoryCache) LookUpUserPermission(user string, permission string) (*bool, bool) { if cachedValue, ok := c.store.Get(c.buildCacheKey(user, permission)); ok { if allowed, ok := cachedValue.(*bool); ok { return allowed, true @@ -31,8 +31,8 @@ func (c *InMemoryCache) LookUpUserPermissions(user string, permission string) (* return nil, false } -// StoreUserPermissions stores the permission check result for a user / permission pair. -func (c *InMemoryCache) StoreUserPermissions(user string, permission string, result bool) { +// StoreUserPermission stores the permission check result for a user / permission pair. +func (c *InMemoryCache) StoreUserPermission(user string, permission string, result bool) { c.store.Set(c.buildCacheKey(user, permission), &result, cache.DefaultExpiration) } diff --git a/api/pkg/authz/enforcer/cache_test.go b/api/pkg/authz/enforcer/cache_test.go index 3072f13b..4eb4696f 100644 --- a/api/pkg/authz/enforcer/cache_test.go +++ b/api/pkg/authz/enforcer/cache_test.go @@ -6,10 +6,10 @@ import ( "github.com/stretchr/testify/assert" ) -func TestInMemoryCache_LookUpUserPermissions(t *testing.T) { +func TestInMemoryCache_LookUpUserPermission(t *testing.T) { cache := newInMemoryCache(600, 600) - cache.StoreUserPermissions("user1@email.com", "mlp.projects.1.get", true) - cache.StoreUserPermissions("user1@email.com", "mlp.projects.1.post", false) + cache.StoreUserPermission("user1@email.com", "mlp.projects.1.get", true) + cache.StoreUserPermission("user1@email.com", "mlp.projects.1.post", false) trueValue, falseValue := true, false tests := map[string]struct { user string @@ -45,7 +45,7 @@ func TestInMemoryCache_LookUpUserPermissions(t *testing.T) { for name, tt := range tests { t.Run(name, func(t *testing.T) { - cachedVal, found := cache.LookUpUserPermissions(tt.user, tt.permission) + cachedVal, found := cache.LookUpUserPermission(tt.user, tt.permission) assert.Equal(t, tt.expectedVal, cachedVal) assert.Equal(t, tt.expectedFound, found) }) diff --git a/api/pkg/authz/enforcer/enforcer.go b/api/pkg/authz/enforcer/enforcer.go index 04dcf401..4c3f7146 100644 --- a/api/pkg/authz/enforcer/enforcer.go +++ b/api/pkg/authz/enforcer/enforcer.go @@ -73,7 +73,7 @@ func newEnforcer( func (e *enforcer) IsUserGrantedPermission(ctx context.Context, user string, permission string) (bool, error) { if e.isCacheEnabled() { - if isAllowed, found := e.cache.LookUpUserPermissions(user, permission); found { + if isAllowed, found := e.cache.LookUpUserPermission(user, permission); found { return *isAllowed, nil } } @@ -90,7 +90,7 @@ func (e *enforcer) IsUserGrantedPermission(ctx context.Context, user string, per } userHasPermission := checkPermissionResult.Allowed if e.isCacheEnabled() { - e.cache.StoreUserPermissions(user, permission, userHasPermission) + e.cache.StoreUserPermission(user, permission, userHasPermission) } return userHasPermission, nil } From 2bb558930165ef4d9331b00fda776c2b7f12c6af Mon Sep 17 00:00:00 2001 From: Khor Shu Heng Date: Tue, 25 Jul 2023 15:28:10 +0800 Subject: [PATCH 07/12] fix: Use input files for keto bootstrap command Signed-off-by: Khor Shu Heng --- api/cmd/bootstrap.go | 42 +++++++++++++++++++++++++++++++++--------- go.mod | 2 +- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/api/cmd/bootstrap.go b/api/cmd/bootstrap.go index c7cafee3..682e7362 100644 --- a/api/cmd/bootstrap.go +++ b/api/cmd/bootstrap.go @@ -3,6 +3,10 @@ package cmd import ( "context" + "github.com/knadh/koanf" + "github.com/knadh/koanf/parsers/yaml" + "github.com/knadh/koanf/providers/file" + "github.com/spf13/cobra" "github.com/caraml-dev/mlp/api/config" @@ -10,18 +14,22 @@ import ( "github.com/caraml-dev/mlp/api/pkg/authz/enforcer" ) -type BootstrapOptions struct { +type BootstrapRoleMembers struct { ProjectReaders []string MLPAdmins []string } var ( - bootstrapOpts = &BootstrapOptions{} - bootstrapCmd = &cobra.Command{ + bootstrapRoleMembersInputFile string + bootstrapCmd = &cobra.Command{ Use: "bootstrap", Short: "Start bootstrap job to populate Keto", Run: func(cmd *cobra.Command, args []string) { - err := startKetoBootstrap(globalConfig, bootstrapOpts) + bootstrapRoleMembers, err := loadRoleMemberFromInputFile(bootstrapRoleMembersInputFile) + if err != nil { + log.Panicf("unable to load role members from input file: %v", err) + } + err = startKetoBootstrap(globalConfig, bootstrapRoleMembers) if err != nil { log.Panicf("unable to bootstrap keto: %v", err) } @@ -30,13 +38,29 @@ var ( ) func init() { - bootstrapCmd.Flags().StringSliceVarP(&bootstrapOpts.ProjectReaders, "project-readers", "r", - []string{}, "Comma separated list of project readers") - bootstrapCmd.Flags().StringSliceVar(&bootstrapOpts.MLPAdmins, "mlp-admins", []string{}, - "Comma separated list of MLP admins") + bootstrapCmd.Flags().StringVarP(&bootstrapRoleMembersInputFile, "role-members", "r", "", + "Path to an input file that map roles to members") + err := bootstrapCmd.MarkFlagRequired("role-members") + if err != nil { + log.Panicf("unable to mark flag as required: %v", err) + } +} + +func loadRoleMemberFromInputFile(path string) (*BootstrapRoleMembers, error) { + bootstrapRoleMembers := &BootstrapRoleMembers{} + k := koanf.New(".") + err := k.Load(file.Provider(path), yaml.Parser()) + if err != nil { + return nil, err + } + err = k.Unmarshal("", bootstrapRoleMembers) + if err != nil { + return nil, err + } + return bootstrapRoleMembers, nil } -func startKetoBootstrap(globalCfg *config.Config, bootstrapOpts *BootstrapOptions) error { +func startKetoBootstrap(globalCfg *config.Config, bootstrapOpts *BootstrapRoleMembers) error { authEnforcer, err := enforcer.NewEnforcerBuilder(). KetoEndpoints(globalCfg.Authorization.KetoRemoteRead, globalCfg.Authorization.KetoRemoteWrite). Build() diff --git a/go.mod b/go.mod index d06d5917..62c8e69d 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,6 @@ require ( github.com/prometheus/client_model v0.2.0 github.com/rs/cors v1.7.0 github.com/spf13/cobra v1.7.0 - github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.1 github.com/uber/jaeger-client-go v2.16.0+incompatible go.uber.org/zap v1.17.0 @@ -106,6 +105,7 @@ require ( github.com/ryanuber/go-glob v1.0.0 // indirect github.com/sanity-io/litter v1.5.5 // indirect github.com/sergi/go-diff v1.0.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/uber-go/atomic v1.4.0 // indirect github.com/uber/jaeger-lib v2.0.0+incompatible // indirect From 5f735638035bfece93a50c1ea0365149c4d57446 Mon Sep 17 00:00:00 2001 From: Khor Shu Heng Date: Wed, 26 Jul 2023 09:36:57 +0800 Subject: [PATCH 08/12] feat: add comment to role member expansion Signed-off-by: Khor Shu Heng --- api/cmd/bootstrap.go | 5 ++++- api/pkg/authz/enforcer/enforcer.go | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/api/cmd/bootstrap.go b/api/cmd/bootstrap.go index 682e7362..4f6264f0 100644 --- a/api/cmd/bootstrap.go +++ b/api/cmd/bootstrap.go @@ -47,7 +47,10 @@ func init() { } func loadRoleMemberFromInputFile(path string) (*BootstrapRoleMembers, error) { - bootstrapRoleMembers := &BootstrapRoleMembers{} + bootstrapRoleMembers := &BootstrapRoleMembers{ + ProjectReaders: []string{}, + MLPAdmins: []string{}, + } k := koanf.New(".") err := k.Load(file.Provider(path), yaml.Parser()) if err != nil { diff --git a/api/pkg/authz/enforcer/enforcer.go b/api/pkg/authz/enforcer/enforcer.go index 4c3f7146..ccf363d5 100644 --- a/api/pkg/authz/enforcer/enforcer.go +++ b/api/pkg/authz/enforcer/enforcer.go @@ -132,6 +132,9 @@ func (e *enforcer) GetRolePermissions(ctx context.Context, role string) ([]strin } func (e *enforcer) GetRoleMembers(ctx context.Context, role string) ([]string, error) { + // The permission tree includes the role as the parent node, and the members as the children nodes. + // Hence, we need at least the depth of 2 to get the members. We don't go beyond 2 as we currently + // do not implement nested roles. expandedRole, _, err := e.ketoReadClient.PermissionApi.ExpandPermissions(ctx). Namespace("Role"). Relation("member"). From 776ba3267f8c64806af34eef29348647355904d6 Mon Sep 17 00:00:00 2001 From: Khor Shu Heng Date: Wed, 26 Jul 2023 11:01:24 +0800 Subject: [PATCH 09/12] feat: add get user permissions method to enforcer Signed-off-by: Khor Shu Heng --- api/pkg/authz/enforcer/enforcer.go | 34 +++++++++++++++++++ api/pkg/authz/enforcer/enforcer_test.go | 43 +++++++++++++++++++++++++ api/service/projects_service.go | 1 + 3 files changed, 78 insertions(+) diff --git a/api/pkg/authz/enforcer/enforcer.go b/api/pkg/authz/enforcer/enforcer.go index ccf363d5..1da67cdb 100644 --- a/api/pkg/authz/enforcer/enforcer.go +++ b/api/pkg/authz/enforcer/enforcer.go @@ -18,6 +18,8 @@ type Enforcer interface { GetUserRoles(ctx context.Context, user string) ([]string, error) // GetRolePermissions get all permissions directly associated with a role GetRolePermissions(ctx context.Context, role string) ([]string, error) + // GetUserPermissions get all permissions associated with a user + GetUserPermissions(ctx context.Context, user string) ([]string, error) // GetRoleMembers get all members for a role GetRoleMembers(ctx context.Context, role string) ([]string, error) // UpdateAuthorization update authorization rules in batches @@ -131,6 +133,38 @@ func (e *enforcer) GetRolePermissions(ctx context.Context, role string) ([]strin return permissions, nil } +func (e *enforcer) GetUserPermissions(ctx context.Context, user string) ([]string, error) { + roles, err := e.GetUserRoles(ctx, user) + if err != nil { + return nil, err + } + permissionSet := sync.Map{} + getPermissionsWorkersGroup := new(errgroup.Group) + for _, role := range roles { + role := role + getPermissionsWorkersGroup.Go(func() error { + permissions, err := e.GetRolePermissions(ctx, role) + for _, permission := range permissions { + permissionSet.Store(permission, true) + } + if err != nil { + return err + } + return nil + }) + } + err = getPermissionsWorkersGroup.Wait() + if err != nil { + return nil, err + } + permissions := make([]string, 0) + permissionSet.Range(func(key, value interface{}) bool { + permissions = append(permissions, key.(string)) + return true + }) + return permissions, nil +} + func (e *enforcer) GetRoleMembers(ctx context.Context, role string) ([]string, error) { // The permission tree includes the role as the parent node, and the members as the children nodes. // Hence, we need at least the depth of 2 to get the members. We don't go beyond 2 as we currently diff --git a/api/pkg/authz/enforcer/enforcer_test.go b/api/pkg/authz/enforcer/enforcer_test.go index 506bfc1a..7aba0885 100644 --- a/api/pkg/authz/enforcer/enforcer_test.go +++ b/api/pkg/authz/enforcer/enforcer_test.go @@ -241,6 +241,49 @@ func TestEnforcer_GetRolePermissions(t *testing.T) { } } +func TestEnforcer_GetUserPermissions(t *testing.T) { + ketoEnforcer, err := NewEnforcerBuilder().Build() + require.NoError(t, err) + readClient := newKetoClient(ketoRemoteRead) + writeClient := newKetoClient(ketoRemoteWrite) + clearRelations(readClient, writeClient) + updateRequest := NewAuthorizationUpdateRequest() + updateRequest.SetRolePermissions("pages.1.reader", []string{"pages.1.get"}) + updateRequest.SetRolePermissions("pages.1.admin", []string{"pages.1.get", "pages.1.post"}) + updateRequest.SetRoleMembers("pages.1.reader", []string{"user-1@example.com"}) + updateRequest.SetRoleMembers("pages.1.admin", []string{"user-1@example.com"}) + err = ketoEnforcer.UpdateAuthorization(context.Background(), updateRequest) + require.NoError(t, err) + tests := []struct { + name string + user string + expectedPermissions []string + }{ + { + "user-1 has reader and admin permissions for page 1", + "user-1@example.com", + []string{ + "pages.1.get", + "pages.1.post", + }, + }, + { + "unknown user has no no permissions", + "anonymous@example.com", + []string{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res, err := ketoEnforcer.GetUserPermissions(context.Background(), tt.user) + require.NoError(t, err) + sort.Strings(tt.expectedPermissions) + sort.Strings(res) + require.Equal(t, tt.expectedPermissions, res) + }) + } +} + func TestEnforcer_GetRoleMembers(t *testing.T) { ketoEnforcer, err := NewEnforcerBuilder().Build() require.NoError(t, err) diff --git a/api/service/projects_service.go b/api/service/projects_service.go index bfaf565d..abf9df1d 100644 --- a/api/service/projects_service.go +++ b/api/service/projects_service.go @@ -179,6 +179,7 @@ func (service *projectsService) updateAuthorizationPolicy(ctx context.Context, p return service.authEnforcer.UpdateAuthorization(ctx, updateRequest) } +// TODO: Evaluate if we should retrieve all permissions granted to a user as opposed to just roles func (service *projectsService) filterAuthorizedProjects(ctx context.Context, projects []*models.Project, user string) ([]*models.Project, error) { if user == "" { From 5dc98ed5f704231823c06e688174e8fc9057a47f Mon Sep 17 00:00:00 2001 From: Khor Shu Heng Date: Wed, 26 Jul 2023 13:19:32 +0800 Subject: [PATCH 10/12] fix: use role template strings for predefined roles Signed-off-by: Khor Shu Heng --- api/cmd/bootstrap.go | 2 +- api/pkg/authz/enforcer/role.go | 46 ++++++++++++++- api/pkg/authz/enforcer/role_test.go | 89 +++++++++++++++++++++++++++++ api/service/projects_service.go | 59 +++++++++---------- 4 files changed, 164 insertions(+), 32 deletions(-) create mode 100644 api/pkg/authz/enforcer/role_test.go diff --git a/api/cmd/bootstrap.go b/api/cmd/bootstrap.go index 4f6264f0..05818daa 100644 --- a/api/cmd/bootstrap.go +++ b/api/cmd/bootstrap.go @@ -71,7 +71,7 @@ func startKetoBootstrap(globalCfg *config.Config, bootstrapOpts *BootstrapRoleMe return err } updateRequest := enforcer.NewAuthorizationUpdateRequest() - updateRequest.SetRoleMembers(enforcer.ProjectReaderRole, bootstrapOpts.ProjectReaders) + updateRequest.SetRoleMembers(enforcer.MLPProjectsReaderRole, bootstrapOpts.ProjectReaders) updateRequest.SetRoleMembers(enforcer.MLPAdminRole, bootstrapOpts.MLPAdmins) err = authEnforcer.UpdateAuthorization(context.Background(), updateRequest) if err != nil { diff --git a/api/pkg/authz/enforcer/role.go b/api/pkg/authz/enforcer/role.go index 065dda78..2b02ae54 100644 --- a/api/pkg/authz/enforcer/role.go +++ b/api/pkg/authz/enforcer/role.go @@ -1,6 +1,48 @@ package enforcer +import ( + "bytes" + "text/template" + + "github.com/caraml-dev/mlp/api/models" +) + const ( - MLPAdminRole = "mlp.administrator" - ProjectReaderRole = "mlp.projects.reader" + MLPAdminRole = "mlp.administrator" + MLPProjectsReaderRole = "mlp.projects.reader" + MLPProjectReaderRole = "mlp.projects.{{ .ProjectId }}.reader" + MLPProjectAdminRole = "mlp.projects.{{ .ProjectId }}.administrator" ) + +func ParseRole(role string, templateContext map[string]string) (string, error) { + roleParser, err := template.New("role").Parse(role) + if err != nil { + return "", err + } + var parseResultBytes bytes.Buffer + err = roleParser.Execute(&parseResultBytes, templateContext) + if err != nil { + return "", err + } + return parseResultBytes.String(), nil +} + +func ParseProjectRole(roleTemplateString string, project *models.Project) (string, error) { + parsedRole, err := ParseRole(roleTemplateString, map[string]string{"ProjectId": project.ID.String()}) + if err != nil { + return "", err + } + return parsedRole, nil +} + +func ParseProjectRoles(roleTemplateStrings []string, project *models.Project) ([]string, error) { + roles := make([]string, len(roleTemplateStrings)) + for i, roleTemplateString := range roleTemplateStrings { + parsedRole, err := ParseProjectRole(roleTemplateString, project) + roles[i] = parsedRole + if err != nil { + return nil, err + } + } + return roles, nil +} diff --git a/api/pkg/authz/enforcer/role_test.go b/api/pkg/authz/enforcer/role_test.go new file mode 100644 index 00000000..dd949efb --- /dev/null +++ b/api/pkg/authz/enforcer/role_test.go @@ -0,0 +1,89 @@ +package enforcer + +import ( + "testing" + + "github.com/caraml-dev/mlp/api/models" +) + +func TestParseRole(t *testing.T) { + type args struct { + role string + templateContext map[string]string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + "parse role with project id", + args{ + role: MLPProjectReaderRole, + templateContext: map[string]string{"ProjectId": "1"}, + }, + "mlp.projects.1.reader", + false, + }, + { + "parse plain string role without template context", + args{ + role: MLPAdminRole, + templateContext: nil, + }, + "mlp.administrator", + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseRole(tt.args.role, tt.args.templateContext) + if (err != nil) != tt.wantErr { + t.Errorf("ParseRole() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ParseRole() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestParseProjectRole(t *testing.T) { + type args struct { + role string + project *models.Project + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + "parse role with project id", + args{ + role: MLPProjectReaderRole, + project: &models.Project{ + ID: 1, + }, + }, + "mlp.projects.1.reader", + false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseProjectRole(tt.args.role, tt.args.project) + if (err != nil) != tt.wantErr { + t.Errorf("ParseProjectRole() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ParseProjectRole() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/api/service/projects_service.go b/api/service/projects_service.go index abf9df1d..4023cb77 100644 --- a/api/service/projects_service.go +++ b/api/service/projects_service.go @@ -118,28 +118,6 @@ func (service *projectsService) save(project *models.Project) (*models.Project, return service.projectRepository.Save(project) } -func projectReaderRole(project *models.Project) string { - return fmt.Sprintf("mlp.projects.%d.reader", project.ID) -} - -func projectAdminRole(project *models.Project) string { - return fmt.Sprintf("mlp.projects.%d.administrator", project.ID) -} - -func rolesWithReadOnlyAccess(project *models.Project) []string { - predefinedRoles := []string{ - enforcer.ProjectReaderRole, - } - return append(predefinedRoles, projectReaderRole(project)) -} - -func rolesWithAdminAccess(project *models.Project) []string { - predefinedRoles := []string{ - enforcer.MLPAdminRole, - } - return append(predefinedRoles, projectAdminRole(project)) -} - func readPermissions(project *models.Project) []string { permissions := make([]string, 0) for _, method := range []string{"get"} { @@ -158,22 +136,45 @@ func adminPermissions(project *models.Project) []string { func (service *projectsService) updateAuthorizationPolicy(ctx context.Context, project *models.Project) error { updateRequest := enforcer.NewAuthorizationUpdateRequest() - for _, role := range rolesWithReadOnlyAccess(project) { + rolesWithReadOnlyAccess, err := enforcer.ParseProjectRoles([]string{ + enforcer.MLPProjectsReaderRole, + enforcer.MLPProjectReaderRole, + }, project) + if err != nil { + return err + } + for _, role := range rolesWithReadOnlyAccess { updateRequest.SetRolePermissions(role, readPermissions(project)) } + projectAdminRole, err := enforcer.ParseProjectRole(enforcer.MLPProjectAdminRole, project) + if err != nil { + return err + } if project.Administrators != nil { - updateRequest.SetRoleMembers(projectAdminRole(project), project.Administrators) + updateRequest.SetRoleMembers(projectAdminRole, project.Administrators) } else { - updateRequest.SetRoleMembers(projectAdminRole(project), []string{}) + updateRequest.SetRoleMembers(projectAdminRole, []string{}) } - for _, role := range rolesWithAdminAccess(project) { + rolesWithAdminAccess, err := enforcer.ParseProjectRoles([]string{ + enforcer.MLPAdminRole, + enforcer.MLPProjectAdminRole, + }, project) + if err != nil { + return err + } + for _, role := range rolesWithAdminAccess { updateRequest.SetRolePermissions(role, adminPermissions(project)) } + projectReaderRole, err := enforcer.ParseProjectRole(enforcer.MLPProjectReaderRole, project) + if err != nil { + return err + } if project.Readers != nil { - updateRequest.SetRoleMembers(projectReaderRole(project), project.Readers) + updateRequest.SetRoleMembers(projectReaderRole, project.Readers) } else { - updateRequest.SetRoleMembers(projectReaderRole(project), []string{}) + updateRequest.SetRoleMembers(projectReaderRole, []string{}) + } return service.authEnforcer.UpdateAuthorization(ctx, updateRequest) @@ -191,7 +192,7 @@ func (service *projectsService) filterAuthorizedProjects(ctx context.Context, pr return nil, err } for _, role := range roles { - if slices.Contains([]string{enforcer.MLPAdminRole, enforcer.ProjectReaderRole}, role) { + if slices.Contains([]string{enforcer.MLPAdminRole, enforcer.MLPProjectsReaderRole}, role) { return projects, nil } } From 81b7d4d129d0c557a0c28d41e3871e4eeedd4530 Mon Sep 17 00:00:00 2001 From: Khor Shu Heng Date: Wed, 26 Jul 2023 16:34:29 +0800 Subject: [PATCH 11/12] fix: remove unnecessary error handling Signed-off-by: Khor Shu Heng --- api/api/projects_api.go | 4 ---- api/service/projects_service.go | 3 --- 2 files changed, 7 deletions(-) diff --git a/api/api/projects_api.go b/api/api/projects_api.go index 4dfb8d24..ef779881 100644 --- a/api/api/projects_api.go +++ b/api/api/projects_api.go @@ -22,10 +22,6 @@ func (c *ProjectsController) ListProjects(r *http.Request, vars map[string]strin return FromError(err) } - if err != nil { - return InternalServerError(err.Error()) - } - return Ok(projects) } diff --git a/api/service/projects_service.go b/api/service/projects_service.go index 4023cb77..9794c75c 100644 --- a/api/service/projects_service.go +++ b/api/service/projects_service.go @@ -196,9 +196,6 @@ func (service *projectsService) filterAuthorizedProjects(ctx context.Context, pr return projects, nil } } - if err != nil { - return nil, err - } authorizedProjects := make([]*models.Project, 0) for _, project := range projects { if (project.Administrators != nil && slices.Contains(project.Administrators, user)) || From 0e3db4fdf49a6d3a68bd7b42319ddaa877cb416b Mon Sep 17 00:00:00 2001 From: Khor Shu Heng Date: Thu, 27 Jul 2023 10:34:19 +0800 Subject: [PATCH 12/12] fix: use separate config for bootstrap command Signed-off-by: Khor Shu Heng --- api/cmd/bootstrap.go | 45 +++++++++++++++++++++----------------------- api/cmd/root.go | 31 +++++++++++------------------- api/cmd/serve.go | 15 +++++++++++++-- 3 files changed, 45 insertions(+), 46 deletions(-) diff --git a/api/cmd/bootstrap.go b/api/cmd/bootstrap.go index 05818daa..56c0aa92 100644 --- a/api/cmd/bootstrap.go +++ b/api/cmd/bootstrap.go @@ -9,27 +9,28 @@ import ( "github.com/spf13/cobra" - "github.com/caraml-dev/mlp/api/config" "github.com/caraml-dev/mlp/api/log" "github.com/caraml-dev/mlp/api/pkg/authz/enforcer" ) -type BootstrapRoleMembers struct { - ProjectReaders []string - MLPAdmins []string +type BootstrapConfig struct { + KetoRemoteRead string + KetoRemoteWrite string + ProjectReaders []string + MLPAdmins []string } var ( - bootstrapRoleMembersInputFile string - bootstrapCmd = &cobra.Command{ + bootstrapConfigFile string + bootstrapCmd = &cobra.Command{ Use: "bootstrap", Short: "Start bootstrap job to populate Keto", Run: func(cmd *cobra.Command, args []string) { - bootstrapRoleMembers, err := loadRoleMemberFromInputFile(bootstrapRoleMembersInputFile) + bootstrapConfig, err := loadBootstrapConfig(bootstrapConfigFile) if err != nil { log.Panicf("unable to load role members from input file: %v", err) } - err = startKetoBootstrap(globalConfig, bootstrapRoleMembers) + err = startKetoBootstrap(bootstrapConfig) if err != nil { log.Panicf("unable to bootstrap keto: %v", err) } @@ -38,16 +39,16 @@ var ( ) func init() { - bootstrapCmd.Flags().StringVarP(&bootstrapRoleMembersInputFile, "role-members", "r", "", - "Path to an input file that map roles to members") - err := bootstrapCmd.MarkFlagRequired("role-members") + bootstrapCmd.Flags().StringVarP(&bootstrapConfigFile, "config", "c", "", + "Path to keto bootstrap configuration") + err := bootstrapCmd.MarkFlagRequired("config") if err != nil { log.Panicf("unable to mark flag as required: %v", err) } } -func loadRoleMemberFromInputFile(path string) (*BootstrapRoleMembers, error) { - bootstrapRoleMembers := &BootstrapRoleMembers{ +func loadBootstrapConfig(path string) (*BootstrapConfig, error) { + bootstrapCfg := &BootstrapConfig{ ProjectReaders: []string{}, MLPAdmins: []string{}, } @@ -56,26 +57,22 @@ func loadRoleMemberFromInputFile(path string) (*BootstrapRoleMembers, error) { if err != nil { return nil, err } - err = k.Unmarshal("", bootstrapRoleMembers) + err = k.Unmarshal("", bootstrapCfg) if err != nil { return nil, err } - return bootstrapRoleMembers, nil + return bootstrapCfg, nil } -func startKetoBootstrap(globalCfg *config.Config, bootstrapOpts *BootstrapRoleMembers) error { +func startKetoBootstrap(bootstrapCfg *BootstrapConfig) error { authEnforcer, err := enforcer.NewEnforcerBuilder(). - KetoEndpoints(globalCfg.Authorization.KetoRemoteRead, globalCfg.Authorization.KetoRemoteWrite). + KetoEndpoints(bootstrapCfg.KetoRemoteRead, bootstrapCfg.KetoRemoteWrite). Build() if err != nil { return err } updateRequest := enforcer.NewAuthorizationUpdateRequest() - updateRequest.SetRoleMembers(enforcer.MLPProjectsReaderRole, bootstrapOpts.ProjectReaders) - updateRequest.SetRoleMembers(enforcer.MLPAdminRole, bootstrapOpts.MLPAdmins) - err = authEnforcer.UpdateAuthorization(context.Background(), updateRequest) - if err != nil { - return err - } - return nil + updateRequest.SetRoleMembers(enforcer.MLPProjectsReaderRole, bootstrapCfg.ProjectReaders) + updateRequest.SetRoleMembers(enforcer.MLPAdminRole, bootstrapCfg.MLPAdmins) + return authEnforcer.UpdateAuthorization(context.Background(), updateRequest) } diff --git a/api/cmd/root.go b/api/cmd/root.go index cc839708..24208c5a 100644 --- a/api/cmd/root.go +++ b/api/cmd/root.go @@ -1,46 +1,37 @@ package cmd import ( + "os" + "github.com/spf13/cobra" + "github.com/spf13/pflag" - "github.com/caraml-dev/mlp/api/config" "github.com/caraml-dev/mlp/api/log" ) var ( - configFiles []string - globalConfig *config.Config - rootCmd = &cobra.Command{ + rootCmd = &cobra.Command{ Use: "mlp", Short: "CaraML Machine Learning Platform Console", Long: "CaraML Machine Learning Platform Console, which provides a web UI to interact with different CaraML " + "services. If no subcommand are provided, serve command will be run as default.", - // Run serve command by default if non is specified - Run: func(cmd *cobra.Command, args []string) { - serveCmd.Run(cmd, args) - }, } ) func init() { - cobra.OnInitialize(initConfig) - rootCmd.PersistentFlags().StringSliceVarP(&configFiles, "config", "c", []string{}, - "Comma separated list of config files to load. The last config file will take precedence over the "+ - "previous ones.") rootCmd.AddCommand(serveCmd) rootCmd.AddCommand(bootstrapCmd) } -func initConfig() { - var err error - globalConfig, err = config.LoadAndValidate(configFiles...) - if err != nil { - log.Fatalf("failed initializing config: %v", err) +func Execute() { + cmd, _, err := rootCmd.Find(os.Args[1:]) + // use serve as default cmd if no cmd is given + if err == nil && cmd.Use == rootCmd.Use && cmd.Flags().Parse(os.Args[1:]) != pflag.ErrHelp { + args := append([]string{serveCmd.Use}, os.Args[1:]...) + rootCmd.SetArgs(args) } -} -func Execute() { - err := rootCmd.Execute() + err = rootCmd.Execute() if err != nil { log.Fatalf("failed executing root command: %v", err) } diff --git a/api/cmd/serve.go b/api/cmd/serve.go index f26d1ebe..9d5eaf65 100644 --- a/api/cmd/serve.go +++ b/api/cmd/serve.go @@ -24,15 +24,26 @@ import ( ) var ( - serveCmd = &cobra.Command{ + configFiles []string + serveCmd = &cobra.Command{ Use: "serve", Short: "Start MLP API server", Run: func(cmd *cobra.Command, args []string) { - startServer(globalConfig) + serveConfig, err := config.LoadAndValidate(configFiles...) + if err != nil { + log.Fatalf("failed initializing config: %v", err) + } + startServer(serveConfig) }, } ) +func init() { + serveCmd.Flags().StringSliceVarP(&configFiles, "config", "c", []string{}, + "Comma separated list of config files to load. The last config file will take precedence over the "+ + "previous ones.") +} + func startServer(cfg *config.Config) { // init db db, err := database.InitDB(cfg.Database)