diff --git a/app/boot_levels.go b/app/boot_levels.go index 04eccff57..dbad65c1b 100644 --- a/app/boot_levels.go +++ b/app/boot_levels.go @@ -270,8 +270,28 @@ func (app *CortezaApp) Provision(ctx context.Context) (err error) { return } - // now set system users with so that the whole app knows what to use + // set system users & roles with so that the whole app knows what to use auth.SetSystemUsers(uu, rr) + auth.SetSystemRoles(rr) + + { + // register temporary RBAC with bypass roles + // this is needed because envoy relies on availability of access-control + // + // @todo envoy should be decoupled from RBAC and import directly into store, + // w/o using any access control + + var ( + ac = rbac.NewService(app.Log, app.Store) + acr = make([]*rbac.Role, 0) + ) + for _, r := range auth.ProvisionUser().Roles() { + acr = append(acr, rbac.BypassRole.Make(r, auth.BypassRoleHandle)) + } + ac.UpdateRoles(acr...) + rbac.SetGlobal(ac) + defer rbac.SetGlobal(nil) + } if !app.Opt.Provision.Always { app.Log.Debug("provisioning skipped (PROVISION_ALWAYS=false)") @@ -311,10 +331,11 @@ func (app *CortezaApp) InitServices(ctx context.Context) (err error) { return } - { + if rbac.Global() == nil { //Initialize RBAC subsystem - // and (re)load rules from the storage backend ac := rbac.NewService(app.Log, app.Store) + + // and (re)load rules from the storage backend ac.Reload(ctx) rbac.SetGlobal(ac) diff --git a/app/cli.go b/app/cli.go index 08309ae4b..25647ac0d 100644 --- a/app/cli.go +++ b/app/cli.go @@ -83,7 +83,7 @@ func (app *CortezaApp) InitCLI() { app.Command.AddCommand( systemCommands.Users(ctx, app), systemCommands.Roles(ctx, app), - systemCommands.RBAC(ctx, app), + systemCommands.RBAC(ctx, storeInit), systemCommands.Sink(ctx, app), systemCommands.Settings(ctx, app), systemCommands.Import(ctx, storeInit), diff --git a/auth/handlers/handle_oauth2.go b/auth/handlers/handle_oauth2.go index 292970149..d211932c3 100644 --- a/auth/handlers/handle_oauth2.go +++ b/auth/handlers/handle_oauth2.go @@ -4,6 +4,13 @@ import ( "context" "encoding/json" "fmt" + "html/template" + "net/http" + "net/url" + "strconv" + "strings" + "time" + "github.com/cortezaproject/corteza-server/auth/oauth2" "github.com/cortezaproject/corteza-server/auth/request" "github.com/cortezaproject/corteza-server/pkg/auth" @@ -13,12 +20,6 @@ import ( oauth2def "github.com/go-oauth2/oauth2/v4" oauth2errors "github.com/go-oauth2/oauth2/v4/errors" "go.uber.org/zap" - "html/template" - "net/http" - "net/url" - "strconv" - "strings" - "time" ) // oauth2 flow authorize step @@ -414,7 +415,7 @@ func Profile(ctx context.Context, ti oauth2def.TokenInfo, data map[string]interf return nil } - userID := auth.ExtractUserIDFromSubClaim(ti.GetUserID()) + userID, roles := auth.ExtractFromSubClaim(ti.GetUserID()) if userID == 0 { return fmt.Errorf("invalid user ID in 'sub' claim") } @@ -422,7 +423,7 @@ func Profile(ctx context.Context, ti oauth2def.TokenInfo, data map[string]interf user, err := systemService.DefaultUser.FindByID( // inject ad-hoc identity into context so that user service is aware who is // doing the lookup - auth.SetIdentityToContext(ctx, auth.NewIdentity(userID)), + auth.SetIdentityToContext(ctx, auth.Authenticated(userID, roles...)), userID, ) diff --git a/auth/handlers/handle_password-reset_test.go b/auth/handlers/handle_password-reset_test.go index 01038f821..f567e9276 100644 --- a/auth/handlers/handle_password-reset_test.go +++ b/auth/handlers/handle_password-reset_test.go @@ -72,7 +72,7 @@ func Test_resetPasswordForm(t *testing.T) { authService = &authServiceMocked{ validatePasswordResetToken: func(ctx context.Context, token string) (user *types.User, err error) { u := makeMockUser(ctx) - u.SetRoles([]uint64{}) + u.SetRoles() return u, nil }, diff --git a/auth/handlers/handle_profile_test.go b/auth/handlers/handle_profile_test.go index 1946f362c..6119b6c48 100644 --- a/auth/handlers/handle_profile_test.go +++ b/auth/handlers/handle_profile_test.go @@ -83,7 +83,7 @@ func Test_profileFormProc(t *testing.T) { userService = &userServiceMocked{ update: func(c context.Context, u *types.User) (*types.User, error) { u = makeMockUser(ctx) - u.SetRoles([]uint64{}) + u.SetRoles() return u, nil }, diff --git a/auth/handlers/handler.go b/auth/handlers/handler.go index 4818eb3cd..b60d9b7b8 100644 --- a/auth/handlers/handler.go +++ b/auth/handlers/handler.go @@ -163,7 +163,10 @@ func (h *AuthHandlers) handle(fn handlerFn) http.HandlerFunc { // so we can properly identify ourselves when interacting // with services if req.AuthUser != nil && !req.AuthUser.PendingMFA() { - req.Request = req.Request.Clone(auth.SetIdentityToContext(req.Context(), req.AuthUser.User)) + req.Request = req.Request.Clone(auth.SetIdentityToContext( + req.Context(), + auth.Authenticated(req.AuthUser.User.ID, req.AuthUser.User.Roles()...), + )) } // Alerts show for 1 session only! diff --git a/auth/handlers/routes.go b/auth/handlers/routes.go index b49b0d0e9..a8e6674a7 100644 --- a/auth/handlers/routes.go +++ b/auth/handlers/routes.go @@ -30,7 +30,7 @@ func (h *AuthHandlers) MountHttpRoutes(r chi.Router) { }) if h.Opt.RequestRateLimit > 0 { - r.Use(httprate.LimitByIP(h.Opt.RequestRateLimit, h.Opt.RequestRateWindowLength)) // @todo make configurable + r.Use(httprate.LimitByIP(h.Opt.RequestRateLimit, h.Opt.RequestRateWindowLength)) } r.Use(request.ExtraReqInfoMiddleware) diff --git a/auth/oauth2/corteza_token_store.go b/auth/oauth2/corteza_token_store.go index 13065e2aa..52873c596 100644 --- a/auth/oauth2/corteza_token_store.go +++ b/auth/oauth2/corteza_token_store.go @@ -4,6 +4,9 @@ import ( "context" "encoding/json" "fmt" + "strconv" + "time" + "github.com/cortezaproject/corteza-server/auth/request" "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/errors" @@ -13,8 +16,6 @@ import ( "github.com/go-oauth2/oauth2/v4" oauth2errors "github.com/go-oauth2/oauth2/v4/errors" oauth2models "github.com/go-oauth2/oauth2/v4/models" - "strconv" - "time" ) type ( @@ -81,7 +82,7 @@ func (c CortezaTokenStore) Create(ctx context.Context, info oauth2.TokenInfo) (e acc.ClientID = oa2t.ClientID if info.GetUserID() != "" { - if oa2t.UserID = auth.ExtractUserIDFromSubClaim(info.GetUserID()); oa2t.UserID == 0 { + if oa2t.UserID, _ = auth.ExtractFromSubClaim(info.GetUserID()); oa2t.UserID == 0 { // UserID stores collection of IDs: user's ID and set of all roles user is member of return fmt.Errorf("could not parse user ID from token info") } diff --git a/automation/service/access_control.gen.go b/automation/service/access_control.gen.go index f567e05bf..1c43b916c 100644 --- a/automation/service/access_control.gen.go +++ b/automation/service/access_control.gen.go @@ -56,20 +56,76 @@ func (svc accessControl) Effective(ctx context.Context, rr ...rbac.Resource) (ee } func (svc accessControl) List() (out []map[string]string) { - return []map[string]string{ - {"resource": "corteza+automation.workflow", "operation": "read"}, - {"resource": "corteza+automation.workflow", "operation": "update"}, - {"resource": "corteza+automation.workflow", "operation": "delete"}, - {"resource": "corteza+automation.workflow", "operation": "undelete"}, - {"resource": "corteza+automation.workflow", "operation": "execute"}, - {"resource": "corteza+automation.workflow", "operation": "triggers.manage"}, - {"resource": "corteza+automation.workflow", "operation": "sessions.manage"}, - {"resource": "corteza+automation", "operation": "grant"}, - {"resource": "corteza+automation", "operation": "workflow.create"}, - {"resource": "corteza+automation", "operation": "triggers.search"}, - {"resource": "corteza+automation", "operation": "sessions.search"}, - {"resource": "corteza+automation", "operation": "workflows.search"}, + def := []map[string]string{ + { + "type": types.WorkflowResourceType, + "any": types.WorkflowRbacResource(0), + "op": "read", + }, + { + "type": types.WorkflowResourceType, + "any": types.WorkflowRbacResource(0), + "op": "update", + }, + { + "type": types.WorkflowResourceType, + "any": types.WorkflowRbacResource(0), + "op": "delete", + }, + { + "type": types.WorkflowResourceType, + "any": types.WorkflowRbacResource(0), + "op": "undelete", + }, + { + "type": types.WorkflowResourceType, + "any": types.WorkflowRbacResource(0), + "op": "execute", + }, + { + "type": types.WorkflowResourceType, + "any": types.WorkflowRbacResource(0), + "op": "triggers.manage", + }, + { + "type": types.WorkflowResourceType, + "any": types.WorkflowRbacResource(0), + "op": "sessions.manage", + }, + { + "type": types.ComponentResourceType, + "any": types.ComponentRbacResource(), + "op": "grant", + }, + { + "type": types.ComponentResourceType, + "any": types.ComponentRbacResource(), + "op": "workflow.create", + }, + { + "type": types.ComponentResourceType, + "any": types.ComponentRbacResource(), + "op": "triggers.search", + }, + { + "type": types.ComponentResourceType, + "any": types.ComponentRbacResource(), + "op": "sessions.search", + }, + { + "type": types.ComponentResourceType, + "any": types.ComponentRbacResource(), + "op": "workflows.search", + }, } + + func(svc interface{}) { + if svc, is := svc.(interface{}).(interface{ list() []map[string]string }); is { + def = append(def, svc.list()...) + } + }(svc) + + return def } // Grant applies one or more RBAC rules @@ -211,22 +267,22 @@ func (svc accessControl) CanSearchWorkflows(ctx context.Context) bool { // // This function is auto-generated func rbacResourceValidator(r string, oo ...string) error { - switch rbac.ResourceSchema(r) { - case "corteza+automation.workflow": + switch rbac.ResourceType(r) { + case types.WorkflowResourceType: return rbacWorkflowResourceValidator(r, oo...) - case "corteza+automation": + case types.ComponentResourceType: return rbacComponentResourceValidator(r, oo...) } - return fmt.Errorf("unknown resource schema '%q'", r) + return fmt.Errorf("unknown resource type '%q'", r) } // rbacResourceOperations returns defined operations for a requested resource // // This function is auto-generated func rbacResourceOperations(r string) map[string]bool { - switch rbac.ResourceSchema(r) { - case "corteza+automation.workflow": + switch rbac.ResourceType(r) { + case types.WorkflowResourceType: return map[string]bool{ "read": true, "update": true, @@ -236,7 +292,7 @@ func rbacResourceOperations(r string) map[string]bool { "triggers.manage": true, "sessions.manage": true, } - case "corteza+automation": + case types.ComponentResourceType: return map[string]bool{ "grant": true, "workflow.create": true, @@ -262,34 +318,38 @@ func rbacWorkflowResourceValidator(r string, oo ...string) error { } } - if !strings.HasPrefix(r, types.WorkflowRbacResourceSchema+":/") { - return fmt.Errorf("invalid schema") - } - - pp := strings.Split(r[len(types.WorkflowRbacResourceSchema)+2:], "/") - if len(pp) != 1 { - return fmt.Errorf("invalid resource path") + if !strings.HasPrefix(r, types.WorkflowResourceType) { + // expecting resource to always include path + return fmt.Errorf("invalid resource type") } + const sep = "/" var ( - ppWildcard bool - pathElements = []string{ + specIdUsed = true + + pp = strings.Split(strings.Trim(r[len(types.WorkflowResourceType):], sep), sep) + prc = []string{ "ID", } ) + if len(pp) != len(prc) { + return fmt.Errorf("invalid resource path structure") + } + for i, p := range pp { if p == "*" { - ppWildcard = true + if !specIdUsed { + return fmt.Errorf("invalid resource path wildcard level (%d) for Workflow", i) + } + + specIdUsed = false continue } - if !ppWildcard { - return fmt.Errorf("invalid resource path wildcard level") - } - + specIdUsed = true if _, err := cast.ToUint64E(p); err != nil { - return fmt.Errorf("invalid ID for %s: '%s'", pathElements[i], p) + return fmt.Errorf("invalid reference for %s: '%s'", prc[i], p) } } @@ -309,8 +369,9 @@ func rbacComponentResourceValidator(r string, oo ...string) error { } } - if !strings.HasPrefix(r, types.ComponentRbacResourceSchema+":/") { - return fmt.Errorf("invalid schema") + if !strings.HasPrefix(r, types.ComponentResourceType) { + // expecting resource to always include path + return fmt.Errorf("invalid resource type") } return nil diff --git a/automation/types/rbac.gen.go b/automation/types/rbac.gen.go index ef0fcc7a0..67b46ebcd 100644 --- a/automation/types/rbac.gen.go +++ b/automation/types/rbac.gen.go @@ -11,6 +11,7 @@ package types // - automation.yaml import ( + "fmt" "strconv" ) @@ -22,13 +23,13 @@ type ( ) const ( - WorkflowRbacResourceSchema = "corteza+automation.workflow" - ComponentRbacResourceSchema = "corteza+automation" + WorkflowResourceType = "corteza::automation:workflow" + ComponentResourceType = "corteza::automation" ) // RbacResource returns string representation of RBAC resource for Workflow by calling WorkflowRbacResource fn // -// RBAC resource is in the corteza+automation.workflow:/... format +// RBAC resource is in the corteza::automation:workflow/... format // // This function is auto-generated func (r Workflow) RbacResource() string { @@ -37,24 +38,29 @@ func (r Workflow) RbacResource() string { // WorkflowRbacResource returns string representation of RBAC resource for Workflow // -// RBAC resource is in the corteza+automation.workflow:/... format +// RBAC resource is in the corteza::automation:workflow/... format // // This function is auto-generated -func WorkflowRbacResource(iD uint64) string { - out := WorkflowRbacResourceSchema + ":" - out += "/" - - if iD != 0 { - out += strconv.FormatUint(iD, 10) +func WorkflowRbacResource(id uint64) string { + cpts := []interface{}{WorkflowResourceType} + if id != 0 { + cpts = append(cpts, strconv.FormatUint(id, 10)) } else { - out += "*" + cpts = append(cpts, "*") } - return out + + return fmt.Sprintf(WorkflowRbacResourceTpl(), cpts...) + +} + +// @todo template +func WorkflowRbacResourceTpl() string { + return "%s/%s" } // RbacResource returns string representation of RBAC resource for Component by calling ComponentRbacResource fn // -// RBAC resource is in the corteza+automation:/... format +// RBAC resource is in the corteza::automation/... format // // This function is auto-generated func (r Component) RbacResource() string { @@ -63,10 +69,15 @@ func (r Component) RbacResource() string { // ComponentRbacResource returns string representation of RBAC resource for Component // -// RBAC resource is in the corteza+automation:/... format +// RBAC resource is in the corteza::automation/ format // // This function is auto-generated func ComponentRbacResource() string { - out := ComponentRbacResourceSchema + ":" - return out + return ComponentResourceType + "/" + +} + +// @todo template +func ComponentRbacResourceTpl() string { + return "%s" } diff --git a/compose/rest/namespace.go b/compose/rest/namespace.go index 378c38f87..a2a143387 100644 --- a/compose/rest/namespace.go +++ b/compose/rest/namespace.go @@ -41,6 +41,7 @@ type ( CanUpdateNamespace(context.Context, *types.Namespace) bool CanDeleteNamespace(context.Context, *types.Namespace) bool + CanManageNamespace(context.Context, *types.Namespace) bool CanCreateModuleOnNamespace(context.Context, *types.Namespace) bool CanCreateChartOnNamespace(context.Context, *types.Namespace) bool @@ -176,6 +177,7 @@ func (ctrl Namespace) makePayload(ctx context.Context, ns *types.Namespace, err CanGrant: ctrl.ac.CanGrant(ctx), CanUpdateNamespace: ctrl.ac.CanUpdateNamespace(ctx, ns), CanDeleteNamespace: ctrl.ac.CanDeleteNamespace(ctx, ns), + CanManageNamespace: ctrl.ac.CanManageNamespace(ctx, ns), CanCreateModule: ctrl.ac.CanCreateModuleOnNamespace(ctx, ns), CanCreateChart: ctrl.ac.CanCreateChartOnNamespace(ctx, ns), diff --git a/compose/rest/record.go b/compose/rest/record.go index 6333da132..b5e3589c6 100644 --- a/compose/rest/record.go +++ b/compose/rest/record.go @@ -495,7 +495,7 @@ func (ctrl *Record) Export(ctx context.Context, r *request.RecordExport) (interf // Find only the stream we are interested in for _, s := range ss { - if s.Resource == resource.COMPOSE_RECORD_RESOURCE_TYPE { + if s.Resource == types.RecordResourceType { io.Copy(w, s.Source) } } diff --git a/compose/service/access_control.gen.go b/compose/service/access_control.gen.go index 04a37b51c..9e6f4c46d 100644 --- a/compose/service/access_control.gen.go +++ b/compose/service/access_control.gen.go @@ -18,11 +18,12 @@ package service import ( "context" "fmt" + "strings" + "github.com/cortezaproject/corteza-server/compose/types" "github.com/cortezaproject/corteza-server/pkg/actionlog" "github.com/cortezaproject/corteza-server/pkg/rbac" "github.com/spf13/cast" - "strings" ) type ( @@ -61,34 +62,146 @@ func (svc accessControl) Effective(ctx context.Context, rr ...rbac.Resource) (ee } func (svc accessControl) List() (out []map[string]string) { - return []map[string]string{ - {"resource": "corteza+compose.chart", "operation": "read"}, - {"resource": "corteza+compose.chart", "operation": "update"}, - {"resource": "corteza+compose.chart", "operation": "delete"}, - {"resource": "corteza+compose.module-field", "operation": "record.value.read"}, - {"resource": "corteza+compose.module-field", "operation": "record.value.update"}, - {"resource": "corteza+compose.module", "operation": "read"}, - {"resource": "corteza+compose.module", "operation": "update"}, - {"resource": "corteza+compose.module", "operation": "delete"}, - {"resource": "corteza+compose.module", "operation": "record.create"}, - {"resource": "corteza+compose.namespace", "operation": "read"}, - {"resource": "corteza+compose.namespace", "operation": "update"}, - {"resource": "corteza+compose.namespace", "operation": "delete"}, - {"resource": "corteza+compose.namespace", "operation": "module.create"}, - {"resource": "corteza+compose.namespace", "operation": "chart.create"}, - {"resource": "corteza+compose.namespace", "operation": "page.create"}, - {"resource": "corteza+compose.page", "operation": "read"}, - {"resource": "corteza+compose.page", "operation": "create"}, - {"resource": "corteza+compose.page", "operation": "update"}, - {"resource": "corteza+compose.page", "operation": "delete"}, - {"resource": "corteza+compose.record", "operation": "read"}, - {"resource": "corteza+compose.record", "operation": "update"}, - {"resource": "corteza+compose.record", "operation": "delete"}, - {"resource": "corteza+compose", "operation": "grant"}, - {"resource": "corteza+compose", "operation": "namespace.create"}, - {"resource": "corteza+compose", "operation": "settings.read"}, - {"resource": "corteza+compose", "operation": "settings.manage"}, + def := []map[string]string{ + { + "type": types.ChartResourceType, + "any": types.ChartRbacResource(0, 0), + "op": "read", + }, + { + "type": types.ChartResourceType, + "any": types.ChartRbacResource(0, 0), + "op": "update", + }, + { + "type": types.ChartResourceType, + "any": types.ChartRbacResource(0, 0), + "op": "delete", + }, + { + "type": types.ModuleFieldResourceType, + "any": types.ModuleFieldRbacResource(0, 0, 0), + "op": "record.value.read", + }, + { + "type": types.ModuleFieldResourceType, + "any": types.ModuleFieldRbacResource(0, 0, 0), + "op": "record.value.update", + }, + { + "type": types.ModuleResourceType, + "any": types.ModuleRbacResource(0, 0), + "op": "read", + }, + { + "type": types.ModuleResourceType, + "any": types.ModuleRbacResource(0, 0), + "op": "update", + }, + { + "type": types.ModuleResourceType, + "any": types.ModuleRbacResource(0, 0), + "op": "delete", + }, + { + "type": types.ModuleResourceType, + "any": types.ModuleRbacResource(0, 0), + "op": "record.create", + }, + { + "type": types.NamespaceResourceType, + "any": types.NamespaceRbacResource(0), + "op": "read", + }, + { + "type": types.NamespaceResourceType, + "any": types.NamespaceRbacResource(0), + "op": "update", + }, + { + "type": types.NamespaceResourceType, + "any": types.NamespaceRbacResource(0), + "op": "delete", + }, + { + "type": types.NamespaceResourceType, + "any": types.NamespaceRbacResource(0), + "op": "manage", + }, + { + "type": types.NamespaceResourceType, + "any": types.NamespaceRbacResource(0), + "op": "module.create", + }, + { + "type": types.NamespaceResourceType, + "any": types.NamespaceRbacResource(0), + "op": "chart.create", + }, + { + "type": types.NamespaceResourceType, + "any": types.NamespaceRbacResource(0), + "op": "page.create", + }, + { + "type": types.PageResourceType, + "any": types.PageRbacResource(0, 0), + "op": "read", + }, + { + "type": types.PageResourceType, + "any": types.PageRbacResource(0, 0), + "op": "update", + }, + { + "type": types.PageResourceType, + "any": types.PageRbacResource(0, 0), + "op": "delete", + }, + { + "type": types.RecordResourceType, + "any": types.RecordRbacResource(0, 0, 0), + "op": "read", + }, + { + "type": types.RecordResourceType, + "any": types.RecordRbacResource(0, 0, 0), + "op": "update", + }, + { + "type": types.RecordResourceType, + "any": types.RecordRbacResource(0, 0, 0), + "op": "delete", + }, + { + "type": types.ComponentResourceType, + "any": types.ComponentRbacResource(), + "op": "grant", + }, + { + "type": types.ComponentResourceType, + "any": types.ComponentRbacResource(), + "op": "namespace.create", + }, + { + "type": types.ComponentResourceType, + "any": types.ComponentRbacResource(), + "op": "settings.read", + }, + { + "type": types.ComponentResourceType, + "any": types.ComponentRbacResource(), + "op": "settings.manage", + }, } + + func(svc interface{}) { + if svc, is := svc.(interface{}).(interface{ list() []map[string]string }); is { + def = append(def, svc.list()...) + } + }(svc) + + return def } // Grant applies one or more RBAC rules @@ -226,6 +339,13 @@ func (svc accessControl) CanDeleteNamespace(ctx context.Context, r *types.Namesp return svc.can(ctx, "delete", r) } +// CanManageNamespace checks if current user can access to namespace admin panel +// +// This function is auto-generated +func (svc accessControl) CanManageNamespace(ctx context.Context, r *types.Namespace) bool { + return svc.can(ctx, "manage", r) +} + // CanCreateModuleOnNamespace checks if current user can create module on namespace // // This function is auto-generated @@ -254,13 +374,6 @@ func (svc accessControl) CanReadPage(ctx context.Context, r *types.Page) bool { return svc.can(ctx, "read", r) } -// CanCreatePage checks if current user can create page -// -// This function is auto-generated -func (svc accessControl) CanCreatePage(ctx context.Context, r *types.Page) bool { - return svc.can(ctx, "create", r) -} - // CanUpdatePage checks if current user can update page // // This function is auto-generated @@ -328,72 +441,72 @@ func (svc accessControl) CanManageSettings(ctx context.Context) bool { // // This function is auto-generated func rbacResourceValidator(r string, oo ...string) error { - switch rbac.ResourceSchema(r) { - case "corteza+compose.chart": + switch rbac.ResourceType(r) { + case types.ChartResourceType: return rbacChartResourceValidator(r, oo...) - case "corteza+compose.module-field": + case types.ModuleFieldResourceType: return rbacModuleFieldResourceValidator(r, oo...) - case "corteza+compose.module": + case types.ModuleResourceType: return rbacModuleResourceValidator(r, oo...) - case "corteza+compose.namespace": + case types.NamespaceResourceType: return rbacNamespaceResourceValidator(r, oo...) - case "corteza+compose.page": + case types.PageResourceType: return rbacPageResourceValidator(r, oo...) - case "corteza+compose.record": + case types.RecordResourceType: return rbacRecordResourceValidator(r, oo...) - case "corteza+compose": + case types.ComponentResourceType: return rbacComponentResourceValidator(r, oo...) } - return fmt.Errorf("unknown resource schema '%q'", r) + return fmt.Errorf("unknown resource type '%q'", r) } // rbacResourceOperations returns defined operations for a requested resource // // This function is auto-generated func rbacResourceOperations(r string) map[string]bool { - switch rbac.ResourceSchema(r) { - case "corteza+compose.chart": + switch rbac.ResourceType(r) { + case types.ChartResourceType: return map[string]bool{ "read": true, "update": true, "delete": true, } - case "corteza+compose.module-field": + case types.ModuleFieldResourceType: return map[string]bool{ "record.value.read": true, "record.value.update": true, } - case "corteza+compose.module": + case types.ModuleResourceType: return map[string]bool{ "read": true, "update": true, "delete": true, "record.create": true, } - case "corteza+compose.namespace": + case types.NamespaceResourceType: return map[string]bool{ "read": true, "update": true, "delete": true, + "manage": true, "module.create": true, "chart.create": true, "page.create": true, } - case "corteza+compose.page": - return map[string]bool{ - "read": true, - "create": true, - "update": true, - "delete": true, - } - case "corteza+compose.record": + case types.PageResourceType: return map[string]bool{ "read": true, "update": true, "delete": true, } - case "corteza+compose": + case types.RecordResourceType: + return map[string]bool{ + "read": true, + "update": true, + "delete": true, + } + case types.ComponentResourceType: return map[string]bool{ "grant": true, "namespace.create": true, @@ -418,35 +531,39 @@ func rbacChartResourceValidator(r string, oo ...string) error { } } - if !strings.HasPrefix(r, types.ChartRbacResourceSchema+":/") { - return fmt.Errorf("invalid schema") - } - - pp := strings.Split(r[len(types.ChartRbacResourceSchema)+2:], "/") - if len(pp) != 2 { - return fmt.Errorf("invalid resource path") + if !strings.HasPrefix(r, types.ChartResourceType) { + // expecting resource to always include path + return fmt.Errorf("invalid resource type") } + const sep = "/" var ( - ppWildcard bool - pathElements = []string{ + specIdUsed = true + + pp = strings.Split(strings.Trim(r[len(types.ChartResourceType):], sep), sep) + prc = []string{ "namespaceID", "ID", } ) + if len(pp) != len(prc) { + return fmt.Errorf("invalid resource path structure") + } + for i, p := range pp { if p == "*" { - ppWildcard = true + if !specIdUsed { + return fmt.Errorf("invalid resource path wildcard level (%d) for Chart", i) + } + + specIdUsed = false continue } - if !ppWildcard { - return fmt.Errorf("invalid resource path wildcard level") - } - + specIdUsed = true if _, err := cast.ToUint64E(p); err != nil { - return fmt.Errorf("invalid ID for %s: '%s'", pathElements[i], p) + return fmt.Errorf("invalid reference for %s: '%s'", prc[i], p) } } @@ -466,36 +583,40 @@ func rbacModuleFieldResourceValidator(r string, oo ...string) error { } } - if !strings.HasPrefix(r, types.ModuleFieldRbacResourceSchema+":/") { - return fmt.Errorf("invalid schema") - } - - pp := strings.Split(r[len(types.ModuleFieldRbacResourceSchema)+2:], "/") - if len(pp) != 3 { - return fmt.Errorf("invalid resource path") + if !strings.HasPrefix(r, types.ModuleFieldResourceType) { + // expecting resource to always include path + return fmt.Errorf("invalid resource type") } + const sep = "/" var ( - ppWildcard bool - pathElements = []string{ + specIdUsed = true + + pp = strings.Split(strings.Trim(r[len(types.ModuleFieldResourceType):], sep), sep) + prc = []string{ "namespaceID", "moduleID", "ID", } ) + if len(pp) != len(prc) { + return fmt.Errorf("invalid resource path structure") + } + for i, p := range pp { if p == "*" { - ppWildcard = true + if !specIdUsed { + return fmt.Errorf("invalid resource path wildcard level (%d) for ModuleField", i) + } + + specIdUsed = false continue } - if !ppWildcard { - return fmt.Errorf("invalid resource path wildcard level") - } - + specIdUsed = true if _, err := cast.ToUint64E(p); err != nil { - return fmt.Errorf("invalid ID for %s: '%s'", pathElements[i], p) + return fmt.Errorf("invalid reference for %s: '%s'", prc[i], p) } } @@ -515,35 +636,39 @@ func rbacModuleResourceValidator(r string, oo ...string) error { } } - if !strings.HasPrefix(r, types.ModuleRbacResourceSchema+":/") { - return fmt.Errorf("invalid schema") - } - - pp := strings.Split(r[len(types.ModuleRbacResourceSchema)+2:], "/") - if len(pp) != 2 { - return fmt.Errorf("invalid resource path") + if !strings.HasPrefix(r, types.ModuleResourceType) { + // expecting resource to always include path + return fmt.Errorf("invalid resource type") } + const sep = "/" var ( - ppWildcard bool - pathElements = []string{ + specIdUsed = true + + pp = strings.Split(strings.Trim(r[len(types.ModuleResourceType):], sep), sep) + prc = []string{ "namespaceID", "ID", } ) + if len(pp) != len(prc) { + return fmt.Errorf("invalid resource path structure") + } + for i, p := range pp { if p == "*" { - ppWildcard = true + if !specIdUsed { + return fmt.Errorf("invalid resource path wildcard level (%d) for Module", i) + } + + specIdUsed = false continue } - if !ppWildcard { - return fmt.Errorf("invalid resource path wildcard level") - } - + specIdUsed = true if _, err := cast.ToUint64E(p); err != nil { - return fmt.Errorf("invalid ID for %s: '%s'", pathElements[i], p) + return fmt.Errorf("invalid reference for %s: '%s'", prc[i], p) } } @@ -563,34 +688,38 @@ func rbacNamespaceResourceValidator(r string, oo ...string) error { } } - if !strings.HasPrefix(r, types.NamespaceRbacResourceSchema+":/") { - return fmt.Errorf("invalid schema") - } - - pp := strings.Split(r[len(types.NamespaceRbacResourceSchema)+2:], "/") - if len(pp) != 1 { - return fmt.Errorf("invalid resource path") + if !strings.HasPrefix(r, types.NamespaceResourceType) { + // expecting resource to always include path + return fmt.Errorf("invalid resource type") } + const sep = "/" var ( - ppWildcard bool - pathElements = []string{ + specIdUsed = true + + pp = strings.Split(strings.Trim(r[len(types.NamespaceResourceType):], sep), sep) + prc = []string{ "ID", } ) + if len(pp) != len(prc) { + return fmt.Errorf("invalid resource path structure") + } + for i, p := range pp { if p == "*" { - ppWildcard = true + if !specIdUsed { + return fmt.Errorf("invalid resource path wildcard level (%d) for Namespace", i) + } + + specIdUsed = false continue } - if !ppWildcard { - return fmt.Errorf("invalid resource path wildcard level") - } - + specIdUsed = true if _, err := cast.ToUint64E(p); err != nil { - return fmt.Errorf("invalid ID for %s: '%s'", pathElements[i], p) + return fmt.Errorf("invalid reference for %s: '%s'", prc[i], p) } } @@ -610,35 +739,39 @@ func rbacPageResourceValidator(r string, oo ...string) error { } } - if !strings.HasPrefix(r, types.PageRbacResourceSchema+":/") { - return fmt.Errorf("invalid schema") - } - - pp := strings.Split(r[len(types.PageRbacResourceSchema)+2:], "/") - if len(pp) != 2 { - return fmt.Errorf("invalid resource path") + if !strings.HasPrefix(r, types.PageResourceType) { + // expecting resource to always include path + return fmt.Errorf("invalid resource type") } + const sep = "/" var ( - ppWildcard bool - pathElements = []string{ + specIdUsed = true + + pp = strings.Split(strings.Trim(r[len(types.PageResourceType):], sep), sep) + prc = []string{ "namespaceID", "ID", } ) + if len(pp) != len(prc) { + return fmt.Errorf("invalid resource path structure") + } + for i, p := range pp { if p == "*" { - ppWildcard = true + if !specIdUsed { + return fmt.Errorf("invalid resource path wildcard level (%d) for Page", i) + } + + specIdUsed = false continue } - if !ppWildcard { - return fmt.Errorf("invalid resource path wildcard level") - } - + specIdUsed = true if _, err := cast.ToUint64E(p); err != nil { - return fmt.Errorf("invalid ID for %s: '%s'", pathElements[i], p) + return fmt.Errorf("invalid reference for %s: '%s'", prc[i], p) } } @@ -658,36 +791,40 @@ func rbacRecordResourceValidator(r string, oo ...string) error { } } - if !strings.HasPrefix(r, types.RecordRbacResourceSchema+":/") { - return fmt.Errorf("invalid schema") - } - - pp := strings.Split(r[len(types.RecordRbacResourceSchema)+2:], "/") - if len(pp) != 3 { - return fmt.Errorf("invalid resource path") + if !strings.HasPrefix(r, types.RecordResourceType) { + // expecting resource to always include path + return fmt.Errorf("invalid resource type") } + const sep = "/" var ( - ppWildcard bool - pathElements = []string{ + specIdUsed = true + + pp = strings.Split(strings.Trim(r[len(types.RecordResourceType):], sep), sep) + prc = []string{ "namespaceID", "moduleID", "ID", } ) + if len(pp) != len(prc) { + return fmt.Errorf("invalid resource path structure") + } + for i, p := range pp { if p == "*" { - ppWildcard = true + if !specIdUsed { + return fmt.Errorf("invalid resource path wildcard level (%d) for Record", i) + } + + specIdUsed = false continue } - if !ppWildcard { - return fmt.Errorf("invalid resource path wildcard level") - } - + specIdUsed = true if _, err := cast.ToUint64E(p); err != nil { - return fmt.Errorf("invalid ID for %s: '%s'", pathElements[i], p) + return fmt.Errorf("invalid reference for %s: '%s'", prc[i], p) } } @@ -707,8 +844,9 @@ func rbacComponentResourceValidator(r string, oo ...string) error { } } - if !strings.HasPrefix(r, types.ComponentRbacResourceSchema+":/") { - return fmt.Errorf("invalid schema") + if !strings.HasPrefix(r, types.ComponentResourceType) { + // expecting resource to always include path + return fmt.Errorf("invalid resource type") } return nil diff --git a/compose/service/chart_test.go b/compose/service/chart_test.go index ecb308cc1..f68131a41 100644 --- a/compose/service/chart_test.go +++ b/compose/service/chart_test.go @@ -2,9 +2,10 @@ package service import ( "context" - "github.com/cortezaproject/corteza-server/pkg/rbac" "testing" + "github.com/cortezaproject/corteza-server/pkg/rbac" + "github.com/cortezaproject/corteza-server/compose/types" "github.com/cortezaproject/corteza-server/pkg/errors" "github.com/cortezaproject/corteza-server/store" diff --git a/compose/service/module.go b/compose/service/module.go index 555edbe3b..36b2b1e29 100644 --- a/compose/service/module.go +++ b/compose/service/module.go @@ -593,7 +593,7 @@ func updateModuleFields(ctx context.Context, s store.Storer, new, old *types.Mod func moduleFieldDefaultPreparer(ctx context.Context, s store.Storer, m *types.Module, newFields types.ModuleFieldSet) (types.ModuleFieldSet, error) { var err error - // prepare an auxillary module to perform isolated validations on + // prepare an auxiliary module to perform isolated validations on auxm := &types.Module{ Handle: "aux_module", NamespaceID: m.NamespaceID, @@ -623,7 +623,8 @@ func moduleFieldDefaultPreparer(ctx context.Context, s store.Storer, m *types.Mo r := &types.Record{ Values: vv, } - rve := defaultValidator().Run(ctx, s, auxm, r) + + rve := defaultValidator(nil).Run(ctx, s, auxm, r) if !rve.IsValid() { return nil, rve } diff --git a/compose/service/record.go b/compose/service/record.go index 93cf1abb2..3d8541618 100644 --- a/compose/service/record.go +++ b/compose/service/record.go @@ -143,8 +143,7 @@ type ( ) func Record() RecordService { - - return &record{ + svc := &record{ actionlog: DefaultActionlog, ac: DefaultAccessControl, eventbus: eventbus.Service(), @@ -153,11 +152,14 @@ func Record() RecordService { formatter: values.Formatter(), sanitizer: values.Sanitizer(), - validator: defaultValidator(), } + + svc.validator = defaultValidator(svc) + + return svc } -func defaultValidator() recordValuesValidator { +func defaultValidator(svc *record) recordValuesValidator { // Initialize validator and setup all checkers it needs validator := values.Validator() @@ -170,11 +172,12 @@ func defaultValidator() recordValuesValidator { }) validator.RecordRefChecker(func(ctx context.Context, s store.Storer, v *types.RecordValue, f *types.ModuleField, m *types.Module) (bool, error) { - if v.Ref == 0 { + if svc == nil && v.Ref == 0 { return false, nil } - r, err := store.LookupComposeRecordByID(ctx, s, m, v.Ref) + //r, err := store.LookupComposeRecordByID(ctx, s, m, v.Ref) + r, err := svc.FindByID(ctx, f.NamespaceID, f.ModuleID, v.Ref) return r != nil, err }) @@ -485,7 +488,7 @@ func (svc record) create(ctx context.Context, new *types.Record) (rec *types.Rec new.SetModule(m) if svc.optEmitEvents { - if rve = svc.procCreate(ctx, svc.store, invokerID, m, new); !rve.IsValid() { + if rve = svc.procCreate(ctx, invokerID, m, new); !rve.IsValid() { return nil, RecordErrValueInput().Wrap(rve) } @@ -499,7 +502,7 @@ func (svc record) create(ctx context.Context, new *types.Record) (rec *types.Rec new.Values = RecordValueDefaults(m, new.Values) // Handle payload from automation scripts - if rve = svc.procCreate(ctx, svc.store, invokerID, m, new); !rve.IsValid() { + if rve = svc.procCreate(ctx, invokerID, m, new); !rve.IsValid() { return nil, RecordErrValueInput().Wrap(rve) } @@ -608,24 +611,7 @@ func RecordUpdateOwner(invokerID uint64, r, old *types.Record) *types.Record { return r } -func RecordValueMerger(ctx context.Context, ac recordValueAccessController, m *types.Module, vv, old types.RecordValueSet) (types.RecordValueSet, *types.RecordValueErrorSet) { - if old != nil { - // Value merge process does not know anything about permissions so - // in case when new values are missing but do exist in the old set and their update/read is denied - // we need to copy them to ensure value merge process them correctly - for _, f := range m.Fields { - if len(vv.FilterByName(f.Name)) == 0 && !ac.CanUpdateRecordValue(ctx, m.Fields.FindByName(f.Name)) { - // copy all fields from old to new - vv = append(vv, old.FilterByName(f.Name).GetClean()...) - } - } - - // Merge new (updated) values with old ones - // This way we get list of updated, stale and deleted values - // that we can selectively update in the repository - vv = old.Merge(vv) - } - +func RecordValueUpdateOpCheck(ctx context.Context, ac recordValueAccessController, m *types.Module, vv types.RecordValueSet) *types.RecordValueErrorSet { rve := &types.RecordValueErrorSet{} _ = vv.Walk(func(v *types.RecordValue) error { f := m.Fields.FindByName(v.Name) @@ -642,7 +628,7 @@ func RecordValueMerger(ctx context.Context, ac recordValueAccessController, m *t return nil }) - return vv, rve + return rve } func RecordPreparer(ctx context.Context, s store.Storer, ss recordValuesSanitizer, vv recordValuesValidator, ff recordValuesFormatter, m *types.Module, new *types.Record) *types.RecordValueErrorSet { @@ -744,7 +730,7 @@ func (svc record) update(ctx context.Context, upd *types.Record) (rec *types.Rec if svc.optEmitEvents { // Handle input payload - if rve = svc.procUpdate(ctx, svc.store, invokerID, m, upd, old); !rve.IsValid() { + if rve = svc.procUpdate(ctx, invokerID, m, upd, old); !rve.IsValid() { return nil, RecordErrValueInput().Wrap(rve) } @@ -761,7 +747,7 @@ func (svc record) update(ctx context.Context, upd *types.Record) (rec *types.Rec } // Handle payload from automation scripts - if rve = svc.procUpdate(ctx, svc.store, invokerID, m, upd, old); !rve.IsValid() { + if rve = svc.procUpdate(ctx, invokerID, m, upd, old); !rve.IsValid() { return nil, RecordErrValueInput().Wrap(rve) } @@ -819,7 +805,7 @@ func (svc record) Create(ctx context.Context, new *types.Record) (rec *types.Rec // of the creation procedure and after results are back from the automation scripts // // Both these points introduce external data that need to be checked fully in the same manner -func (svc record) procCreate(ctx context.Context, s store.Storer, invokerID uint64, m *types.Module, new *types.Record) (rve *types.RecordValueErrorSet) { +func (svc record) procCreate(ctx context.Context, invokerID uint64, m *types.Module, new *types.Record) (rve *types.RecordValueErrorSet) { new.Values.SetUpdatedFlag(true) // Reset values to new record @@ -833,10 +819,11 @@ func (svc record) procCreate(ctx context.Context, s store.Storer, invokerID uint new.DeletedBy = 0 new = RecordUpdateOwner(invokerID, new, nil) - new.Values, rve = RecordValueMerger(ctx, svc.ac, m, new.Values, nil) - if !rve.IsValid() { - return rve + + if rve = RecordValueUpdateOpCheck(ctx, svc.ac, m, new.Values); !rve.IsValid() { + return } + rve = RecordPreparer(ctx, svc.store, svc.sanitizer, svc.validator, svc.formatter, m, new) return rve } @@ -862,7 +849,7 @@ func (svc record) Update(ctx context.Context, upd *types.Record) (rec *types.Rec // of the update procedure and after results are back from the automation scripts // // Both these points introduce external data that need to be checked fully in the same manner -func (svc record) procUpdate(ctx context.Context, s store.Storer, invokerID uint64, m *types.Module, upd *types.Record, old *types.Record) (rve *types.RecordValueErrorSet) { +func (svc record) procUpdate(ctx context.Context, invokerID uint64, m *types.Module, upd *types.Record, old *types.Record) (rve *types.RecordValueErrorSet) { // Mark all values as updated (new) upd.Values.SetUpdatedFlag(true) @@ -883,12 +870,16 @@ func (svc record) procUpdate(ctx context.Context, s store.Storer, invokerID uint upd.DeletedBy = old.DeletedBy upd = RecordUpdateOwner(invokerID, upd, old) - upd.Values, rve = RecordValueMerger(ctx, svc.ac, m, upd.Values, old.Values) - if !rve.IsValid() { + + upd.Values = old.Values.Merge(m.Fields, upd.Values, func(f *types.ModuleField) bool { + return svc.ac.CanUpdateRecordValue(ctx, m.Fields.FindByName(f.Name)) + }) + + if rve = RecordValueUpdateOpCheck(ctx, svc.ac, m, upd.Values); !rve.IsValid() { return rve } - rve = RecordPreparer(ctx, svc.store, svc.sanitizer, svc.validator, svc.formatter, m, upd) - return rve + + return RecordPreparer(ctx, svc.store, svc.sanitizer, svc.validator, svc.formatter, m, upd) } func (svc record) recordInfoUpdate(ctx context.Context, r *types.Record) { @@ -1306,7 +1297,7 @@ func (svc record) Iterator(ctx context.Context, f types.RecordFilter, fn eventbu rec.Values = RecordValueDefaults(m, rec.Values) // Handle payload from automation scripts - if rve := svc.procCreate(ctx, svc.store, invokerID, m, rec); !rve.IsValid() { + if rve := svc.procCreate(ctx, invokerID, m, rec); !rve.IsValid() { return RecordErrValueInput().Wrap(rve) } @@ -1317,7 +1308,7 @@ func (svc record) Iterator(ctx context.Context, f types.RecordFilter, fn eventbu recordableAction = RecordActionIteratorUpdate // Handle input payload - if rve := svc.procUpdate(ctx, svc.store, invokerID, m, rec, rec); !rve.IsValid() { + if rve := svc.procUpdate(ctx, invokerID, m, rec, rec); !rve.IsValid() { return RecordErrValueInput().Wrap(rve) } @@ -1353,8 +1344,17 @@ func (svc record) Iterator(ctx context.Context, f types.RecordFilter, fn eventbu } func ComposeRecordFilterChecker(ctx context.Context, ac recordAccessController, m *types.Module) func(*types.Record) (bool, error) { - return func(res *types.Record) (bool, error) { - if !ac.CanReadRecord(ctx, res) { + return func(rec *types.Record) (bool, error) { + // Setting module right before we do access control + // + // Why? + // - Access control can use one of the contextual roles + // - Contextual role can use expression that accesses values + // - Record's values are only exported into expression's scope when + // module is set on record at the time when Dict() fn is called. + rec.SetModule(m) + + if !ac.CanReadRecord(ctx, rec) { return false, nil } diff --git a/compose/service/record_test.go b/compose/service/record_test.go index e028a27d7..0191d3439 100644 --- a/compose/service/record_test.go +++ b/compose/service/record_test.go @@ -2,15 +2,20 @@ package service import ( "context" + "fmt" "testing" "github.com/cortezaproject/corteza-server/compose/service/values" "github.com/cortezaproject/corteza-server/compose/types" "github.com/cortezaproject/corteza-server/pkg/auth" + "github.com/cortezaproject/corteza-server/pkg/errors" + "github.com/cortezaproject/corteza-server/pkg/expr" + "github.com/cortezaproject/corteza-server/pkg/logger" "github.com/cortezaproject/corteza-server/pkg/rbac" "github.com/cortezaproject/corteza-server/store" "github.com/cortezaproject/corteza-server/store/sqlite3" sysTypes "github.com/cortezaproject/corteza-server/system/types" + "github.com/davecgh/go-spew/spew" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" @@ -92,10 +97,8 @@ func TestDefaultValueSetting(t *testing.T) { func TestProcUpdateOwnerPreservation(t *testing.T) { var ( - ctx = context.Background() - store store.Storer - - a = assert.New(t) + ctx = context.Background() + a = assert.New(t) svc = record{ sanitizer: values.Sanitizer(), @@ -117,18 +120,16 @@ func TestProcUpdateOwnerPreservation(t *testing.T) { } ) - svc.procUpdate(ctx, store, 10, mod, newRec, oldRec) + svc.procUpdate(ctx, 10, mod, newRec, oldRec) a.Equal(newRec.OwnedBy, uint64(1)) - svc.procUpdate(ctx, store, 10, mod, newRec, oldRec) + svc.procUpdate(ctx, 10, mod, newRec, oldRec) a.Equal(newRec.OwnedBy, uint64(1)) } func TestProcUpdateOwnerChanged(t *testing.T) { var ( - ctx = context.Background() - store store.Storer - - a = assert.New(t) + ctx = context.Background() + a = assert.New(t) svc = record{ sanitizer: values.Sanitizer(), @@ -150,9 +151,9 @@ func TestProcUpdateOwnerChanged(t *testing.T) { } ) - svc.procUpdate(ctx, store, 10, mod, newRec, oldRec) + a.True(svc.procUpdate(ctx, 10, mod, newRec, oldRec).IsValid()) a.Equal(newRec.OwnedBy, uint64(9)) - svc.procUpdate(ctx, store, 10, mod, newRec, oldRec) + a.True(svc.procUpdate(ctx, 10, mod, newRec, oldRec).IsValid()) a.Equal(newRec.OwnedBy, uint64(9)) } @@ -168,18 +169,22 @@ func TestRecord_boolFieldPermissionIssueKBR(t *testing.T) { req.NoError(err) req.NoError(store.Upgrade(ctx, zap.NewNop(), s)) + req.NoError(store.TruncateComposeNamespaces(ctx, s)) req.NoError(store.TruncateComposeModules(ctx, s)) req.NoError(store.TruncateComposeModuleFields(ctx, s)) req.NoError(store.TruncateComposeRecords(ctx, s, nil)) req.NoError(store.TruncateRbacRules(ctx, s)) var ( - rbacService = rbac.NewService(zap.NewNop(), s) - ac = &accessControl{rbac: rbacService} + rbacService = rbac.NewService( + //zap.NewNop(), + logger.MakeDebugLogger(), + nil, + ) + ac = &accessControl{rbac: rbacService} - svc = record{ + svc = &record{ sanitizer: values.Sanitizer(), - validator: values.Validator(), formatter: values.Formatter(), ac: ac, store: s, @@ -191,6 +196,8 @@ func TestRecord_boolFieldPermissionIssueKBR(t *testing.T) { stringField = &types.ModuleField{ID: nextID(), ModuleID: mod.ID, Name: "string", Kind: "String"} boolField = &types.ModuleField{ID: nextID(), ModuleID: mod.ID, Name: "bool", Kind: "Boolean"} + authRoleID uint64 = 1 + readerRole = &sysTypes.Role{Name: "reader", ID: nextID()} writerRole = &sysTypes.Role{Name: "writer", ID: nextID()} @@ -206,32 +213,46 @@ func TestRecord_boolFieldPermissionIssueKBR(t *testing.T) { } ) + svc.validator = defaultValidator(svc) + req.NoError(store.CreateComposeNamespace(ctx, s, ns)) req.NoError(store.CreateComposeModule(ctx, s, mod)) req.NoError(store.CreateComposeModuleField(ctx, s, stringField, boolField)) - rbacService.Grant(ctx, - rbac.AllowRule(readerRole.ID, mod.RbacResource(), "record.read"), - rbac.AllowRule(readerRole.ID, mod.RbacResource(), "record.create"), - rbac.AllowRule(readerRole.ID, mod.RbacResource(), "record.update"), - rbac.AllowRule(readerRole.ID, stringField.RbacResource(), "record.value.read"), - rbac.DenyRule(readerRole.ID, boolField.RbacResource(), "record.value.update"), + rbacService.UpdateRoles( + rbac.CommonRole.Make(readerRole.ID, readerRole.Name), + rbac.CommonRole.Make(writerRole.ID, writerRole.Name), + rbac.AuthenticatedRole.Make(authRoleID, "authenticated"), + ) - rbac.AllowRule(writerRole.ID, mod.RbacResource(), "record.read"), - rbac.AllowRule(writerRole.ID, mod.RbacResource(), "record.create"), - rbac.AllowRule(writerRole.ID, mod.RbacResource(), "record.update"), + rbacService.Grant(ctx, + // base permissions + rbac.AllowRule(authRoleID, mod.RbacResource(), "record.create"), + rbac.AllowRule(authRoleID, types.RecordRbacResource(0, 0, 0), "read"), + rbac.AllowRule(authRoleID, types.RecordRbacResource(0, 0, 0), "update"), + rbac.DenyRule(authRoleID, types.ModuleFieldRbacResource(0, 0, 0), "record.value.read"), + rbac.DenyRule(authRoleID, types.ModuleFieldRbacResource(0, 0, 0), "record.value.update"), + + // special perm for writer rbac.AllowRule(writerRole.ID, stringField.RbacResource(), "record.value.read"), rbac.AllowRule(writerRole.ID, stringField.RbacResource(), "record.value.update"), + rbac.AllowRule(writerRole.ID, boolField.RbacResource(), "record.value.read"), + rbac.AllowRule(writerRole.ID, boolField.RbacResource(), "record.value.update"), + + // special perm for reader + rbac.AllowRule(readerRole.ID, stringField.RbacResource(), "record.value.read"), + rbac.AllowRule(readerRole.ID, boolField.RbacResource(), "record.value.read"), + rbac.DenyRule(readerRole.ID, boolField.RbacResource(), "record.value.update"), ) { // security context w/ writer role - ctx = auth.SetIdentityToContext(ctx, auth.NewIdentity(userID, writerRole.ID)) + ctx = auth.SetIdentityToContext(ctx, auth.Authenticated(userID, writerRole.ID, authRoleID)) recChecked, err = svc.Create(ctx, &types.Record{ModuleID: mod.ID, NamespaceID: ns.ID, Values: valChecked}) - req.NoError(err) + req.NoError(errors.Unwrap(err)) - req.NotNil(recChecked.Values.Get("bool", 0)) + req.NotNil(recChecked.Values.Get("bool", 0), "should be checked") req.Equal("1", recChecked.Values.Get("bool", 0).Value) recUnchecked, err = svc.Create(ctx, &types.Record{ModuleID: mod.ID, NamespaceID: ns.ID, Values: valUnchecked}) @@ -241,17 +262,18 @@ func TestRecord_boolFieldPermissionIssueKBR(t *testing.T) { // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** - // security context w/ writer role - ctx = auth.SetIdentityToContext(ctx, auth.NewIdentity(userID, readerRole.ID)) + // security context w/ reader role + ctx = auth.SetIdentityToContext(ctx, auth.Authenticated(userID, readerRole.ID, authRoleID)) recChecked.Values = types.RecordValueSet{ &types.RecordValue{Name: "string", Value: "abc"}, } + spew.Dump(recChecked.GetModule()) recChecked, err = svc.Update(ctx, recChecked) req.NoError(err) - req.NotNil(recChecked.Values.Get("bool", 0)) + req.NotNil(recChecked.Values.Get("bool", 0), "should still be checked") req.Equal("1", recChecked.Values.Get("bool", 0).Value) recUnchecked.Values = types.RecordValueSet{ @@ -260,7 +282,439 @@ func TestRecord_boolFieldPermissionIssueKBR(t *testing.T) { recUnchecked, err = svc.Update(ctx, recUnchecked) req.NoError(err) - req.Nil(recUnchecked.Values.Get("bool", 0)) + req.Nil(recUnchecked.Values.Get("bool", 0), "should not be checked anymore") + + // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** + + // security context w/ writer role + ctx = auth.SetIdentityToContext(ctx, auth.Authenticated(userID, writerRole.ID, authRoleID)) + + recChecked.Values = types.RecordValueSet{ + &types.RecordValue{Name: "string", Value: "abc"}, + &types.RecordValue{Name: "bool", Value: "1"}, + } + + recChecked, err = svc.Update(ctx, recChecked) + req.NoError(err) + + req.NotNil(recChecked.Values.Get("bool", 0), "should checked again") + req.Equal("1", recChecked.Values.Get("bool", 0).Value, "should checked again") + } } + +func TestRecord_refAccessControl(t *testing.T) { + var ( + req = require.New(t) + + // uncomment to enable sql conn debugging + //ctx = logger.ContextWithValue(context.Background(), logger.MakeDebugLogger()) + ctx = context.Background() + s, err = sqlite3.ConnectInMemoryWithDebug(ctx) + ) + + req.NoError(err) + req.NoError(store.Upgrade(ctx, zap.NewNop(), s)) + req.NoError(store.TruncateComposeNamespaces(ctx, s)) + req.NoError(store.TruncateComposeModules(ctx, s)) + req.NoError(store.TruncateComposeModuleFields(ctx, s)) + req.NoError(store.TruncateComposeRecords(ctx, s, nil)) + req.NoError(store.TruncateRbacRules(ctx, s)) + + var ( + rbacService = rbac.NewService( + //zap.NewNop(), + logger.MakeDebugLogger(), + nil, + ) + ac = &accessControl{rbac: rbacService} + + svc = &record{ + sanitizer: values.Sanitizer(), + formatter: values.Formatter(), + ac: ac, + store: s, + } + + nextIDi uint64 = 1 + nextID = func() uint64 { + nextIDi++ + return nextIDi + } + + userID = nextID() + ns = &types.Namespace{ID: nextID()} + mod1 = &types.Module{ID: nextID(), NamespaceID: ns.ID, Name: "mod one"} + mod2 = &types.Module{ID: nextID(), NamespaceID: ns.ID, Name: "mod two"} + mod1strField = &types.ModuleField{ID: nextID(), NamespaceID: ns.ID, ModuleID: mod1.ID, Name: "str", Kind: "String"} + mod2refField = &types.ModuleField{ID: nextID(), NamespaceID: ns.ID, ModuleID: mod2.ID, Name: "ref", Kind: "Record"} + + testerRole = &sysTypes.Role{Name: "tester", ID: nextID()} + + mod1rec1 = &types.Record{ + NamespaceID: ns.ID, + ModuleID: mod1.ID, + Values: types.RecordValueSet{ + &types.RecordValue{Name: "str", Value: "abc"}, + }, + } + + mod2rec1 = &types.Record{NamespaceID: ns.ID, ModuleID: mod2.ID} + ) + + svc.validator = defaultValidator(svc) + + t.Log("create test namespace") + req.NoError(store.CreateComposeNamespace(ctx, s, ns)) + + t.Log("create test modules") + req.NoError(store.CreateComposeModule(ctx, s, mod1, mod2)) + + t.Log("create test module fields") + req.NoError(store.CreateComposeModuleField(ctx, s, mod1strField, mod2refField)) + + t.Log("inform rbac service about new roles") + rbacService.UpdateRoles( + rbac.CommonRole.Make(testerRole.ID, testerRole.Name), + ) + + t.Log("log-in with test user ") + ctx = auth.SetIdentityToContext(ctx, auth.Authenticated(userID, testerRole.ID)) + + _ = mod2rec1 + + { + t.Log("creating record on 1st module; should failed because we do not have permissions to create records") + _, err = svc.Create(ctx, mod1rec1) + req.EqualError(err, "not allowed to create records") + + t.Logf("granting permissions to create records on this module") + rbacService.Grant(ctx, rbac.AllowRule(testerRole.ID, mod1.RbacResource(), "record.create")) + + t.Log("retry creating record on 1st module; should fail because we do not have permissions to update field") + _, err = svc.Create(ctx, mod1rec1) + req.Error(err) + req.True(types.IsRecordValueErrorSet(err).HasKind("updateDenied")) + + t.Logf("granting permissions to update records values on module field") + rbacService.Grant(ctx, rbac.AllowRule(testerRole.ID, mod1strField.RbacResource(), "record.value.update")) + + t.Log("retry creating record on 1st module; should succeed") + mod1rec1, err = svc.Create(ctx, mod1rec1) + req.NoError(err) + } + { + t.Log("can record be read") + _, err = svc.FindByID(ctx, mod1rec1.NamespaceID, mod1rec1.ModuleID, mod1rec1.ID) + req.EqualError(err, "not allowed to read this record") + } + { + t.Log("link 2nd record to 1st one") + mod2rec1.Values = mod2rec1.Values.Set(&types.RecordValue{Name: "ref", Value: fmt.Sprintf("%d", mod1rec1.ID)}) + + t.Log("create record on 2nd module with ref to record on the 1st module; must fail, no create perm") + _, err = svc.Create(ctx, mod2rec1) + req.EqualError(err, "not allowed to create records") + + t.Log("grant record.create on namespace level") + rbacService.Grant(ctx, rbac.AllowRule(testerRole.ID, types.ModuleRbacResource(ns.ID, 0), "record.create")) + + t.Log("grant record.value.update on namespace level") + rbacService.Grant(ctx, rbac.AllowRule(testerRole.ID, types.ModuleFieldRbacResource(ns.ID, 0, 0), "record.value.update")) + + t.Log("create record on 2nd module with ref to record on the 1st module; most fail, not allowed to read (referenced) mod1rec1") + _, err = svc.Create(ctx, mod2rec1) + req.EqualError(err, "invalid record value input") + + t.Log("grant read on record") + rbacService.Grant(ctx, rbac.AllowRule(testerRole.ID, mod1rec1.RbacResource(), "read")) + + t.Log("create record on 2nd module with ref to record on the 1st module") + mod2rec1, err = svc.Create(ctx, mod2rec1) + req.NoError(errors.Unwrap(err)) + } + { + t.Log("update record on 2nd module with unchanged values; must fail, no update permissions") + _, err = svc.Update(ctx, mod2rec1) + req.EqualError(err, "not allowed to update this record") + + t.Log("grant update on namespace level") + rbacService.Grant(ctx, rbac.AllowRule(testerRole.ID, types.RecordRbacResource(ns.ID, 0, 0), "update")) + + t.Log("update record on 2nd module with unchanged values") + mod2rec1, err = svc.Update(ctx, mod2rec1) + req.NoError(errors.Unwrap(err)) + + t.Log("update record on 2nd module with unchanged values; unset record value") + mod2rec1.Values = nil + mod2rec1, err = svc.Update(ctx, mod2rec1) + req.NoError(errors.Unwrap(err)) + + t.Log("link 2nd record to 1st one again") + mod2rec1.Values = mod2rec1.Values.Set(&types.RecordValue{Name: "ref", Value: fmt.Sprintf("%d", mod1rec1.ID)}) + mod2rec1, err = svc.Update(ctx, mod2rec1) + req.NoError(errors.Unwrap(err)) + } + { + t.Log("revoke read on record") + rbacService.Grant(ctx, rbac.DenyRule(testerRole.ID, mod1rec1.RbacResource(), "read")) + + t.Log("link 2nd record to 1st one again but w/o permissions; must work, value did not change") + mod2rec1.Values = mod2rec1.Values.Set(&types.RecordValue{Name: "ref", Value: fmt.Sprintf("%d", mod1rec1.ID)}) + mod2rec1, err = svc.Update(ctx, mod2rec1) + req.NoError(errors.Unwrap(err)) + } +} + +func TestRecord_searchAccessControl(t *testing.T) { + var ( + req = require.New(t) + + // uncomment to enable sql conn debugging + //ctx = logger.ContextWithValue(context.Background(), logger.MakeDebugLogger()) + ctx = context.Background() + s, err = sqlite3.ConnectInMemoryWithDebug(ctx) + ) + + req.NoError(err) + req.NoError(store.Upgrade(ctx, zap.NewNop(), s)) + req.NoError(store.TruncateComposeNamespaces(ctx, s)) + req.NoError(store.TruncateComposeModules(ctx, s)) + req.NoError(store.TruncateComposeModuleFields(ctx, s)) + req.NoError(store.TruncateComposeRecords(ctx, s, nil)) + req.NoError(store.TruncateRbacRules(ctx, s)) + + var ( + rbacService = rbac.NewService( + //zap.NewNop(), + logger.MakeDebugLogger(), + nil, + ) + ac = &accessControl{rbac: rbacService} + + svc = &record{ + ac: ac, + store: s, + sanitizer: values.Sanitizer(), + } + + nextIDi uint64 = 1 + nextID = func() uint64 { + nextIDi++ + return nextIDi + } + + userID = nextID() + ns = &types.Namespace{ID: nextID()} + mod = &types.Module{ID: nextID(), NamespaceID: ns.ID, Name: "mod one"} + //strField = &types.ModuleField{ID: nextID(), NamespaceID: ns.ID, ModuleID: mod.ID, Name: "str", Kind: "String"} + + testerRole = &sysTypes.Role{Name: "tester", ID: nextID()} + + rr = make([]*types.Record, 10) + hits []*types.Record + + f = types.RecordFilter{ + ModuleID: mod.ID, + NamespaceID: ns.ID, + } + ) + + t.Log("create test namespace") + req.NoError(store.CreateComposeNamespace(ctx, s, ns)) + + t.Log("create test modules") + req.NoError(store.CreateComposeModule(ctx, s, mod)) + + //t.Log("create test module fields") + //req.NoError(store.CreateComposeModuleField(ctx, s, strField)) + + t.Log("create test records") + for i := 0; i < cap(rr); i++ { + rr[i] = &types.Record{ + ID: nextID(), + NamespaceID: ns.ID, + ModuleID: mod.ID, + } + + req.NoError(store.CreateComposeRecord(ctx, s, mod, rr[i])) + } + + t.Log("inform rbac service about new roles") + rbacService.UpdateRoles( + rbac.CommonRole.Make(testerRole.ID, testerRole.Name), + ) + + t.Log("log-in with test user ") + ctx = auth.SetIdentityToContext(ctx, auth.Authenticated(userID, testerRole.ID)) + + t.Log("search for the newly created records; should not find any (all denied)") + f.IncTotal = true + hits, f, err = svc.Find(ctx, f) + req.Len(hits, 0) + req.Equal(uint(0), f.Total) + + t.Log("allow read access for two records") + rbacService.Grant(ctx, rbac.AllowRule(testerRole.ID, rr[3].RbacResource(), "read")) + rbacService.Grant(ctx, rbac.AllowRule(testerRole.ID, rr[6].RbacResource(), "read")) + + t.Log("search for the newly created records; should find 2 we're allowed to read") + f.IncTotal = true + hits, f, err = svc.Find(ctx, f) + req.Len(hits, 2) + req.Equal(uint(2), f.Total) +} + +func TestRecord_contextualRolesAccessControl(t *testing.T) { + var ( + req = require.New(t) + + // uncomment to enable sql conn debugging + ctx = logger.ContextWithValue(context.Background(), logger.MakeDebugLogger()) + //ctx = context.Background() + s, err = sqlite3.ConnectInMemoryWithDebug(ctx) + ) + + req.NoError(err) + req.NoError(store.Upgrade(ctx, zap.NewNop(), s)) + req.NoError(store.TruncateComposeNamespaces(ctx, s)) + req.NoError(store.TruncateComposeModules(ctx, s)) + req.NoError(store.TruncateComposeModuleFields(ctx, s)) + req.NoError(store.TruncateComposeRecords(ctx, s, nil)) + req.NoError(store.TruncateRbacRules(ctx, s)) + + var ( + rbacService = rbac.NewService( + //zap.NewNop(), + logger.MakeDebugLogger(), + nil, + ) + ac = &accessControl{rbac: rbacService} + + svc = &record{ + ac: ac, + store: s, + sanitizer: values.Sanitizer(), + } + + nextIDi uint64 = 1 + nextID = func() uint64 { + nextIDi++ + return nextIDi + } + + userID = nextID() + ns = &types.Namespace{ID: nextID()} + mod = &types.Module{ID: nextID(), NamespaceID: ns.ID, Name: "mod one"} + numField = &types.ModuleField{ID: nextID(), NamespaceID: ns.ID, ModuleID: mod.ID, Name: "num", Kind: "String"} + boolField = &types.ModuleField{ID: nextID(), NamespaceID: ns.ID, ModuleID: mod.ID, Name: "yes", Kind: "String"} + + ownerRole = &sysTypes.Role{Name: "owner", ID: nextID()} + truthyRole = &sysTypes.Role{Name: "whenBoolTrue", ID: nextID()} + tttRole = &sysTypes.Role{Name: "whenNum333", ID: nextID()} + + rr = make([]*types.Record, 10) + hits []*types.Record + + f = types.RecordFilter{ + ModuleID: mod.ID, + NamespaceID: ns.ID, + } + + // setting up rbac context role expression parser + roleCheckFnMaker = func(expression string) func(scope map[string]interface{}) bool { + p := expr.NewParser() + + return func(scope map[string]interface{}) bool { + spew.Dump(expression, scope) + v, _ := expr.NewVars(scope) + if e, err := p.Parse(expression); err != nil { + t.Logf("could not parse expression: %v", err) + } else if c, err := e.Test(ctx, v); err != nil { + t.Logf("could not exec expression: %v", err) + } else { + return c + } + + return false + } + + } + ) + + t.Log("create test namespace") + req.NoError(store.CreateComposeNamespace(ctx, s, ns)) + + mod.Fields = types.ModuleFieldSet{numField, boolField} + numField.ModuleID = mod.ID + boolField.ModuleID = mod.ID + + t.Log("create test modules") + req.NoError(store.CreateComposeModule(ctx, s, mod)) + req.NoError(store.CreateComposeModuleField(ctx, s, numField, boolField)) + + t.Log("create test records") + for i := 0; i < cap(rr); i++ { + rr[i] = &types.Record{ + ID: nextID(), + NamespaceID: ns.ID, + ModuleID: mod.ID, + } + + if i%2 == 0 { + // let's own half of the records + rr[i].OwnedBy = userID + } + + if i%3 == 0 { + // set 333 to num on every 3rd record + rr[i].Values = rr[i].Values.Set(&types.RecordValue{Name: "num", Value: "333"}) + } + + if i >= 5 { + // and set true to bool field on the last 5 records + rr[i].Values = rr[i].Values.Set(&types.RecordValue{Name: "yes", Value: "1"}) + } + + req.NoError(store.CreateComposeRecord(ctx, s, mod, rr[i])) + } + + // result + // i 0 1 2 3 4 5 6 7 8 9 + // -------------------------- + // owner x x x x x + // bool x x x x x + // 333 x x x x + // -------------------------- + // read: x x x x x x x x x (all but one) + + t.Log("inform rbac service about new roles") + rbacService.UpdateRoles( + rbac.MakeContextRole(ownerRole.ID, ownerRole.Name, roleCheckFnMaker("resource.ownedBy == userID"), types.RecordResourceType), + rbac.MakeContextRole(truthyRole.ID, truthyRole.Name, roleCheckFnMaker(`has(resource.values, "yes") ? resource.values.yes : false`), types.RecordResourceType), + rbac.MakeContextRole(tttRole.ID, tttRole.Name, roleCheckFnMaker(`has(resource.values, "num") ? resource.values.num == 333 : false`), types.RecordResourceType), + ) + + t.Log("log-in with test user") + ctx = auth.SetIdentityToContext(ctx, auth.Authenticated(userID, ownerRole.ID, truthyRole.ID, tttRole.ID)) + + t.Log("expecting not find any (all denied)") + hits, _, err = svc.Find(ctx, f) + req.Len(hits, 0) + + t.Log("expecting to find 5 records (owned by us)") + rbacService.Grant(ctx, rbac.AllowRule(ownerRole.ID, types.RecordRbacResource(0, 0, 0), "read")) + hits, _, err = svc.Find(ctx, f) + req.Len(hits, 5) + + t.Log("expecting to find 2 records (owned by us and with true value for 'yes' field)") + rbacService.Grant(ctx, rbac.AllowRule(truthyRole.ID, types.RecordRbacResource(0, 0, 0), "read")) + hits, _, err = svc.Find(ctx, f) + req.Len(hits, 8) + + t.Log("expecting to find 2 records (owned by us and with true value for 'yes' field + 333 for num)") + rbacService.Grant(ctx, rbac.AllowRule(tttRole.ID, types.RecordRbacResource(0, 0, 0), "read")) + hits, _, err = svc.Find(ctx, f) + req.Len(hits, 9) +} diff --git a/compose/service/values/validator.go b/compose/service/values/validator.go index e56432307..d8efc6469 100644 --- a/compose/service/values/validator.go +++ b/compose/service/values/validator.go @@ -94,7 +94,7 @@ func (vldtr *validator) FileRefChecker(fn ReferenceChecker) { // - check if required values are present // - check for unique-multi-value in multi value fields // - field-kind specific validation on all values -// - unique check on all all values +// - unique check on all values func (vldtr validator) Run(ctx context.Context, s store.Storer, m *types.Module, r *types.Record) (out *types.RecordValueErrorSet) { var ( f *types.ModuleField diff --git a/compose/types/rbac.gen.go b/compose/types/rbac.gen.go index 2a0e6562e..6832ddc0b 100644 --- a/compose/types/rbac.gen.go +++ b/compose/types/rbac.gen.go @@ -16,6 +16,7 @@ package types // - compose.yaml import ( + "fmt" "strconv" ) @@ -27,18 +28,18 @@ type ( ) const ( - ChartRbacResourceSchema = "corteza+compose.chart" - ModuleFieldRbacResourceSchema = "corteza+compose.module-field" - ModuleRbacResourceSchema = "corteza+compose.module" - NamespaceRbacResourceSchema = "corteza+compose.namespace" - PageRbacResourceSchema = "corteza+compose.page" - RecordRbacResourceSchema = "corteza+compose.record" - ComponentRbacResourceSchema = "corteza+compose" + ChartResourceType = "corteza::compose:chart" + ModuleFieldResourceType = "corteza::compose:module-field" + ModuleResourceType = "corteza::compose:module" + NamespaceResourceType = "corteza::compose:namespace" + PageResourceType = "corteza::compose:page" + RecordResourceType = "corteza::compose:record" + ComponentResourceType = "corteza::compose" ) // RbacResource returns string representation of RBAC resource for Chart by calling ChartRbacResource fn // -// RBAC resource is in the corteza+compose.chart:/... format +// RBAC resource is in the corteza::compose:chart/... format // // This function is auto-generated func (r Chart) RbacResource() string { @@ -47,31 +48,35 @@ func (r Chart) RbacResource() string { // ChartRbacResource returns string representation of RBAC resource for Chart // -// RBAC resource is in the corteza+compose.chart:/... format +// RBAC resource is in the corteza::compose:chart/... format // // This function is auto-generated -func ChartRbacResource(namespaceID uint64, iD uint64) string { - out := ChartRbacResourceSchema + ":" - out += "/" - +func ChartRbacResource(namespaceID uint64, id uint64) string { + cpts := []interface{}{ChartResourceType} if namespaceID != 0 { - out += strconv.FormatUint(namespaceID, 10) + cpts = append(cpts, strconv.FormatUint(namespaceID, 10)) } else { - out += "*" + cpts = append(cpts, "*") } - out += "/" - if iD != 0 { - out += strconv.FormatUint(iD, 10) + if id != 0 { + cpts = append(cpts, strconv.FormatUint(id, 10)) } else { - out += "*" + cpts = append(cpts, "*") } - return out + + return fmt.Sprintf(ChartRbacResourceTpl(), cpts...) + +} + +// @todo template +func ChartRbacResourceTpl() string { + return "%s/%s/%s" } // RbacResource returns string representation of RBAC resource for ModuleField by calling ModuleFieldRbacResource fn // -// RBAC resource is in the corteza+compose.module-field:/... format +// RBAC resource is in the corteza::compose:module-field/... format // // This function is auto-generated func (r ModuleField) RbacResource() string { @@ -80,38 +85,41 @@ func (r ModuleField) RbacResource() string { // ModuleFieldRbacResource returns string representation of RBAC resource for ModuleField // -// RBAC resource is in the corteza+compose.module-field:/... format +// RBAC resource is in the corteza::compose:module-field/... format // // This function is auto-generated -func ModuleFieldRbacResource(namespaceID uint64, moduleID uint64, iD uint64) string { - out := ModuleFieldRbacResourceSchema + ":" - out += "/" - +func ModuleFieldRbacResource(namespaceID uint64, moduleID uint64, id uint64) string { + cpts := []interface{}{ModuleFieldResourceType} if namespaceID != 0 { - out += strconv.FormatUint(namespaceID, 10) + cpts = append(cpts, strconv.FormatUint(namespaceID, 10)) } else { - out += "*" + cpts = append(cpts, "*") } - out += "/" if moduleID != 0 { - out += strconv.FormatUint(moduleID, 10) + cpts = append(cpts, strconv.FormatUint(moduleID, 10)) } else { - out += "*" + cpts = append(cpts, "*") } - out += "/" - if iD != 0 { - out += strconv.FormatUint(iD, 10) + if id != 0 { + cpts = append(cpts, strconv.FormatUint(id, 10)) } else { - out += "*" + cpts = append(cpts, "*") } - return out + + return fmt.Sprintf(ModuleFieldRbacResourceTpl(), cpts...) + +} + +// @todo template +func ModuleFieldRbacResourceTpl() string { + return "%s/%s/%s/%s" } // RbacResource returns string representation of RBAC resource for Module by calling ModuleRbacResource fn // -// RBAC resource is in the corteza+compose.module:/... format +// RBAC resource is in the corteza::compose:module/... format // // This function is auto-generated func (r Module) RbacResource() string { @@ -120,31 +128,35 @@ func (r Module) RbacResource() string { // ModuleRbacResource returns string representation of RBAC resource for Module // -// RBAC resource is in the corteza+compose.module:/... format +// RBAC resource is in the corteza::compose:module/... format // // This function is auto-generated -func ModuleRbacResource(namespaceID uint64, iD uint64) string { - out := ModuleRbacResourceSchema + ":" - out += "/" - +func ModuleRbacResource(namespaceID uint64, id uint64) string { + cpts := []interface{}{ModuleResourceType} if namespaceID != 0 { - out += strconv.FormatUint(namespaceID, 10) + cpts = append(cpts, strconv.FormatUint(namespaceID, 10)) } else { - out += "*" + cpts = append(cpts, "*") } - out += "/" - if iD != 0 { - out += strconv.FormatUint(iD, 10) + if id != 0 { + cpts = append(cpts, strconv.FormatUint(id, 10)) } else { - out += "*" + cpts = append(cpts, "*") } - return out + + return fmt.Sprintf(ModuleRbacResourceTpl(), cpts...) + +} + +// @todo template +func ModuleRbacResourceTpl() string { + return "%s/%s/%s" } // RbacResource returns string representation of RBAC resource for Namespace by calling NamespaceRbacResource fn // -// RBAC resource is in the corteza+compose.namespace:/... format +// RBAC resource is in the corteza::compose:namespace/... format // // This function is auto-generated func (r Namespace) RbacResource() string { @@ -153,24 +165,29 @@ func (r Namespace) RbacResource() string { // NamespaceRbacResource returns string representation of RBAC resource for Namespace // -// RBAC resource is in the corteza+compose.namespace:/... format +// RBAC resource is in the corteza::compose:namespace/... format // // This function is auto-generated -func NamespaceRbacResource(iD uint64) string { - out := NamespaceRbacResourceSchema + ":" - out += "/" - - if iD != 0 { - out += strconv.FormatUint(iD, 10) +func NamespaceRbacResource(id uint64) string { + cpts := []interface{}{NamespaceResourceType} + if id != 0 { + cpts = append(cpts, strconv.FormatUint(id, 10)) } else { - out += "*" + cpts = append(cpts, "*") } - return out + + return fmt.Sprintf(NamespaceRbacResourceTpl(), cpts...) + +} + +// @todo template +func NamespaceRbacResourceTpl() string { + return "%s/%s" } // RbacResource returns string representation of RBAC resource for Page by calling PageRbacResource fn // -// RBAC resource is in the corteza+compose.page:/... format +// RBAC resource is in the corteza::compose:page/... format // // This function is auto-generated func (r Page) RbacResource() string { @@ -179,31 +196,35 @@ func (r Page) RbacResource() string { // PageRbacResource returns string representation of RBAC resource for Page // -// RBAC resource is in the corteza+compose.page:/... format +// RBAC resource is in the corteza::compose:page/... format // // This function is auto-generated -func PageRbacResource(namespaceID uint64, iD uint64) string { - out := PageRbacResourceSchema + ":" - out += "/" - +func PageRbacResource(namespaceID uint64, id uint64) string { + cpts := []interface{}{PageResourceType} if namespaceID != 0 { - out += strconv.FormatUint(namespaceID, 10) + cpts = append(cpts, strconv.FormatUint(namespaceID, 10)) } else { - out += "*" + cpts = append(cpts, "*") } - out += "/" - if iD != 0 { - out += strconv.FormatUint(iD, 10) + if id != 0 { + cpts = append(cpts, strconv.FormatUint(id, 10)) } else { - out += "*" + cpts = append(cpts, "*") } - return out + + return fmt.Sprintf(PageRbacResourceTpl(), cpts...) + +} + +// @todo template +func PageRbacResourceTpl() string { + return "%s/%s/%s" } // RbacResource returns string representation of RBAC resource for Record by calling RecordRbacResource fn // -// RBAC resource is in the corteza+compose.record:/... format +// RBAC resource is in the corteza::compose:record/... format // // This function is auto-generated func (r Record) RbacResource() string { @@ -212,38 +233,41 @@ func (r Record) RbacResource() string { // RecordRbacResource returns string representation of RBAC resource for Record // -// RBAC resource is in the corteza+compose.record:/... format +// RBAC resource is in the corteza::compose:record/... format // // This function is auto-generated -func RecordRbacResource(namespaceID uint64, moduleID uint64, iD uint64) string { - out := RecordRbacResourceSchema + ":" - out += "/" - +func RecordRbacResource(namespaceID uint64, moduleID uint64, id uint64) string { + cpts := []interface{}{RecordResourceType} if namespaceID != 0 { - out += strconv.FormatUint(namespaceID, 10) + cpts = append(cpts, strconv.FormatUint(namespaceID, 10)) } else { - out += "*" + cpts = append(cpts, "*") } - out += "/" if moduleID != 0 { - out += strconv.FormatUint(moduleID, 10) + cpts = append(cpts, strconv.FormatUint(moduleID, 10)) } else { - out += "*" + cpts = append(cpts, "*") } - out += "/" - if iD != 0 { - out += strconv.FormatUint(iD, 10) + if id != 0 { + cpts = append(cpts, strconv.FormatUint(id, 10)) } else { - out += "*" + cpts = append(cpts, "*") } - return out + + return fmt.Sprintf(RecordRbacResourceTpl(), cpts...) + +} + +// @todo template +func RecordRbacResourceTpl() string { + return "%s/%s/%s/%s" } // RbacResource returns string representation of RBAC resource for Component by calling ComponentRbacResource fn // -// RBAC resource is in the corteza+compose:/... format +// RBAC resource is in the corteza::compose/... format // // This function is auto-generated func (r Component) RbacResource() string { @@ -252,10 +276,15 @@ func (r Component) RbacResource() string { // ComponentRbacResource returns string representation of RBAC resource for Component // -// RBAC resource is in the corteza+compose:/... format +// RBAC resource is in the corteza::compose/ format // // This function is auto-generated func ComponentRbacResource() string { - out := ComponentRbacResourceSchema + ":" - return out + return ComponentResourceType + "/" + +} + +// @todo template +func ComponentRbacResourceTpl() string { + return "%s" } diff --git a/compose/types/record_value.go b/compose/types/record_value.go index 826f50f17..35b56aece 100644 --- a/compose/types/record_value.go +++ b/compose/types/record_value.go @@ -4,11 +4,12 @@ import ( "database/sql/driver" "encoding/json" "fmt" + "strconv" + "time" + "github.com/cortezaproject/corteza-server/pkg/filter" "github.com/pkg/errors" "github.com/spf13/cast" - "strconv" - "time" ) type ( @@ -236,11 +237,30 @@ func (set RecordValueSet) GetClean() (out RecordValueSet) { } // Merge merges old value set with new one and expects unchanged values to be in the new set +func (set RecordValueSet) Merge(mfs ModuleFieldSet, new RecordValueSet, canAccessField func(f *ModuleField) bool) (out RecordValueSet) { + new, _ = new.Filter(func(v *RecordValue) (bool, error) { + return mfs.HasName(v.Name), nil + }) + + // Value merge process does not know anything about permissions so + // in case when new values are missing but do exist in the old set and their update/read is denied + // we need to copy them to ensure value merge process them correctly + for _, f := range mfs { + if len(new.FilterByName(f.Name)) == 0 && !canAccessField(f) { + // copy all fields from old to new + new = append(new, set.FilterByName(f.Name).GetClean()...) + } + } + + return set.merge(new) +} + +// Raw merge of old and one and new values, skipping unchanged // // This satisfies current requirements where record values are always // manipulated as a whole (not partial) // -func (set RecordValueSet) Merge(new RecordValueSet) (out RecordValueSet) { +func (set RecordValueSet) merge(new RecordValueSet) (out RecordValueSet) { if len(set) == 0 { // Empty set, copy all new values and return them for i := range new { diff --git a/compose/types/record_value_test.go b/compose/types/record_value_test.go index 1bd4345a0..5c535f58f 100644 --- a/compose/types/record_value_test.go +++ b/compose/types/record_value_test.go @@ -36,13 +36,52 @@ func TestRecordValueSet_Set(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := tt.set.Set(&tt.new); !reflect.DeepEqual(got, tt.want) { - t.Errorf("Set() = %v, want %v", got, tt.want) + t.Errorf("got:\n%+v\n\nwant\n%+v", got, tt.want) } }) } } func TestRecordValueSet_Merge(t *testing.T) { + tests := []struct { + name string + set RecordValueSet + new RecordValueSet + mfs ModuleFieldSet + fn func(f *ModuleField) bool + want RecordValueSet + }{ + { + name: "update with partial access", + set: RecordValueSet{{Name: "n", Value: "1"}, {Name: "accessible", Value: "skip me"}, {Name: "inaccessible", Value: "don't skip me"}}, + new: RecordValueSet{{Name: "n", Value: "2"}}, + mfs: ModuleFieldSet{ + &ModuleField{Name: "n"}, + &ModuleField{Name: "accessible"}, + &ModuleField{Name: "inaccessible"}, + }, + fn: func(f *ModuleField) bool { + // is field accessible? + return f.Name == "accessible" + }, + want: RecordValueSet{ + {Name: "n", Value: "2", OldValue: "1", Updated: true}, + {Name: "accessible", Value: "skip me", OldValue: "skip me", Updated: true, DeletedAt: &time.Time{}}, + {Name: "inaccessible", Value: "don't skip me", OldValue: "don't skip me"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.set.Merge(tt.mfs, tt.new, tt.fn); !reflect.DeepEqual(got, tt.want) { + t.Errorf("got:\n%+v\n\nwant\n%+v", got, tt.want) + } + }) + } +} + +func TestRecordValueSet_merge(t *testing.T) { tests := []struct { name string set RecordValueSet @@ -94,7 +133,7 @@ func TestRecordValueSet_Merge(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := tt.set.Merge(tt.new); !reflect.DeepEqual(got, tt.want) { + if got := tt.set.merge(tt.new); !reflect.DeepEqual(got, tt.want) { t.Errorf("got:\n%+v\n\nwant\n%+v", got, tt.want) } }) diff --git a/compose/types/validated.go b/compose/types/validated.go index 79337e689..3855404d8 100644 --- a/compose/types/validated.go +++ b/compose/types/validated.go @@ -35,7 +35,7 @@ func (v *RecordValueErrorSet) Error() string { no = len(v.Set) } - return fmt.Sprintf("%d issue(s) found: %+v", no, v.Set) + return fmt.Sprintf("%d issue(s) found", no) } func (v RecordValueErrorSet) MarshalJSON() ([]byte, error) { @@ -48,6 +48,20 @@ func (v RecordValueErrorSet) MarshalJSON() ([]byte, error) { }) } +func (v *RecordValueErrorSet) HasKind(kind string) bool { + if v == nil || v.IsValid() { + return false + } + + for _, e := range v.Set { + if e.Kind == kind { + return true + } + } + + return false +} + // IsRecordValueErrorSet tests if given error is RecordValueErrorSet (or it wraps it) and it has errors // If not is not (or !IsValid), it return nil! func IsRecordValueErrorSet(err error) *RecordValueErrorSet { diff --git a/def/automation.yaml b/def/automation.yaml index 8de683daa..0baa3b626 100644 --- a/def/automation.yaml +++ b/def/automation.yaml @@ -1,5 +1,5 @@ rbac: - resource: { elements: [] } + resource: { references: [] } operations: grant: diff --git a/def/compose.chart.yaml b/def/compose.chart.yaml index 709e14f46..3f4435168 100644 --- a/def/compose.chart.yaml +++ b/def/compose.chart.yaml @@ -1,6 +1,6 @@ rbac: resource: - elements: [ namespaceID, ID ] + references: [ namespace, ID ] operations: read: diff --git a/def/compose.module-field.yaml b/def/compose.module-field.yaml index eca8a5d9b..a0eba5213 100644 --- a/def/compose.module-field.yaml +++ b/def/compose.module-field.yaml @@ -1,6 +1,6 @@ rbac: resource: - elements: [ namespaceID, moduleID, ID ] + references: [ namespace, module, ID ] operations: record.value.read: diff --git a/def/compose.module.yaml b/def/compose.module.yaml index 359d4045f..9af85cf38 100644 --- a/def/compose.module.yaml +++ b/def/compose.module.yaml @@ -1,6 +1,6 @@ rbac: resource: - elements: [ namespaceID, ID ] + references: [ namespace, ID ] operations: read: diff --git a/def/compose.namespace.yaml b/def/compose.namespace.yaml index c289cee3e..1b2d8acb5 100644 --- a/def/compose.namespace.yaml +++ b/def/compose.namespace.yaml @@ -6,6 +6,8 @@ rbac: description: Update namespace delete: description: Delete namespace + manage: + description: Access to namespace admin panel module.create: description: Create module on namespace chart.create: diff --git a/def/compose.page.yaml b/def/compose.page.yaml index dee829f18..627189337 100644 --- a/def/compose.page.yaml +++ b/def/compose.page.yaml @@ -1,12 +1,10 @@ rbac: resource: - elements: [ namespaceID, ID ] + references: [ namespace, ID ] operations: read: description: Read page - create: - description: Create page update: description: Update page delete: diff --git a/def/compose.record.yaml b/def/compose.record.yaml index 6df824ea8..a51137b0a 100644 --- a/def/compose.record.yaml +++ b/def/compose.record.yaml @@ -1,6 +1,6 @@ rbac: resource: - elements: [ namespaceID, moduleID, ID ] + references: [ namespace, module, ID ] operations: read: diff --git a/def/compose.yaml b/def/compose.yaml index 5a3db37e8..d2406883e 100644 --- a/def/compose.yaml +++ b/def/compose.yaml @@ -1,5 +1,5 @@ rbac: - resource: { elements: [] } + resource: { references: [] } operations: grant: diff --git a/def/def-envoy.json b/def/def-envoy.json new file mode 100644 index 000000000..ee59940d9 --- /dev/null +++ b/def/def-envoy.json @@ -0,0 +1,7 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "boolean", + "enum": [ "false" ], + "title": "Disable envoy support", + "description": "Enabled by default" +} diff --git a/def/def-rbac.json b/def/def-rbac.json index fc919fed8..a6b3ab977 100644 --- a/def/def-rbac.json +++ b/def/def-rbac.json @@ -2,20 +2,38 @@ "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { - "schema": { - "type": "string", - "description": "Schema used for prefixing RBAC resource string/url", - "pattern": "^([a-z]+)\\+([a-z]((.|-)[a-z]+)*)$" - }, "resource": { "additionalProperties": false, "type": "object", "description": "RBAC resource definition", "properties": { - "elements": { + "prefix": { + "type": "string", + "description": "Resource name", + "pattern": "^[a-z]+:?[a-zA-Z]+(:[a-zA-Z]+)?$" + }, + "references": { "type": "array", - "title": "Resource elements", - "description": "When not explicitly defined it fallbacks to one item array with 'ID'" + "title": "Reference components", + "description": "When not explicitly defined it fallbacks to one item array with 'ID'", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "field": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + { + "type": "string" + } + ] + } }, "attributes": { "anyOf": [ diff --git a/def/def.json b/def/def.json index 3c9b8d621..273fda9f0 100644 --- a/def/def.json +++ b/def/def.json @@ -3,6 +3,12 @@ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { + "imports": { + "type": "array", + "title": "List of go packages to be imported when generating files", + "description": "Attributes are used for generating list of contextual roles" + }, + "component": { "type": "string", "title": "Component of the definition", @@ -13,7 +19,8 @@ "title": "Resource of the definition", "description": "By default, resource is taken from the filename of the definition: from the first dot to the end of the filename without extension" }, - "rbac": { "$ref": "def-rbac.json" } + "rbac": { "$ref": "def-rbac.json" }, + "envoy": { "$ref": "def-envoy.json" } }, "additionalProperties": false } diff --git a/def/federation.exposed-module.yaml b/def/federation.exposed-module.yaml index 061caa09f..7dff05e90 100644 --- a/def/federation.exposed-module.yaml +++ b/def/federation.exposed-module.yaml @@ -1,7 +1,9 @@ rbac: resource: - elements: [ NodeID, ID ] + references: [ node, ID ] operations: manage: description: Manage shared module + +envoy: false diff --git a/def/federation.node.yaml b/def/federation.node.yaml index 54b98038d..bf46d9edc 100644 --- a/def/federation.node.yaml +++ b/def/federation.node.yaml @@ -4,3 +4,5 @@ rbac: description: Manage federation node module.create: description: Create shared module + +envoy: false diff --git a/def/federation.shared-module.yaml b/def/federation.shared-module.yaml index f050da4a5..9ecb0ad40 100644 --- a/def/federation.shared-module.yaml +++ b/def/federation.shared-module.yaml @@ -1,7 +1,9 @@ rbac: resource: - elements: [ NodeID, ID ] + references: [ node, ID ] operations: map: description: Map shared module + +envoy: false diff --git a/def/federation.yaml b/def/federation.yaml index 6d73950d7..fc1a9f44e 100644 --- a/def/federation.yaml +++ b/def/federation.yaml @@ -1,5 +1,5 @@ rbac: - resource: { elements: [] } + resource: { references: [] } operations: grant: description: Manage federation permissions @@ -11,3 +11,5 @@ rbac: description: Read settings settings.manage: description: Manage settings + +envoy: false diff --git a/def/system.yaml b/def/system.yaml index 50fb89d48..bf4df643c 100644 --- a/def/system.yaml +++ b/def/system.yaml @@ -1,5 +1,5 @@ rbac: - resource: { elements: [] } + resource: { references: [] } operations: grant: @@ -24,5 +24,5 @@ rbac: description: Create template reminder.assign: description: Assign reminders - messagebus-queue.create: + queue.create: description: Create messagebus queues diff --git a/federation/service/access_control.gen.go b/federation/service/access_control.gen.go index 862d72850..cdcaa565d 100644 --- a/federation/service/access_control.gen.go +++ b/federation/service/access_control.gen.go @@ -58,17 +58,61 @@ func (svc accessControl) Effective(ctx context.Context, rr ...rbac.Resource) (ee } func (svc accessControl) List() (out []map[string]string) { - return []map[string]string{ - {"resource": "corteza+federation.exposed-module", "operation": "manage"}, - {"resource": "corteza+federation.node", "operation": "manage"}, - {"resource": "corteza+federation.node", "operation": "module.create"}, - {"resource": "corteza+federation.shared-module", "operation": "map"}, - {"resource": "corteza+federation", "operation": "grant"}, - {"resource": "corteza+federation", "operation": "pair"}, - {"resource": "corteza+federation", "operation": "node.create"}, - {"resource": "corteza+federation", "operation": "settings.read"}, - {"resource": "corteza+federation", "operation": "settings.manage"}, + def := []map[string]string{ + { + "type": types.ExposedModuleResourceType, + "any": types.ExposedModuleRbacResource(0, 0), + "op": "manage", + }, + { + "type": types.NodeResourceType, + "any": types.NodeRbacResource(0), + "op": "manage", + }, + { + "type": types.NodeResourceType, + "any": types.NodeRbacResource(0), + "op": "module.create", + }, + { + "type": types.SharedModuleResourceType, + "any": types.SharedModuleRbacResource(0, 0), + "op": "map", + }, + { + "type": types.ComponentResourceType, + "any": types.ComponentRbacResource(), + "op": "grant", + }, + { + "type": types.ComponentResourceType, + "any": types.ComponentRbacResource(), + "op": "pair", + }, + { + "type": types.ComponentResourceType, + "any": types.ComponentRbacResource(), + "op": "node.create", + }, + { + "type": types.ComponentResourceType, + "any": types.ComponentRbacResource(), + "op": "settings.read", + }, + { + "type": types.ComponentResourceType, + "any": types.ComponentRbacResource(), + "op": "settings.manage", + }, } + + func(svc interface{}) { + if svc, is := svc.(interface{}).(interface{ list() []map[string]string }); is { + def = append(def, svc.list()...) + } + }(svc) + + return def } // Grant applies one or more RBAC rules @@ -189,39 +233,39 @@ func (svc accessControl) CanManageSettings(ctx context.Context) bool { // // This function is auto-generated func rbacResourceValidator(r string, oo ...string) error { - switch rbac.ResourceSchema(r) { - case "corteza+federation.exposed-module": + switch rbac.ResourceType(r) { + case types.ExposedModuleResourceType: return rbacExposedModuleResourceValidator(r, oo...) - case "corteza+federation.node": + case types.NodeResourceType: return rbacNodeResourceValidator(r, oo...) - case "corteza+federation.shared-module": + case types.SharedModuleResourceType: return rbacSharedModuleResourceValidator(r, oo...) - case "corteza+federation": + case types.ComponentResourceType: return rbacComponentResourceValidator(r, oo...) } - return fmt.Errorf("unknown resource schema '%q'", r) + return fmt.Errorf("unknown resource type '%q'", r) } // rbacResourceOperations returns defined operations for a requested resource // // This function is auto-generated func rbacResourceOperations(r string) map[string]bool { - switch rbac.ResourceSchema(r) { - case "corteza+federation.exposed-module": + switch rbac.ResourceType(r) { + case types.ExposedModuleResourceType: return map[string]bool{ "manage": true, } - case "corteza+federation.node": + case types.NodeResourceType: return map[string]bool{ "manage": true, "module.create": true, } - case "corteza+federation.shared-module": + case types.SharedModuleResourceType: return map[string]bool{ "map": true, } - case "corteza+federation": + case types.ComponentResourceType: return map[string]bool{ "grant": true, "pair": true, @@ -247,35 +291,39 @@ func rbacExposedModuleResourceValidator(r string, oo ...string) error { } } - if !strings.HasPrefix(r, types.ExposedModuleRbacResourceSchema+":/") { - return fmt.Errorf("invalid schema") - } - - pp := strings.Split(r[len(types.ExposedModuleRbacResourceSchema)+2:], "/") - if len(pp) != 2 { - return fmt.Errorf("invalid resource path") + if !strings.HasPrefix(r, types.ExposedModuleResourceType) { + // expecting resource to always include path + return fmt.Errorf("invalid resource type") } + const sep = "/" var ( - ppWildcard bool - pathElements = []string{ - "NodeID", + specIdUsed = true + + pp = strings.Split(strings.Trim(r[len(types.ExposedModuleResourceType):], sep), sep) + prc = []string{ + "nodeID", "ID", } ) + if len(pp) != len(prc) { + return fmt.Errorf("invalid resource path structure") + } + for i, p := range pp { if p == "*" { - ppWildcard = true + if !specIdUsed { + return fmt.Errorf("invalid resource path wildcard level (%d) for ExposedModule", i) + } + + specIdUsed = false continue } - if !ppWildcard { - return fmt.Errorf("invalid resource path wildcard level") - } - + specIdUsed = true if _, err := cast.ToUint64E(p); err != nil { - return fmt.Errorf("invalid ID for %s: '%s'", pathElements[i], p) + return fmt.Errorf("invalid reference for %s: '%s'", prc[i], p) } } @@ -295,34 +343,38 @@ func rbacNodeResourceValidator(r string, oo ...string) error { } } - if !strings.HasPrefix(r, types.NodeRbacResourceSchema+":/") { - return fmt.Errorf("invalid schema") - } - - pp := strings.Split(r[len(types.NodeRbacResourceSchema)+2:], "/") - if len(pp) != 1 { - return fmt.Errorf("invalid resource path") + if !strings.HasPrefix(r, types.NodeResourceType) { + // expecting resource to always include path + return fmt.Errorf("invalid resource type") } + const sep = "/" var ( - ppWildcard bool - pathElements = []string{ + specIdUsed = true + + pp = strings.Split(strings.Trim(r[len(types.NodeResourceType):], sep), sep) + prc = []string{ "ID", } ) + if len(pp) != len(prc) { + return fmt.Errorf("invalid resource path structure") + } + for i, p := range pp { if p == "*" { - ppWildcard = true + if !specIdUsed { + return fmt.Errorf("invalid resource path wildcard level (%d) for Node", i) + } + + specIdUsed = false continue } - if !ppWildcard { - return fmt.Errorf("invalid resource path wildcard level") - } - + specIdUsed = true if _, err := cast.ToUint64E(p); err != nil { - return fmt.Errorf("invalid ID for %s: '%s'", pathElements[i], p) + return fmt.Errorf("invalid reference for %s: '%s'", prc[i], p) } } @@ -342,35 +394,39 @@ func rbacSharedModuleResourceValidator(r string, oo ...string) error { } } - if !strings.HasPrefix(r, types.SharedModuleRbacResourceSchema+":/") { - return fmt.Errorf("invalid schema") - } - - pp := strings.Split(r[len(types.SharedModuleRbacResourceSchema)+2:], "/") - if len(pp) != 2 { - return fmt.Errorf("invalid resource path") + if !strings.HasPrefix(r, types.SharedModuleResourceType) { + // expecting resource to always include path + return fmt.Errorf("invalid resource type") } + const sep = "/" var ( - ppWildcard bool - pathElements = []string{ - "NodeID", + specIdUsed = true + + pp = strings.Split(strings.Trim(r[len(types.SharedModuleResourceType):], sep), sep) + prc = []string{ + "nodeID", "ID", } ) + if len(pp) != len(prc) { + return fmt.Errorf("invalid resource path structure") + } + for i, p := range pp { if p == "*" { - ppWildcard = true + if !specIdUsed { + return fmt.Errorf("invalid resource path wildcard level (%d) for SharedModule", i) + } + + specIdUsed = false continue } - if !ppWildcard { - return fmt.Errorf("invalid resource path wildcard level") - } - + specIdUsed = true if _, err := cast.ToUint64E(p); err != nil { - return fmt.Errorf("invalid ID for %s: '%s'", pathElements[i], p) + return fmt.Errorf("invalid reference for %s: '%s'", prc[i], p) } } @@ -390,8 +446,9 @@ func rbacComponentResourceValidator(r string, oo ...string) error { } } - if !strings.HasPrefix(r, types.ComponentRbacResourceSchema+":/") { - return fmt.Errorf("invalid schema") + if !strings.HasPrefix(r, types.ComponentResourceType) { + // expecting resource to always include path + return fmt.Errorf("invalid resource type") } return nil diff --git a/federation/service/exposed_module.go b/federation/service/exposed_module.go index 0812aab43..7b81ae7e9 100644 --- a/federation/service/exposed_module.go +++ b/federation/service/exposed_module.go @@ -276,8 +276,8 @@ func (svc exposedModule) Create(ctx context.Context, new *types.ExposedModule) ( } if fedRole != nil { - // get first id from role and add it as an allow rule - err = cs.DefaultAccessControl.Grant(ctx, rbac.AllowRule(fedRole.ID, m.RbacResource(), "record.read")) + // get first ID from role and add it as allow rule + err = cs.DefaultAccessControl.Grant(ctx, rbac.AllowRule(fedRole.ID, ct.RecordRbacResource(m.NamespaceID, m.ID, 0), "read")) } // set labels diff --git a/federation/service/node.go b/federation/service/node.go index 389e2fba5..5222a4098 100644 --- a/federation/service/node.go +++ b/federation/service/node.go @@ -484,17 +484,20 @@ func (svc node) fetchFederatedUser(ctx context.Context, n *types.Node) (*sysType return nil, err } - u, err = svc.sysUser.Create(ctx, user) + // context with fed. user credentials + ctxfed := auth.SetIdentityToContext(ctx, auth.FederationUser()) + + u, err = svc.sysUser.Create(ctxfed, user) if err != nil { return nil, err } - if err = service.DefaultRole.MemberAdd(ctx, r.ID, u.ID); err != nil { + if err = service.DefaultRole.MemberAdd(ctxfed, r.ID, u.ID); err != nil { return nil, err } - u.SetRoles(append(u.Roles(), r.ID)) + u.SetRoles(append(u.Roles(), r.ID)...) return u, nil } diff --git a/federation/service/sync.go b/federation/service/sync.go index a91f88635..adc7bc02f 100644 --- a/federation/service/sync.go +++ b/federation/service/sync.go @@ -224,7 +224,7 @@ func (s *Sync) LoadUserWithRoles(ctx context.Context, nodeID uint64) (*st.User, return nil, err } - u.SetRoles(rr.IDs()) + u.SetRoles(rr.IDs()...) return u, nil } diff --git a/federation/types/rbac.gen.go b/federation/types/rbac.gen.go index d3aa91f34..7c59affe9 100644 --- a/federation/types/rbac.gen.go +++ b/federation/types/rbac.gen.go @@ -13,6 +13,7 @@ package types // - federation.yaml import ( + "fmt" "strconv" ) @@ -24,15 +25,15 @@ type ( ) const ( - ExposedModuleRbacResourceSchema = "corteza+federation.exposed-module" - NodeRbacResourceSchema = "corteza+federation.node" - SharedModuleRbacResourceSchema = "corteza+federation.shared-module" - ComponentRbacResourceSchema = "corteza+federation" + ExposedModuleResourceType = "corteza::federation:exposed-module" + NodeResourceType = "corteza::federation:node" + SharedModuleResourceType = "corteza::federation:shared-module" + ComponentResourceType = "corteza::federation" ) // RbacResource returns string representation of RBAC resource for ExposedModule by calling ExposedModuleRbacResource fn // -// RBAC resource is in the corteza+federation.exposed-module:/... format +// RBAC resource is in the corteza::federation:exposed-module/... format // // This function is auto-generated func (r ExposedModule) RbacResource() string { @@ -41,31 +42,35 @@ func (r ExposedModule) RbacResource() string { // ExposedModuleRbacResource returns string representation of RBAC resource for ExposedModule // -// RBAC resource is in the corteza+federation.exposed-module:/... format +// RBAC resource is in the corteza::federation:exposed-module/... format // // This function is auto-generated -func ExposedModuleRbacResource(nodeID uint64, iD uint64) string { - out := ExposedModuleRbacResourceSchema + ":" - out += "/" - +func ExposedModuleRbacResource(nodeID uint64, id uint64) string { + cpts := []interface{}{ExposedModuleResourceType} if nodeID != 0 { - out += strconv.FormatUint(nodeID, 10) + cpts = append(cpts, strconv.FormatUint(nodeID, 10)) } else { - out += "*" + cpts = append(cpts, "*") } - out += "/" - if iD != 0 { - out += strconv.FormatUint(iD, 10) + if id != 0 { + cpts = append(cpts, strconv.FormatUint(id, 10)) } else { - out += "*" + cpts = append(cpts, "*") } - return out + + return fmt.Sprintf(ExposedModuleRbacResourceTpl(), cpts...) + +} + +// @todo template +func ExposedModuleRbacResourceTpl() string { + return "%s/%s/%s" } // RbacResource returns string representation of RBAC resource for Node by calling NodeRbacResource fn // -// RBAC resource is in the corteza+federation.node:/... format +// RBAC resource is in the corteza::federation:node/... format // // This function is auto-generated func (r Node) RbacResource() string { @@ -74,24 +79,29 @@ func (r Node) RbacResource() string { // NodeRbacResource returns string representation of RBAC resource for Node // -// RBAC resource is in the corteza+federation.node:/... format +// RBAC resource is in the corteza::federation:node/... format // // This function is auto-generated -func NodeRbacResource(iD uint64) string { - out := NodeRbacResourceSchema + ":" - out += "/" - - if iD != 0 { - out += strconv.FormatUint(iD, 10) +func NodeRbacResource(id uint64) string { + cpts := []interface{}{NodeResourceType} + if id != 0 { + cpts = append(cpts, strconv.FormatUint(id, 10)) } else { - out += "*" + cpts = append(cpts, "*") } - return out + + return fmt.Sprintf(NodeRbacResourceTpl(), cpts...) + +} + +// @todo template +func NodeRbacResourceTpl() string { + return "%s/%s" } // RbacResource returns string representation of RBAC resource for SharedModule by calling SharedModuleRbacResource fn // -// RBAC resource is in the corteza+federation.shared-module:/... format +// RBAC resource is in the corteza::federation:shared-module/... format // // This function is auto-generated func (r SharedModule) RbacResource() string { @@ -100,31 +110,35 @@ func (r SharedModule) RbacResource() string { // SharedModuleRbacResource returns string representation of RBAC resource for SharedModule // -// RBAC resource is in the corteza+federation.shared-module:/... format +// RBAC resource is in the corteza::federation:shared-module/... format // // This function is auto-generated -func SharedModuleRbacResource(nodeID uint64, iD uint64) string { - out := SharedModuleRbacResourceSchema + ":" - out += "/" - +func SharedModuleRbacResource(nodeID uint64, id uint64) string { + cpts := []interface{}{SharedModuleResourceType} if nodeID != 0 { - out += strconv.FormatUint(nodeID, 10) + cpts = append(cpts, strconv.FormatUint(nodeID, 10)) } else { - out += "*" + cpts = append(cpts, "*") } - out += "/" - if iD != 0 { - out += strconv.FormatUint(iD, 10) + if id != 0 { + cpts = append(cpts, strconv.FormatUint(id, 10)) } else { - out += "*" + cpts = append(cpts, "*") } - return out + + return fmt.Sprintf(SharedModuleRbacResourceTpl(), cpts...) + +} + +// @todo template +func SharedModuleRbacResourceTpl() string { + return "%s/%s/%s" } // RbacResource returns string representation of RBAC resource for Component by calling ComponentRbacResource fn // -// RBAC resource is in the corteza+federation:/... format +// RBAC resource is in the corteza::federation/... format // // This function is auto-generated func (r Component) RbacResource() string { @@ -133,10 +147,15 @@ func (r Component) RbacResource() string { // ComponentRbacResource returns string representation of RBAC resource for Component // -// RBAC resource is in the corteza+federation:/... format +// RBAC resource is in the corteza::federation/ format // // This function is auto-generated func ComponentRbacResource() string { - out := ComponentRbacResourceSchema + ":" - return out + return ComponentResourceType + "/" + +} + +// @todo template +func ComponentRbacResourceTpl() string { + return "%s" } diff --git a/pkg/auth/identity.go b/pkg/auth/identity.go index b359f7031..d28afc0a9 100644 --- a/pkg/auth/identity.go +++ b/pkg/auth/identity.go @@ -7,40 +7,43 @@ import ( ) type ( - Identity struct { + identity struct { id uint64 memberOf []uint64 } ) -func NewIdentity(id uint64, rr ...uint64) *Identity { - return &Identity{ - id: id, - memberOf: rr, +// Anonymous constructs and returns new anonymous identity with system anonymous roles +func Anonymous() *identity { + return &identity{ + memberOf: AnonymousRoles().IDs(), } } -func (i Identity) Identity() uint64 { +// Authenticated constructs and returns new authenticated identity with assigned roles + system authenticated roles +func Authenticated(id uint64, rr ...uint64) *identity { + return &identity{ + id: id, + memberOf: append(rr, AuthenticatedRoles().IDs()...), + } +} + +func (i identity) Identity() uint64 { return i.id } -func (i Identity) Roles() []uint64 { +func (i identity) Roles() []uint64 { return i.memberOf } -func (i Identity) Valid() bool { +func (i identity) Valid() bool { return i.id > 0 } -func (i Identity) String() string { +func (i identity) String() string { return fmt.Sprintf("%d", i.Identity()) } -func ExtractUserIDFromSubClaim(sub string) uint64 { - userID, _ := ExtractFromSubClaim(sub) - return userID -} - func ExtractFromSubClaim(sub string) (userID uint64, rr []uint64) { parts := strings.Split(sub, " ") rr = make([]uint64, len(parts)-1) diff --git a/pkg/auth/identity_context.go b/pkg/auth/identity_context.go index 4cc9628b4..9d5f9f216 100644 --- a/pkg/auth/identity_context.go +++ b/pkg/auth/identity_context.go @@ -12,10 +12,13 @@ func SetIdentityToContext(ctx context.Context, identity Identifiable) context.Co return context.WithValue(ctx, identityCtxKey{}, identity) } +// GetIdentityFromContext always returns identity, either valid or anonymous +// +// For anonymous user, it auto appends all anonymous defined on the system func GetIdentityFromContext(ctx context.Context) Identifiable { - if identity, ok := ctx.Value(identityCtxKey{}).(Identifiable); ok && identity != nil { + if identity, ok := ctx.Value(identityCtxKey{}).(Identifiable); ok && identity != nil && identity.Valid() { return identity } else { - return NewIdentity(0) + return Anonymous() } } diff --git a/pkg/auth/jwt.go b/pkg/auth/jwt.go index 8c96a2d11..914189481 100644 --- a/pkg/auth/jwt.go +++ b/pkg/auth/jwt.go @@ -3,14 +3,15 @@ package auth import ( "context" "fmt" - "github.com/cortezaproject/corteza-server/pkg/api" - "github.com/dgrijalva/jwt-go" - "github.com/go-chi/jwtauth" - "github.com/pkg/errors" "net/http" "strconv" "strings" "time" + + "github.com/cortezaproject/corteza-server/pkg/api" + "github.com/dgrijalva/jwt-go" + "github.com/go-chi/jwtauth" + "github.com/pkg/errors" ) type ( @@ -104,7 +105,7 @@ func (t *token) encode(i Identifiable, clientID uint64, scope ...string) string return tkn } -// HttpAuthenticator converts JWT claims into Identity and stores it into context +// HttpAuthenticator converts JWT claims into identity and stores it into context func (t *token) HttpAuthenticator() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -132,7 +133,7 @@ func (t *token) HttpAuthenticator() func(http.Handler) http.Handler { } // ClaimsToIdentity decodes sub & roles claims into identity -func ClaimsToIdentity(c jwt.MapClaims) (i *Identity) { +func ClaimsToIdentity(c jwt.MapClaims) (i *identity) { var ( aux interface{} ok bool @@ -143,7 +144,7 @@ func ClaimsToIdentity(c jwt.MapClaims) (i *Identity) { return } - i = &Identity{} + i = &identity{} if id, ok = aux.(string); ok { i.id, _ = strconv.ParseUint(id, 10, 64) } diff --git a/pkg/auth/middleware.go b/pkg/auth/middleware.go index e7bef4ae4..4eeb80438 100644 --- a/pkg/auth/middleware.go +++ b/pkg/auth/middleware.go @@ -1,8 +1,9 @@ package auth import ( - "github.com/cortezaproject/corteza-server/pkg/errors" "net/http" + + "github.com/cortezaproject/corteza-server/pkg/errors" ) func MiddlewareValidOnly(next http.Handler) http.Handler { diff --git a/pkg/auth/system.go b/pkg/auth/system.go index bcaf4b5b6..dce96af75 100644 --- a/pkg/auth/system.go +++ b/pkg/auth/system.go @@ -18,6 +18,10 @@ var ( provisionUser *types.User serviceUser *types.User federationUser *types.User + + authenticatedRoles []*types.Role + anonymousRoles []*types.Role + bypassRoles []*types.Role ) // SetSystemUsers takes list of users and sets/updates appropriate @@ -31,17 +35,45 @@ func SetSystemUsers(uu types.UserSet, rr types.RoleSet) { switch u.Handle { case ProvisionUserHandle: provisionUser = u.Clone() - federationUser.SetRoles([]uint64{bpr.ID}) + provisionUser.SetRoles(bpr.ID) case ServiceUserHandle: serviceUser = u.Clone() - federationUser.SetRoles([]uint64{bpr.ID}) + serviceUser.SetRoles(bpr.ID) case FederationUserHandle: federationUser = u.Clone() - federationUser.SetRoles([]uint64{bpr.ID}) + federationUser.SetRoles(bpr.ID) } } } +func SetSystemRoles(rr types.RoleSet) { + for _, r := range rr { + switch r.Handle { + case BypassRoleHandle: + bypassRoles = append(bypassRoles, r.Clone()) + case AuthenticatedRoleHandle: + authenticatedRoles = append(authenticatedRoles, r.Clone()) + case AnonymousRoleHandle: + anonymousRoles = append(anonymousRoles, r.Clone()) + } + } +} + +// BypassRoles returns all bypass Roles +func BypassRoles() types.RoleSet { + return bypassRoles +} + +// AuthenticatedRoles returns all authenticated Roles +func AuthenticatedRoles() types.RoleSet { + return authenticatedRoles +} + +// AnonymousRoles returns all anonymous Roles +func AnonymousRoles() types.RoleSet { + return anonymousRoles +} + // ProvisionUser returns clone of system provision user func ProvisionUser() *types.User { return provisionUser.Clone() diff --git a/pkg/codegen-v2/internal/def/def.go b/pkg/codegen-v2/internal/def/def.go deleted file mode 100644 index dabded3e3..000000000 --- a/pkg/codegen-v2/internal/def/def.go +++ /dev/null @@ -1,108 +0,0 @@ -package def - -import ( - "github.com/cortezaproject/corteza-server/pkg/codegen-v2/internal/tpl" - "github.com/cortezaproject/corteza-server/pkg/y7s" - "github.com/davecgh/go-spew/spew" - "gopkg.in/yaml.v3" - "strings" -) - -var _ = spew.Dump - -type ( - Document struct { - Skip bool `yaml:"(skip)"` - Component string - IsComponentResource bool `yaml:"-"` - Resource string - Source string - RBAC *rbac - } - - rbac struct { - Schema string - Resource *rbacResource - Operations rbacOperations - } - - rbacResource struct { - Elements []string - Attributes *rbacAttributes - } - - rbacOperations []*rbacOperation - - rbacOperation struct { - Operation string - CanFnName string `yaml:"canFnName"` - Description string - } - - rbacAttributes struct { - Fields []string `yaml:"-"` - } -) - -func (set *rbacOperations) UnmarshalYAML(n *yaml.Node) error { - return y7s.Each(n, func(k *yaml.Node, v *yaml.Node) (err error) { - def := rbacOperation{} - if k != nil { - def.Operation = k.Value - } - - *set = append(*set, &def) - return v.Decode(&def) - }) -} - -func (op *rbacOperation) UnmarshalYAML(n *yaml.Node) error { - if y7s.IsKind(n, yaml.ScalarNode) { - // @todo handle disabled operations - // the idea is that when service operations are defined we implicitly define - // RBAC operations. Here, we'll be able to remove implicitly defined operation - return nil - } - - type auxType rbacOperation - var aux = (*auxType)(op) - return n.Decode(aux) -} - -func (a *rbacAttributes) UnmarshalYAML(n *yaml.Node) error { - if y7s.IsKind(n, yaml.ScalarNode) { - return nil - } - - // if not scalar, assume we will get list of fields - a.Fields = make([]string, 0) - return n.Decode(&a.Fields) -} - -func RbacOperationCanFnName(res, op string) string { - // when check function name is not explicitly defined we try - // to use resource and operation name and generate easy-to-read name - // - // + => Can - // + => CanOn - - if res == "Component" { - res = "" - } - - if strings.Contains(op, ".") { - parts := strings.Split(op, ".") - l := len(parts) - - parts = append(parts[l-1:], parts[:l-1]...) - - if res != "" { - // Only append "on" if there is resource - parts = append(parts, "on") - } - - op = tpl.Export(parts...) - } - - return tpl.Export("can", op, res) -} diff --git a/pkg/codegen-v2/internal/def/proc.go b/pkg/codegen-v2/internal/def/proc.go deleted file mode 100644 index f1fb27b1f..000000000 --- a/pkg/codegen-v2/internal/def/proc.go +++ /dev/null @@ -1,60 +0,0 @@ -package def - -import ( - "fmt" - "github.com/cortezaproject/corteza-server/pkg/codegen-v2/internal/tpl" - "strings" -) - -// Preproc preprocesses the document and sets defaults -func (doc *Document) Proc(filename string) error { - doc.Source = filename - - // filename parts - fp := strings.Split(filename, ".") - // trim extension - fp = fp[:len(fp)-1] - if len(fp) > 0 { - // set component from the 1st part - doc.Component = fp[0] - } - - if len(fp) > 1 { - // if there are more parts, set resource - doc.Resource = fp[1] - } - - if strings.ToLower(doc.Resource) == "component" { - return fmt.Errorf("can not use 'component' as a resource name") - } else if doc.Resource == "" { - doc.Resource = "component" - doc.IsComponentResource = true - } - - doc.Resource = tpl.Export(doc.Resource) - - if err := doc.RBAC.proc(doc.Resource, fmt.Sprintf("corteza+%s", strings.Join(fp, "."))); err != nil { - return err - } - - return nil -} - -func (r *rbac) proc(resource, schema string) error { - if r.Schema == "" { - r.Schema = schema - } - - for _, op := range r.Operations { - // Generate all check name - if op.CanFnName == "" { - op.CanFnName = RbacOperationCanFnName(resource, op.Operation) - } - } - - if r.Resource == nil { - r.Resource = &rbacResource{Elements: []string{"ID"}} - } - - return nil -} diff --git a/pkg/codegen-v3/assets/templates/gocode/envoy/resource-rbac_references.go.tpl b/pkg/codegen-v3/assets/templates/gocode/envoy/resource-rbac_references.go.tpl new file mode 100644 index 000000000..37a4051ef --- /dev/null +++ b/pkg/codegen-v3/assets/templates/gocode/envoy/resource-rbac_references.go.tpl @@ -0,0 +1,35 @@ +package {{ .Package }} + +{{ template "header-gentext.tpl" }} +{{ template "header-definitions.tpl" . }} + +import ( +{{- range .Imports }} + {{ . }} +{{- end }} +) + +{{- range .Def }} +{{- if gt (len .RBAC.Resource.References) 0 }} +// {{ export .Component .Resource }}RbacReferences generates RBAC references +// +// Resources with "envoy: false" are skipped +// +// This function is auto-generated +func {{ export .Component .Resource }}RbacReferences({{- range .RBAC.Resource.References }}{{ unexport .Resource }} string, {{- end }}) (res *Ref, pp []*Ref, err error) { + {{- range .RBAC.Resource.References }} + {{- if eq .Field "ID" }} + if {{ unexport .Resource }} != "*" { + res = &Ref{ResourceType: types.{{ export .Resource }}ResourceType, Identifiers: MakeIdentifiers({{ unexport .Resource }})} + } + {{- else }} + if {{ unexport .Resource }} != "*" { + pp = append(pp, &Ref{ResourceType: types.{{ export .Resource }}ResourceType, Identifiers: MakeIdentifiers({{ unexport .Resource }})}) + } + {{- end }} + {{- end }} + + return +} +{{- end }} +{{- end }} diff --git a/pkg/codegen-v3/assets/templates/gocode/envoy/resource-rbac_rules_parse.go.tpl b/pkg/codegen-v3/assets/templates/gocode/envoy/resource-rbac_rules_parse.go.tpl new file mode 100644 index 000000000..97efa93c5 --- /dev/null +++ b/pkg/codegen-v3/assets/templates/gocode/envoy/resource-rbac_rules_parse.go.tpl @@ -0,0 +1,71 @@ +package {{ .Package }} + +{{ template "header-gentext.tpl" }} +{{ template "header-definitions.tpl" . }} + +import ( + "fmt" + "strings" +{{- range .Imports }} + {{ . }} +{{- end }} +) + + +// Parse generates resource setting logic for each resource +// +// Resources with "envoy: false" are skipped +// +// This function is auto-generated +func ParseRule(res string) (string, *Ref, []*Ref, error) { + if res == "" { + return "", nil, nil, fmt.Errorf("empty resource") + } + + sp := "/" + + res = strings.TrimSpace(res) + res = strings.TrimRight(res, sp) + rr := strings.Split(res, sp) + + // only service defined (corteza::system, corteza::compose, ...) + if len(rr) == 1 { + return res, nil, nil, nil + } + + // full thing + resourceType, path := rr[0], rr[1:] + + for p := 1; p < len(path); p++ { + if path[p] != "*" && path[p-1] == "*" { + return "", nil, nil, fmt.Errorf("invalid path wildcard combination for '%s'", res) + } + } + + + // make the resource provide the slice of parent resources we should nest under + switch resourceType { + {{- range .Def }} + case {{ unexport .Component "types" }}.{{ export .Resource }}ResourceType: + if len(path) != {{ len .RBAC.Resource.References }} { + return "", nil, nil, fmt.Errorf("expecting {{ len .RBAC.Resource.References }} reference components in path, got %d", len(path)) + } + {{- if gt (len .RBAC.Resource.References) 0 }} + ref, pp, err := {{ export .Component .Resource }}RbacReferences( + {{- range $i, $r := .RBAC.Resource.References }} + // {{ unexport $r.Resource }} + path[{{ $i }}], + {{ end }} + ) + return {{ unexport .Component "types" }}.{{ export .Resource }}ResourceType, ref, pp, err + {{ else }} + + // Component resource, no path + return {{ unexport .Component "types" }}.{{ export .Resource }}ResourceType, nil, nil, nil + {{- end }} + {{- end}} + } + + // return unhandled resource as-is + return resourceType, nil, nil, nil +} diff --git a/pkg/codegen-v2/assets/templates/gocode/header-definitions.tpl b/pkg/codegen-v3/assets/templates/gocode/header-definitions.tpl similarity index 100% rename from pkg/codegen-v2/assets/templates/gocode/header-definitions.tpl rename to pkg/codegen-v3/assets/templates/gocode/header-definitions.tpl diff --git a/pkg/codegen-v2/assets/templates/gocode/header-gentext.tpl b/pkg/codegen-v3/assets/templates/gocode/header-gentext.tpl similarity index 100% rename from pkg/codegen-v2/assets/templates/gocode/header-gentext.tpl rename to pkg/codegen-v3/assets/templates/gocode/header-gentext.tpl diff --git a/pkg/codegen-v2/assets/templates/gocode/access_control.go.tpl b/pkg/codegen-v3/assets/templates/gocode/rbac/access_control.go.tpl similarity index 77% rename from pkg/codegen-v2/assets/templates/gocode/access_control.go.tpl rename to pkg/codegen-v3/assets/templates/gocode/rbac/access_control.go.tpl index 4bd3de97d..b56448089 100644 --- a/pkg/codegen-v2/assets/templates/gocode/access_control.go.tpl +++ b/pkg/codegen-v3/assets/templates/gocode/rbac/access_control.go.tpl @@ -11,7 +11,7 @@ import ( "github.com/cortezaproject/corteza-server/pkg/rbac" "github.com/cortezaproject/corteza-server/pkg/actionlog" {{- range .Imports }} - {{ normalizeImport . }} + {{ . }} {{- end }} ) @@ -54,14 +54,27 @@ func (svc accessControl) Effective(ctx context.Context, rr ... rbac.Resource) (e } func (svc accessControl) List() (out []map[string]string) { - return []map[string]string{ + def := []map[string]string{ {{- range .Def }} - {{- $Schema := .RBAC.Schema }} + {{- $Resource := .Resource }} + {{- $RbacResource := .RBAC.Resource }} {{- range .RBAC.Operations }} - { "resource": {{ printf "%q" $Schema }}, "operation": {{ printf "%q" .Operation }} }, + { + "type": types.{{ coalesce $Resource }}ResourceType, + "any": types.{{ coalesce $Resource }}RbacResource({{ range $RbacResource.References }}0,{{ end }}), + "op": {{ printf "%q" .Operation }}, + }, {{- end }} {{- end }} } + + func(svc interface{}) { + if svc, is := svc.(interface{}).(interface{ list() []map[string]string }); is { + def = append(def, svc.list()...) + } + }(svc) + + return def } @@ -134,9 +147,6 @@ func (svc accessControl) FindRulesByRoleID(ctx context.Context, roleID uint64) ( {{- end }} {{ else }} - - {{ $ResStruct := .RBAC.Resource.Elements }} - {{- range .RBAC.Operations }} // {{ export .CanFnName }} checks if current user can {{ lower .Description }} // @@ -154,23 +164,23 @@ func (svc accessControl) FindRulesByRoleID(ctx context.Context, roleID uint64) ( // // This function is auto-generated func rbacResourceValidator(r string, oo ...string) error { - switch rbac.ResourceSchema(r) { + switch rbac.ResourceType(r) { {{- range .Def }} - case {{ printf "%q" .RBAC.Schema }}: + case types.{{ coalesce .Resource }}ResourceType: return rbac{{ .Resource }}ResourceValidator(r, oo...) {{- end }} } - return fmt.Errorf("unknown resource schema '%q'", r) + return fmt.Errorf("unknown resource type '%q'", r) } // rbacResourceOperations returns defined operations for a requested resource // // This function is auto-generated func rbacResourceOperations(r string) map[string]bool { - switch rbac.ResourceSchema(r) { + switch rbac.ResourceType(r) { {{- range .Def }} - case {{ printf "%q" .RBAC.Schema }}: + case types.{{ coalesce .Resource }}ResourceType: return map[string]bool{ {{- range .RBAC.Operations }} {{ printf "%q" .Operation }}: true, @@ -200,39 +210,42 @@ func rbac{{ .Resource }}ResourceValidator(r string, oo ...string) error { } } - if !strings.HasPrefix(r, {{ $GoType }}RbacResourceSchema + ":/") { - return fmt.Errorf("invalid schema") + if !strings.HasPrefix(r, {{ $GoType }}ResourceType) { + // expecting resource to always include path + return fmt.Errorf("invalid resource type") } -{{ if not .IsComponentResource }} - pp := strings.Split(r[len({{ $GoType }}RbacResourceSchema)+2:], "/") - if len(pp) != {{ len .RBAC.Resource.Elements }} { - return fmt.Errorf("invalid resource path") - } -{{- end }} -{{ if .RBAC.Resource.Elements }} +{{ if .RBAC.Resource.References }} + const sep = "/" var ( - ppWildcard bool - pathElements = []string{ - {{- range .RBAC.Resource.Elements }} - {{ printf "%q" . }}, + specIdUsed = true + + pp = strings.Split(strings.Trim(r[len({{ $GoType }}ResourceType):], sep), sep) + prc = []string{ + {{- range .RBAC.Resource.References }} + {{ printf "%q" .Field }}, {{- end }} } ) + if len(pp) != len(prc) { + return fmt.Errorf("invalid resource path structure") + } + for i, p := range pp { if p == "*" { - ppWildcard = true + if !specIdUsed { + return fmt.Errorf("invalid resource path wildcard level (%d) for {{ .Resource }}", i) + } + + specIdUsed = false continue } - if !ppWildcard { - return fmt.Errorf("invalid resource path wildcard level") - } - + specIdUsed = true if _, err := cast.ToUint64E(p); err != nil { - return fmt.Errorf("invalid ID for %s: '%s'", pathElements[i], p) + return fmt.Errorf("invalid reference for %s: '%s'", prc[i], p) } } {{- end }} diff --git a/pkg/codegen-v2/assets/templates/gocode/rbac.go.tpl b/pkg/codegen-v3/assets/templates/gocode/rbac/types.go.tpl similarity index 60% rename from pkg/codegen-v2/assets/templates/gocode/rbac.go.tpl rename to pkg/codegen-v3/assets/templates/gocode/rbac/types.go.tpl index dee984f3d..5442cd6af 100644 --- a/pkg/codegen-v2/assets/templates/gocode/rbac.go.tpl +++ b/pkg/codegen-v3/assets/templates/gocode/rbac/types.go.tpl @@ -4,6 +4,7 @@ package {{ .Package }} {{ template "header-definitions.tpl" . }} import ( + "fmt" "strconv" ) @@ -16,7 +17,7 @@ type ( const ( {{- range .Def }} - {{ coalesce .Resource "Component" }}RbacResourceSchema = "{{ .RBAC.Schema }}" + {{ coalesce .Resource "Component" }}ResourceType = "{{ .RBAC.ResourceType }}" {{- end }} ) @@ -28,30 +29,45 @@ const ( // RbacResource returns string representation of RBAC resource for {{ .Resource }} by calling {{ .Resource }}RbacResource fn // -// RBAC resource is in the {{ .RBAC.Schema }}:/... format +// RBAC resource is in the {{ .RBAC.ResourceType }}/... format // // This function is auto-generated func (r {{ .Resource }}) RbacResource() string { - return {{ .Resource }}RbacResource({{ if .RBAC.Resource }}{{ range .RBAC.Resource.Elements }}r.{{ export . }},{{ end }}{{ end }}) + return {{ .Resource }}RbacResource({{ if .RBAC.Resource }}{{ range .RBAC.Resource.References }}r.{{ export .Field }},{{ end }}{{ end }}) } // {{ .Resource }}RbacResource returns string representation of RBAC resource for {{ .Resource }} // -// RBAC resource is in the {{ .RBAC.Schema }}:/... format +// RBAC resource is in the {{ .RBAC.ResourceType }}/{{- if .RBAC.Resource.References }}...{{ end }} format // // This function is auto-generated -func {{ .Resource }}RbacResource({{ if .RBAC.Resource }}{{ range .RBAC.Resource.Elements }}{{ unexport . }} uint64,{{ end }}{{ end }}) string { - out := {{ .Resource }}RbacResourceSchema + ":" - {{- range .RBAC.Resource.Elements }} - out += "/" - - if {{ unexport . }} != 0 { - out += strconv.FormatUint({{ unexport . }}, 10) +func {{ .Resource }}RbacResource({{ if .RBAC.Resource }}{{ range .RBAC.Resource.References }}{{ unexport .Field }} uint64,{{ end }}{{ end }}) string { + {{- if .RBAC.Resource.References }} + cpts := []interface{{"{}"}}{{"{"}}{{ .Resource }}ResourceType{{"}"}} + {{- range .RBAC.Resource.References }} + if {{ unexport .Field }} != 0 { + cpts = append(cpts, strconv.FormatUint({{ unexport .Field }}, 10)) } else { - out += "*" + cpts = append(cpts, "*") } + + {{ end }} + return fmt.Sprintf({{ .Resource }}RbacResourceTpl(), cpts...) + {{- else }} + return {{ .Resource }}ResourceType + "/" + {{- end }} + +} + +// @todo template +func {{ .Resource }}RbacResourceTpl() string { + {{- if .RBAC.Resource.References }} + return "%s + {{- range .RBAC.Resource.References }}/%s{{- end }}" + + {{- else }} + return "%s" {{- end }} - return out } {{ if .RBAC.Resource.Attributes }} @@ -65,7 +81,7 @@ func {{ .Resource }}RbacResource({{ if .RBAC.Resource }}{{ range .RBAC.Resource. {{ if .RBAC.Resource.Attributes.Fields }} // {{ .Resource }}RbacResource returns string representation of RBAC resource for {{ .Resource }} // - // RBAC resource is in the {{ .RBAC.Schema }}:/... format + // RBAC resource is in the {{ .RBAC.ResourceType }}/... format // // This function is auto-generated func {{ unexport .Resource }}RbacAttributes(r {{ .Resource }}) map[string]interface{} { diff --git a/pkg/codegen-v3/internal/def/doc.go b/pkg/codegen-v3/internal/def/doc.go new file mode 100644 index 000000000..ab11b98fe --- /dev/null +++ b/pkg/codegen-v3/internal/def/doc.go @@ -0,0 +1,86 @@ +package def + +import ( + "fmt" + "strings" + + "github.com/cortezaproject/corteza-server/pkg/codegen-v3/internal/tpl" + "github.com/cortezaproject/corteza-server/pkg/y7s" + "gopkg.in/yaml.v3" +) + +type ( + Document struct { + Skip bool `yaml:"(skip)"` + Imports []string + Component string + IsComponentResource bool `yaml:"-"` + Resource string + Source string + RBAC *rbac + Envoy bool `yaml:"envoy"` + } +) + +func (set *rbacOperations) UnmarshalYAML(n *yaml.Node) error { + return y7s.Each(n, func(k *yaml.Node, v *yaml.Node) (err error) { + def := rbacOperation{} + if k != nil { + def.Operation = k.Value + } + + *set = append(*set, &def) + return v.Decode(&def) + }) +} + +// Preproc preprocesses the document and sets defaults +func (doc *Document) Proc(filename string) error { + doc.Source = filename + + // filename parts + fp := strings.Split(filename, ".") + // trim extension + fp = fp[:len(fp)-1] + if len(fp) > 0 && doc.Component == "" { + // set component from the 1st part + // component is system, compose, ... + doc.Component = fp[0] + } + + if len(fp) > 1 && doc.Resource == "" { + // if there are more parts, set resource + // resource is user, module, record, workflow, ... + doc.Resource = fp[1] + } + + if strings.ToLower(doc.Resource) == "component" { + return fmt.Errorf("can not use 'component' as a resource name, this is done automatically") + } else if doc.Resource == "" { + doc.Resource = "component" + doc.IsComponentResource = true + } + + doc.Imports = normalizeImport(doc.Imports...) + + if err := doc.RBAC.proc(doc.Component, doc.Resource); err != nil { + return err + } + + doc.Resource = tpl.Export(doc.Resource) + + return nil +} + +func normalizeImport(ii ...string) []string { + for i := range ii { + if strings.Contains(ii[i], " ") { + p := strings.SplitN(ii[i], " ", 2) + ii[i] = fmt.Sprintf(`%s "%s"`, p[0], strings.Trim(p[1], `"`)) + } else { + ii[i] = fmt.Sprintf(`"%s"`, strings.Trim(ii[i], `"'`+"`")) + } + } + + return ii +} diff --git a/pkg/codegen-v3/internal/def/rbac.go b/pkg/codegen-v3/internal/def/rbac.go new file mode 100644 index 000000000..245ecfa7f --- /dev/null +++ b/pkg/codegen-v3/internal/def/rbac.go @@ -0,0 +1,165 @@ +package def + +import ( + "fmt" + "strings" + + "github.com/cortezaproject/corteza-server/pkg/codegen-v3/internal/tpl" + "github.com/cortezaproject/corteza-server/pkg/y7s" + "gopkg.in/yaml.v3" +) + +type ( + rbac struct { + // fully qualified resource name + ResourceType string `yaml:"resourceType"` + Resource *rbacResource + Operations rbacOperations + } + + rbacResource struct { + References []*rbacResourceRef + Attributes *rbacAttributes + } + + rbacResourceRef struct { + Field string + ResourceType string + Resource string + Component string + + custom bool + } + + rbacOperations []*rbacOperation + + rbacOperation struct { + Operation string + CanFnName string `yaml:"canFnName"` + Description string + } + + rbacAttributes struct { + Fields []string `yaml:"-"` + } +) + +func (r *rbac) proc(component, resource string) error { + const ( + defaultNS = "corteza" + nsDelimiter = "::" + ) + + if r.ResourceType == "" { + if strings.ToLower(resource) == "component" { + r.ResourceType = component + } else { + r.ResourceType = fmt.Sprintf("%s:%s", component, resource) + } + + r.ResourceType = defaultNS + nsDelimiter + r.ResourceType + } + + if !strings.Contains(r.ResourceType, nsDelimiter) { + return fmt.Errorf("no namespace prefix found (e.g.: 'corteza::') in resource type") + } + + for _, op := range r.Operations { + // Generate all check name + if op.CanFnName == "" { + op.CanFnName = RbacOperationCanFnName(resource, op.Operation) + } + } + + if r.Resource == nil { + r.Resource = &rbacResource{References: []*rbacResourceRef{{Field: "ID"}}} + } + + // check types of each referenced component + // and prefix non-custom components with corteza:: + // and self-references (field==ID) with own resource type + for _, rc := range r.Resource.References { + if !rc.custom { + if rc.Field == "ID" { + rc.ResourceType = r.ResourceType + rc.Component = component + rc.Resource = resource + } else { + rc.ResourceType = defaultNS + nsDelimiter + fmt.Sprintf("%s:%s", component, rc.Field) + rc.Component = component + rc.Resource = rc.Field + rc.Field = rc.Field + "ID" + } + } + } + + return nil +} + +func (op *rbacOperation) UnmarshalYAML(n *yaml.Node) error { + if y7s.IsKind(n, yaml.ScalarNode) { + // @todo handle disabled operations + // the idea is that when service operations are defined we implicitly define + // RBAC operations. Here, we'll be able to remove implicitly defined operation + return nil + } + + type auxType rbacOperation + var aux = (*auxType)(op) + return n.Decode(aux) +} + +func (op *rbacResourceRef) UnmarshalYAML(n *yaml.Node) error { + if y7s.IsKind(n, yaml.ScalarNode) { + op.Field = n.Value + if n.Value != "ID" { + op.ResourceType = n.Value + // @todo expand resource & component + } + + return nil + } + + type auxType rbacResourceRef + var aux = (*auxType)(op) + aux.custom = true + return n.Decode(aux) +} + +func (a *rbacAttributes) UnmarshalYAML(n *yaml.Node) error { + if y7s.IsKind(n, yaml.ScalarNode) { + return nil + } + + // if not scalar, assume we will get list of fields + a.Fields = make([]string, 0) + return n.Decode(&a.Fields) +} + +func RbacOperationCanFnName(res, op string) string { + // when check function name is not explicitly defined we try + // to use resource and operation name and generate easy-to-read name + // + // + => Can + // + => CanOn + + if strings.ToLower(res) == "component" { + res = "" + } + + if strings.Contains(op, ".") { + parts := strings.Split(op, ".") + l := len(parts) + + parts = append(parts[l-1:], parts[:l-1]...) + + if res != "" { + // Only append "on" if there is resource + parts = append(parts, "on") + } + + op = tpl.Export(parts...) + } + + return tpl.Export("can", op, res) +} diff --git a/pkg/codegen-v3/internal/gen/envoy.go b/pkg/codegen-v3/internal/gen/envoy.go new file mode 100644 index 000000000..4ca895cb4 --- /dev/null +++ b/pkg/codegen-v3/internal/gen/envoy.go @@ -0,0 +1,82 @@ +package gen + +import ( + "fmt" + "text/template" + + "github.com/cortezaproject/corteza-server/pkg/codegen-v3/internal/def" + "github.com/cortezaproject/corteza-server/pkg/codegen-v3/internal/tpl" + "github.com/cortezaproject/corteza-server/pkg/slice" +) + +func Envoy(t *template.Template, dd []*def.Document) error { + return List{ + "resource rbac parse": envoyResourceRbacUnmarshal, + "resource rbac references": envoyResourceRbacReferences, + }.Generate(t, dd) +} + +// EnvoyResourceRbacUnmarshal envoy rbac unmarshal +// /service/rbac.gen.go +// +// Contains all RBAC related definitions +func envoyResourceRbacUnmarshal(t *template.Template, dd []*def.Document) (err error) { + const ( + templateName = "envoy/resource-rbac_rules_parse.go.tpl" + outputPathTpl = "pkg/envoy/resource/rbac_rules_parse.gen.go" + ) + + dd = filter(dd, func(d *def.Document) bool { return d.Envoy }) + + // build list of component type imports + ctImports := make([]string, 0) + for _, d := range dd { + imp := d.Component + "Types " + cImport(d.Component, "types") + if !slice.HasString(ctImports, imp) { + ctImports = append(ctImports, imp) + } + } + + w := tpl.Wrap{ + Package: "resource", + Def: dd, + Imports: append(collectImports(dd...), ctImports...), + } + + err = tpl.GoTemplate(outputPathTpl, t.Lookup(templateName), w) + if err != nil { + return + } + + return +} + +// EnvoyResourceRbacReferences generates one rbac definition file per component +// /service/rbac.gen.go +// +// Contains all RBAC related definitions +func envoyResourceRbacReferences(t *template.Template, dd []*def.Document) (err error) { + const ( + templateName = "envoy/resource-rbac_references.go.tpl" + outputPathTpl = "pkg/envoy/resource/rbac_references_%s.gen.go" + ) + + dd = filter(dd, func(d *def.Document) bool { return d.Envoy }) + + for component, perComponent := range partByComponent(dd) { + + w := tpl.Wrap{ + Package: "resource", + Component: component, + Def: perComponent, + Imports: append(collectImports(perComponent...), cImport(component, "types")), + } + + err = tpl.GoTemplate(fmt.Sprintf(outputPathTpl, component), t.Lookup(templateName), w) + if err != nil { + return + } + } + + return +} diff --git a/pkg/codegen-v3/internal/gen/gen.go b/pkg/codegen-v3/internal/gen/gen.go new file mode 100644 index 000000000..977b88f26 --- /dev/null +++ b/pkg/codegen-v3/internal/gen/gen.go @@ -0,0 +1,67 @@ +package gen + +import ( + "fmt" + "text/template" + + "github.com/cortezaproject/corteza-server/pkg/codegen-v3/internal/def" +) + +type ( + List map[string]func(*template.Template, []*def.Document) error +) + +func (gg List) Generate(tpls *template.Template, dd []*def.Document) (err error) { + for l, g := range gg { + if err = g(tpls, dd); err != nil { + return fmt.Errorf("codegen for %s failed: %w", l, err) + } + } + + return +} + +func filter(dd []*def.Document, check func(*def.Document) bool) []*def.Document { + aux := make([]*def.Document, 0, len(dd)) + for _, d := range dd { + if !check(d) { + continue + } + + aux = append(aux, d) + } + return aux +} + +func partByComponent(dd []*def.Document) map[string][]*def.Document { + var ( + parted = make(map[string][]*def.Document) + ) + + for _, d := range dd { + parted[d.Component] = append(parted[d.Component], d) + } + + return parted +} + +func collectImports(dd ...*def.Document) []string { + mm := make(map[string]bool) + for _, d := range dd { + for _, i := range d.Imports { + mm[i] = true + } + } + + ii := make([]string, 0, len(mm)) + for i := range mm { + ii = append(ii, i) + } + + return ii +} + +// component import +func cImport(c, s string) string { + return fmt.Sprintf(`"github.com/cortezaproject/corteza-server/%s/%s"`, c, s) +} diff --git a/pkg/codegen-v2/codegen.go b/pkg/codegen-v3/internal/gen/rbac.go similarity index 52% rename from pkg/codegen-v2/codegen.go rename to pkg/codegen-v3/internal/gen/rbac.go index 62b2921be..f99a978e2 100644 --- a/pkg/codegen-v2/codegen.go +++ b/pkg/codegen-v3/internal/gen/rbac.go @@ -1,47 +1,27 @@ -package main +package gen import ( "fmt" - "github.com/cortezaproject/corteza-server/pkg/codegen-v2/internal/def" - "github.com/cortezaproject/corteza-server/pkg/codegen-v2/internal/tpl" "text/template" + + "github.com/cortezaproject/corteza-server/pkg/codegen-v3/internal/def" + "github.com/cortezaproject/corteza-server/pkg/codegen-v3/internal/tpl" ) -// rbac generates one rbac definition file per service -// /service/rbac.gen.go -// -// Contains all RBAC related definitions -func accessControlService(t *template.Template, dd []*def.Document) (err error) { - const ( - template = "access_control.go.tpl" - outputPathTpl = "%s/service/access_control.gen.go" - ) - - for component, perComponent := range partByComponent(dd) { - w := tpl.Wrap{ - Package: "service", - Component: component, - Def: perComponent, - } - - w.Imports = append(w.Imports, cImport(component, "types")) - - err = tpl.GoTemplate(fmt.Sprintf(outputPathTpl, component), t.Lookup(template), w) - if err != nil { - return - } - } - - return +func RBAC(t *template.Template, dd []*def.Document) error { + return List{ + "type": rbacTypes, + "service access control": rbacAccessControlService, + }.Generate(t, dd) } -// rbac generates one rbac definition file per service +// RbacTypes generates rbac definitions (one per component) // /service/rbac.gen.go // // Contains all RBAC related definitions func rbacTypes(t *template.Template, dd []*def.Document) (err error) { const ( - template = "rbac.go.tpl" + templateName = "rbac/types.go.tpl" outputPathTpl = "%s/types/rbac.gen.go" ) @@ -52,7 +32,7 @@ func rbacTypes(t *template.Template, dd []*def.Document) (err error) { Def: perComponent, } - err = tpl.GoTemplate(fmt.Sprintf(outputPathTpl, component), t.Lookup(template), w) + err = tpl.GoTemplate(fmt.Sprintf(outputPathTpl, component), t.Lookup(templateName), w) if err != nil { return } @@ -61,18 +41,29 @@ func rbacTypes(t *template.Template, dd []*def.Document) (err error) { return } -func partByComponent(dd []*def.Document) map[string][]*def.Document { - var ( - parted = make(map[string][]*def.Document) +// RbacAccessControlService generates access control functions (one file per component) +// /service/rbac.gen.go +// +// Contains all RBAC related definitions +func rbacAccessControlService(t *template.Template, dd []*def.Document) (err error) { + const ( + templateName = "rbac/access_control.go.tpl" + outputPathTpl = "%s/service/access_control.gen.go" ) - for _, d := range dd { - parted[d.Component] = append(parted[d.Component], d) + for component, perComponent := range partByComponent(dd) { + w := tpl.Wrap{ + Package: "service", + Component: component, + Def: perComponent, + Imports: append(collectImports(perComponent...), cImport(component, "types")), + } + + err = tpl.GoTemplate(fmt.Sprintf(outputPathTpl, component), t.Lookup(templateName), w) + if err != nil { + return + } } - return parted -} - -func cImport(c, s string) string { - return fmt.Sprintf("github.com/cortezaproject/corteza-server/%s/%s", c, s) + return } diff --git a/pkg/codegen-v2/internal/tpl/templating.go b/pkg/codegen-v3/internal/tpl/templating.go similarity index 68% rename from pkg/codegen-v2/internal/tpl/templating.go rename to pkg/codegen-v3/internal/tpl/templating.go index 9cb94dd1b..119abe40b 100644 --- a/pkg/codegen-v2/internal/tpl/templating.go +++ b/pkg/codegen-v3/internal/tpl/templating.go @@ -3,13 +3,16 @@ package tpl import ( "bytes" "fmt" - "github.com/Masterminds/sprig" "go/format" "io" + "io/ioutil" "os" + "path/filepath" "regexp" "strings" "text/template" + + "github.com/Masterminds/sprig" ) type ( @@ -48,32 +51,51 @@ func Unexport(pp ...string) (out string) { return } - return strings.ToLower(out[:1]) + out[1:] -} - -func NormalizeImport(i string) string { - if strings.Contains(i, " ") { - p := strings.SplitN(i, " ", 2) - return fmt.Sprintf(`%s "%s"`, p[0], strings.Trim(p[1], `"`)) - } else { - return fmt.Sprintf(`"%s"`, strings.Trim(i, `"`)) + if out == "ID" { + return "id" } + + return strings.ToLower(out[:1]) + out[1:] } func BaseTemplate() *template.Template { return template.New(""). Funcs(sprig.TxtFuncMap()). Funcs(map[string]interface{}{ - "export": Export, - "unexport": Unexport, - "normalizeImport": NormalizeImport, + "export": Export, + "unexport": Unexport, }) } +func LoadTemplates(rTpl *template.Template, rootDir string) (*template.Template, error) { + cleanRoot := filepath.Clean(rootDir) + pfx := len(cleanRoot) + 1 + + return rTpl, filepath.Walk(cleanRoot, func(path string, info os.FileInfo, err error) error { + if info.IsDir() || !strings.HasSuffix(path, ".tpl") || err != nil { + return err + } + + b, err := ioutil.ReadFile(path) + if err != nil { + return err + } + + name := path[pfx:] + rTpl, err = rTpl.New(name).Parse(string(b)) + + return err + }) +} + func GoTemplate(dst string, tpl *template.Template, payload Wrap) (err error) { var output io.WriteCloser buf := bytes.Buffer{} + if tpl == nil { + return fmt.Errorf("could not find template for %s", dst) + } + if err := tpl.Execute(&buf, payload); err != nil { return err } diff --git a/pkg/codegen-v2/loader.go b/pkg/codegen-v3/loader.go similarity index 69% rename from pkg/codegen-v2/loader.go rename to pkg/codegen-v3/loader.go index 9de33b89f..a0f0ac865 100644 --- a/pkg/codegen-v2/loader.go +++ b/pkg/codegen-v3/loader.go @@ -2,38 +2,40 @@ package main import ( "fmt" - "github.com/cortezaproject/corteza-server/pkg/cli" - "github.com/cortezaproject/corteza-server/pkg/codegen-v2/internal/def" - "github.com/cortezaproject/corteza-server/pkg/codegen-v2/internal/tpl" - "github.com/davecgh/go-spew/spew" - "gopkg.in/yaml.v3" "io" "os" "path/filepath" + + "github.com/cortezaproject/corteza-server/pkg/cli" + "github.com/cortezaproject/corteza-server/pkg/codegen-v3/internal/def" + "github.com/cortezaproject/corteza-server/pkg/codegen-v3/internal/gen" + "github.com/cortezaproject/corteza-server/pkg/codegen-v3/internal/tpl" + "github.com/davecgh/go-spew/spew" + "gopkg.in/yaml.v3" ) var _ = spew.Dump func main() { - def, err := loadDefinitions(os.Args[1]) + dd, err := loadDefinitions(os.Args[1]) cli.HandleError(err) - tpls, err := tpl.BaseTemplate().ParseGlob("./pkg/codegen-v2/assets/templates/gocode/*.tpl") + tpls, err := tpl.LoadTemplates(tpl.BaseTemplate(), "./pkg/codegen-v3/assets/templates/gocode") if err != nil { cli.HandleError(fmt.Errorf("could not load templates: %w", err)) } - if err = rbacTypes(tpls, def); err != nil { - cli.HandleError(fmt.Errorf("could not generate RBAC type code: %w", err)) - } - - if err = accessControlService(tpls, def); err != nil { - cli.HandleError(fmt.Errorf("could not generate access control service code: %w", err)) - } + cli.HandleError(gen.List{ + "RBAC": gen.RBAC, + "Envoy": gen.Envoy, + }.Generate(tpls, dd)) } func loadDefinition(r io.Reader) (*def.Document, error) { - doc := &def.Document{} + doc := &def.Document{ + Envoy: true, + } + return doc, yaml.NewDecoder(r).Decode(doc) } diff --git a/pkg/codegen/assets/store_rdbms.gen.go.tpl b/pkg/codegen/assets/store_rdbms.gen.go.tpl index 05e7b23b8..843a8d70c 100644 --- a/pkg/codegen/assets/store_rdbms.gen.go.tpl +++ b/pkg/codegen/assets/store_rdbms.gen.go.tpl @@ -293,6 +293,7 @@ func (s Store) {{ export "query" $.Types.Plural }} ( check func(*{{ $.Types.GoType }}) (bool, error), ) ([]*{{ $.Types.GoType }}, error) { var ( + tmp = make([]*{{ $.Types.GoType }}, 0, DefaultSliceCapacity) set = make([]*{{ $.Types.GoType }}, 0, DefaultSliceCapacity) res *{{ $.Types.GoType }} @@ -314,6 +315,16 @@ func (s Store) {{ export "query" $.Types.Plural }} ( return nil, err } + tmp = append(tmp, res) + } + +{{ if .RDBMS.CustomPostLoadProcessor }} + if err = s.{{ unexport $.Types.Singular }}PostLoadProcessor(ctx{{ template "extraArgsCall" . }}, tmp...); err != nil { + return nil, err + } +{{end }} + + for _, res = range tmp { {{ if $.Search.EnableFilterCheckFn }} // check fn set, call it and see if it passed the test // if not, skip the item @@ -328,13 +339,7 @@ func (s Store) {{ export "query" $.Types.Plural }} ( set = append(set, res) } -{{ if .RDBMS.CustomPostLoadProcessor }} - if err = s.{{ unexport $.Types.Singular }}PostLoadProcessor(ctx{{ template "extraArgsCall" . }}, set...); err != nil { - return nil, err - } -{{end }} - - return set, rows.Err() + return set, nil } diff --git a/pkg/corredor/service.go b/pkg/corredor/service.go index b7a808850..ef0d3f36c 100644 --- a/pkg/corredor/service.go +++ b/pkg/corredor/service.go @@ -11,7 +11,6 @@ import ( "github.com/cortezaproject/corteza-server/pkg/errors" "github.com/cortezaproject/corteza-server/pkg/eventbus" "github.com/cortezaproject/corteza-server/pkg/options" - "github.com/cortezaproject/corteza-server/pkg/rbac" "github.com/cortezaproject/corteza-server/pkg/sentry" "github.com/cortezaproject/corteza-server/system/types" "github.com/go-chi/chi/middleware" @@ -73,8 +72,11 @@ type ( // caching user lookups (w/ errors) userLookupCache userLookupCacheMap - // set of permission rules, generated from security info of each script - permissions rbac.RuleSet + // exec control + // pairs of scripts & roles that are explicitly denied exec the script + // + // Note: if script/role is missing from map it will be allowed to execute the script + denyExec map[string]map[uint64]bool } ScriptArgs interface { @@ -93,10 +95,6 @@ type ( Unregister(ptrs ...uintptr) } - iteratorRegistry interface { - Exec(ctx context.Context, resourceType string, f map[string]string, action string) error - } - userFinder interface { FindByAny(context.Context, interface{}) (*types.User, error) } @@ -108,10 +106,6 @@ type ( authTokenMaker interface { Encode(auth.Identifiable, ...string) string } - - permissionRuleChecker interface { - Check(res, op string, roles ...uint64) rbac.Access - } ) const ( @@ -176,7 +170,8 @@ func NewService(logger *zap.Logger, opt options.CorredorOpt) *service { authTokenMaker: auth.DefaultJwtHandler, eventRegistry: eventbus.Service(), - permissions: rbac.RuleSet{}, + + denyExec: make(map[string]map[uint64]bool), userLookupCache: userLookupCacheMap{}, @@ -392,14 +387,20 @@ func (svc service) Exec(ctx context.Context, scriptName string, args ScriptArgs) return svc.exec(ctx, scriptName, runAs, args) } -// Can current user execute this script -// -// This is used only in case of explicit execution (onManual) and never when -// scripts are executed implicitly (deferred, before/after...) +// Check for any explicit denies for any of the user roles on the script func (svc service) canExec(ctx context.Context, script string) bool { - // @todo RBACv2 convert roles u.Roles()... - //u := auth.GetIdentityFromContext(ctx) - //return svc.permissions.Check(nil, script, permOpExec) != rbac.Deny + i := auth.GetIdentityFromContext(ctx) + + if svc.denyExec[script] == nil { + return true + } + + for _, roleID := range i.Roles() { + if _, has := svc.denyExec[script][roleID]; has { + return false + } + } + return true } @@ -451,7 +452,7 @@ func (svc *service) registerServerScripts(ctx context.Context, ss ...*ServerScri svc.explicit = make(map[string]map[string]bool) // Reset security - svc.permissions = rbac.RuleSet{} + svc.denyExec = make(map[string]map[uint64]bool) // reset the cache svc.userLookupCache = userLookupCacheMap{} @@ -473,11 +474,8 @@ func (svc *service) registerServerScripts(ctx context.Context, ss ...*ServerScri } if len(s.Errors) == 0 { - if sec, rr, err := svc.serverScriptSecurity(ctx, script); err != nil { + if err := svc.serverScriptSecurity(ctx, script, s); err != nil { s.Errors = append(s.Errors, err.Error()) - } else { - s.Security = sec - svc.permissions = append(svc.permissions, rr...) } } @@ -921,37 +919,12 @@ func (svc *service) registerClientScripts(ss ...*ClientScript) { // User and role caches (uc, rc args) hold list of users/roles // that were already loaded/checked // -func (svc *service) serverScriptSecurity(ctx context.Context, script *ServerScript) (sec *ScriptSecurity, rr rbac.RuleSet, err error) { +func (svc *service) serverScriptSecurity(ctx context.Context, script *ServerScript, s *Script) (err error) { if script.Security == nil { return } - var ( - // collectors for allow&deny rules - // we'll merge - allow = rbac.RuleSet{} - deny = rbac.RuleSet{} - - permRuleGenerator = func(script string, access rbac.Access, roles ...string) (rbac.RuleSet, error) { - out := make([]*rbac.Rule, len(roles)) - for i, role := range roles { - if r, err := svc.roles.FindByAny(ctx, role); err != nil { - return nil, fmt.Errorf("could not load security role: %s: %w", role, err) - } else { - out[i] = &rbac.Rule{ - RoleID: r.ID, - Resource: script, - Operation: permOpExec, - Access: access, - } - } - - } - return out, nil - } - ) - - sec = &ScriptSecurity{Security: script.Security} + sec := &ScriptSecurity{Security: script.Security} if sec.RunAs != "" { _, err = svc.userLookupCache.lookup( @@ -965,15 +938,20 @@ func (svc *service) serverScriptSecurity(ctx context.Context, script *ServerScri } } - if allow, err = permRuleGenerator(script.Name, rbac.Allow, script.Security.Allow...); err != nil { - return + denyExec := make(map[uint64]bool) + for _, role := range script.Security.Deny { + if r, err := svc.roles.FindByAny(ctx, role); err != nil { + return fmt.Errorf("could not load security role: %s: %w", role, err) + } else { + denyExec[r.ID] = true + } } - if deny, err = permRuleGenerator(script.Name, rbac.Deny, script.Security.Deny...); err != nil { - return + if len(denyExec) > 0 { + svc.denyExec[script.Name] = denyExec } - rr = append(allow, deny...) + s.Security = sec return } diff --git a/pkg/corredor/service_test.go b/pkg/corredor/service_test.go index b86976c6b..5ea6dea8b 100644 --- a/pkg/corredor/service_test.go +++ b/pkg/corredor/service_test.go @@ -2,15 +2,15 @@ package corredor import ( "context" + "testing" + "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/eventbus" "github.com/cortezaproject/corteza-server/pkg/logger" "github.com/cortezaproject/corteza-server/pkg/options" - "github.com/cortezaproject/corteza-server/pkg/rbac" "github.com/cortezaproject/corteza-server/system/types" "github.com/stretchr/testify/assert" "go.uber.org/zap" - "testing" ) type ( @@ -67,7 +67,7 @@ func TestFindOnManual(t *testing.T) { ctx = context.Background() svc = &service{ - permissions: rbac.RuleSet{}, + denyExec: make(map[string]map[uint64]bool), sScripts: ScriptSet{ &Script{ Name: "s1", @@ -164,12 +164,12 @@ func TestService_canExec(t *testing.T) { var ( a = assert.New(t) svc = &service{ - users: &mockUserSvc{user: &types.User{ID: 42, Email: "dummy@mo.ck", Handle: "dummy"}}, - roles: &mockRoleSvc{role: &types.Role{ID: 84, Handle: "role", Name: "ROLE"}}, - permissions: rbac.RuleSet{}, + users: &mockUserSvc{user: &types.User{ID: 42, Email: "dummy@mo.ck", Handle: "dummy"}}, + roles: &mockRoleSvc{role: &types.Role{ID: 84, Handle: "role", Name: "ROLE"}}, + denyExec: make(map[string]map[uint64]bool), } - ctx = auth.SetIdentityToContext(context.Background(), auth.NewIdentity(42, 84)) + ctx = auth.SetIdentityToContext(context.Background(), auth.Authenticated(42, 84)) script1 = &ServerScript{ Name: "s1", @@ -221,10 +221,8 @@ func TestService_canExec(t *testing.T) { svc.registerServerScripts(ctx, script1, script2, script3, script4) a.Len(svc.sScripts, 3) - a.Len(svc.permissions, 3) + a.Len(svc.denyExec, 2) - // @todo RBACv2 - t.Skip() a.True(svc.canExec(ctx, script1.Name)) a.False(svc.canExec(ctx, script2.Name)) } diff --git a/pkg/envoy/store/compose_record_marshal.go b/pkg/envoy/store/compose_record_marshal.go index e49e20438..c5b1ad6a2 100644 --- a/pkg/envoy/store/compose_record_marshal.go +++ b/pkg/envoy/store/compose_record_marshal.go @@ -13,7 +13,6 @@ import ( "github.com/cortezaproject/corteza-server/pkg/filter" "github.com/cortezaproject/corteza-server/store" systemTypes "github.com/cortezaproject/corteza-server/system/types" - "github.com/davecgh/go-spew/spew" ) var ( @@ -439,13 +438,11 @@ func (n *composeRecord) Encode(ctx context.Context, pl *payload) (err error) { rve = service.RecordValueUpdateOpCheck(ctx, pl.composeAccessControl, mod, rec.Values) if !rve.IsValid() { - spew.Dump("RecordValueUpdateOpCheck", rve) return rve } rve = service.RecordPreparer(ctx, pl.s, rvSanitizer, rvValidator, rvFormatter, mod, rec) if !rve.IsValid() { - spew.Dump("RecordPreparer", rve) return rve } diff --git a/pkg/errors/error.go b/pkg/errors/error.go index a95b9a948..609a19016 100644 --- a/pkg/errors/error.go +++ b/pkg/errors/error.go @@ -108,8 +108,14 @@ func (e *Error) Is(target error) bool { } // Unwrap is alias for errors.Unwrap so users can avoid importing both errors packages +// +// This function DOES NOT SUPPRESS errors if they are not wrapped! func Unwrap(err error) error { - return errors.Unwrap(err) + if err != nil && errors.Unwrap(err) != nil { + return errors.Unwrap(err) + } + + return err } // Is is alias for errors.Is so users can avoid importing both errors packages diff --git a/pkg/eventbus/eventbus.go b/pkg/eventbus/eventbus.go index c2df15cb4..4118a4948 100644 --- a/pkg/eventbus/eventbus.go +++ b/pkg/eventbus/eventbus.go @@ -152,3 +152,16 @@ func (b *eventbus) Unregister(ptrs ...uintptr) { delete(b.handlers, ptr) } } + +// UnregisterByResource removes one or more registered handlers that match the given resource +func (b *eventbus) UnregisterByResource(r string) { + b.l.Lock() + defer b.l.Unlock() + for p, h := range b.handlers { + if h.resourceTypes[r] { + delete(b.handlers, p) + continue + } + + } +} diff --git a/pkg/eventbus/handlers_test.go b/pkg/eventbus/handlers_test.go index 92a1fa87f..6816deca3 100644 --- a/pkg/eventbus/handlers_test.go +++ b/pkg/eventbus/handlers_test.go @@ -171,7 +171,7 @@ func TestHandlerHandler(t *testing.T) { a.False(passedthrough) a.Nil(ev.identity) - a.NoError(trSimple.Handle(auth.SetIdentityToContext(ctx, auth.NewIdentity(identity)), ev)) + a.NoError(trSimple.Handle(auth.SetIdentityToContext(ctx, auth.Authenticated(identity)), ev)) a.NotNil(ev.identity) a.Equal(identity, ev.identity.Identity()) diff --git a/pkg/expr/expr_types.go b/pkg/expr/expr_types.go index 01b76a447..cf09a4a03 100644 --- a/pkg/expr/expr_types.go +++ b/pkg/expr/expr_types.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "fmt" - "github.com/PaesslerAG/gval" "io" "io/ioutil" "net/http" @@ -14,6 +13,7 @@ import ( "strings" "time" + "github.com/PaesslerAG/gval" "github.com/cortezaproject/corteza-server/pkg/errors" "github.com/cortezaproject/corteza-server/pkg/handle" "github.com/spf13/cast" diff --git a/pkg/expr/func_arr.go b/pkg/expr/func_arr.go index 6cac2c26e..8f9d5482b 100644 --- a/pkg/expr/func_arr.go +++ b/pkg/expr/func_arr.go @@ -2,8 +2,9 @@ package expr import ( "fmt" - "github.com/PaesslerAG/gval" "reflect" + + "github.com/PaesslerAG/gval" ) func ArrayFunctions() []gval.Language { @@ -113,14 +114,22 @@ func count(arr interface{}, v ...interface{}) (count int, err error) { return } -// has finds any occurrence of the values in slice -func has(arr interface{}, v ...interface{}) (b bool, err error) { - if arr, err = toSlice(arr); err != nil { +// has finds any occurrence of the values in slice or key in a map +func has(arr interface{}, vv ...interface{}) (b bool, err error) { + arr = UntypedValue(arr) + + if isMap(arr) { + for _, v := range vv { + if reflect.ValueOf(arr).MapIndex(reflect.ValueOf(v)).IsValid() { + return true, nil + } + } + return } var c int - if c, err = count(arr, v...); err != nil { + if c, err = count(arr, vv...); err != nil { return } diff --git a/pkg/expr/func_arr_test.go b/pkg/expr/func_arr_test.go index bbfd14b4c..83a518a36 100644 --- a/pkg/expr/func_arr_test.go +++ b/pkg/expr/func_arr_test.go @@ -312,6 +312,16 @@ func Test_has(t *testing.T) { val: []interface{}{0.1, 1.1}, expect: false, }, + { + arr: map[string]interface{}{"a": 1}, + val: []interface{}{"a", "b"}, + expect: true, + }, + { + arr: map[string]interface{}{"a": 1}, + val: []interface{}{"b"}, + expect: false, + }, } for p, tc := range tcc { diff --git a/pkg/expr/func_generic.go b/pkg/expr/func_generic.go index c80d6c1d3..280cdc341 100644 --- a/pkg/expr/func_generic.go +++ b/pkg/expr/func_generic.go @@ -106,6 +106,10 @@ func isSlice(v interface{}) bool { return reflect.TypeOf(v).Kind() == reflect.Slice || reflect.TypeOf(v).Kind() == reflect.Array } +func isMap(v interface{}) bool { + return reflect.TypeOf(v).Kind() == reflect.Map +} + // toArray removes expr types (if wrapped) and checks if the variable is slice // internal only // diff --git a/pkg/expr/parser.go b/pkg/expr/parser.go index 5e9b5e1ae..ee91a103c 100644 --- a/pkg/expr/parser.go +++ b/pkg/expr/parser.go @@ -3,6 +3,7 @@ package expr import ( "context" "fmt" + "github.com/PaesslerAG/gval" ) @@ -85,7 +86,7 @@ func Parser(ll ...gval.Language) gval.Language { func AllFunctions() []gval.Language { ff := make([]gval.Language, 0, 100) - //ff = append(ff, GenericFunctions()...) + ff = append(ff, GenericFunctions()...) ff = append(ff, StringFunctions()...) ff = append(ff, JsonFunctions()...) ff = append(ff, NumericFunctions()...) diff --git a/pkg/messagebus/rbac.go b/pkg/messagebus/rbac.go new file mode 100644 index 000000000..9deeecbce --- /dev/null +++ b/pkg/messagebus/rbac.go @@ -0,0 +1,32 @@ +package messagebus + +import ( + "strconv" +) + +const ( + // QueueResourceType + // Using system as component (even if it is currently parked under pkg/messagebus) + // and queue as name (even if is currently named queue-settings + QueueResourceType = "corteza::system:queue" +) + +// RbacResource +// +// Not following the naming pattern here (queue settings is queue) +func (r QueueSettings) RbacResource() string { + return QueueRbacResource(r.ID) +} + +// QueueRbacResource returns string representation of RBAC resource for Queue +func QueueRbacResource(id uint64) string { + out := QueueResourceType + if id != 0 { + out += "/" + strconv.FormatUint(id, 10) + } else { + out += "/*" + } + + return out + +} diff --git a/pkg/options/RBAC.gen.go b/pkg/options/RBAC.gen.go index 827c1e224..6674c03a7 100644 --- a/pkg/options/RBAC.gen.go +++ b/pkg/options/RBAC.gen.go @@ -20,7 +20,6 @@ type ( // RBAC initializes and returns a RBACOpt with default values func RBAC() (o *RBACOpt) { o = &RBACOpt{ - ServiceUser: "corteza", BypassRoles: "super-admin", AuthenticatedRoles: "authenticated", AnonymousRoles: "anonymous", diff --git a/pkg/provision/migrations_202103_templates.go b/pkg/provision/migrations_202103_templates.go index b3fad8e4f..f5e04e9c6 100644 --- a/pkg/provision/migrations_202103_templates.go +++ b/pkg/provision/migrations_202103_templates.go @@ -3,7 +3,6 @@ package provision import ( "context" "fmt" - "time" "github.com/cortezaproject/corteza-server/pkg/errors" "github.com/cortezaproject/corteza-server/pkg/id" @@ -82,7 +81,7 @@ func migrateEmailTemplates(ctx context.Context, log *zap.Logger, s store.Storer) } tmpl.ID = id.Next() - tmpl.CreatedAt = time.Now() + tmpl.CreatedAt = *now() tmpl.Template = sval.String() err = store.CreateTemplate(ctx, s, tmpl) diff --git a/pkg/provision/migrations_202106_rbac.go b/pkg/provision/migrations_202106_rbac.go new file mode 100644 index 000000000..8767dec73 --- /dev/null +++ b/pkg/provision/migrations_202106_rbac.go @@ -0,0 +1,135 @@ +package provision + +import ( + "context" + "strconv" + "strings" + + composeTypes "github.com/cortezaproject/corteza-server/compose/types" + federationTypes "github.com/cortezaproject/corteza-server/federation/types" + "github.com/cortezaproject/corteza-server/pkg/rbac" + "github.com/cortezaproject/corteza-server/store" + systemTypes "github.com/cortezaproject/corteza-server/system/types" + "go.uber.org/zap" +) + +// MigrateOperations creates system roles +func migratePre202106RbacRules(ctx context.Context, log *zap.Logger, s store.Storer) error { + return store.Tx(ctx, s, func(ctx context.Context, s store.Storer) error { + rr, _, err := store.SearchRbacRules(ctx, s, rbac.RuleFilter{}) + if err != nil { + return err + } + + log.Info("migrating RBAC rules to new format", zap.Int("rules", len(rr))) + for _, r := range rr { + var ( + cr = *r + action = migratePre202106RbacRule(r) + ) + + if action != 0 { + err = store.DeleteRbacRule(ctx, s, &cr) + if err != nil { + return err + } + } + + if action == 1 { + err = store.CreateRbacRule(ctx, s, r) + if err != nil { + return err + } + } + } + + return nil + }) +} + +// 0 - no action +// -1 - remove +// 1 - update +func migratePre202106RbacRule(r *rbac.Rule) (op int) { + const ( + nsSep = "::" + nsDef = "corteza" + ) + + if strings.Contains(r.Resource, nsSep) { + return + } + + if r.Resource == "" { + return -1 + } + + // split old format + parts := strings.SplitN(r.Resource, ":", 3) + + switch { + case parts[0] == "messaging": + return -1 + case len(parts) > 1 && parts[1] == "automation-script": + return -1 + case len(parts) == 1: + r.Resource = nsDef + nsSep + strings.Join(parts[:1], ":") + default: + r.Resource = nsDef + nsSep + strings.Join(parts[:2], ":") + } + + op = 1 + + rType := rbac.ResourceType(r.Resource) + switch { + case rType == systemTypes.UserResourceType && strings.HasPrefix(r.Operation, "unmask."): + // flipping terms in user unmask operations + r.Operation = strings.TrimPrefix(r.Operation, "unmask.") + ".unmask" + + case rType == "corteza::federation:module": + // fed. module resource was split into two resources - exposed & shared + if r.Operation == "manage" { + rType = federationTypes.ExposedModuleResourceType + } else if r.Operation == "map" { + rType = federationTypes.SharedModuleResourceType + } else { + return -1 + } + + case rType == composeTypes.ModuleResourceType && strings.HasPrefix(r.Operation, "record.") && r.Operation != "record.create": + // change resource type from module to record on record read, delete, update operations & remove the prefix + rType = composeTypes.RecordResourceType + r.Operation = strings.TrimPrefix(r.Operation, "record.") + } + + if len(parts) == 3 { + var ID, _ = strconv.ParseUint(parts[2], 10, 64) + + // exceptions with nested references + switch rType { + case composeTypes.ModuleFieldResourceType: + r.Resource = composeTypes.ModuleFieldRbacResource(0, 0, ID) + case composeTypes.ModuleResourceType: + r.Resource = composeTypes.ModuleRbacResource(0, ID) + case composeTypes.RecordResourceType: + // ID belongs to module! + r.Resource = composeTypes.RecordRbacResource(0, ID, 0) + case composeTypes.ChartResourceType: + r.Resource = composeTypes.ChartRbacResource(0, ID) + case composeTypes.PageResourceType: + r.Resource = composeTypes.PageRbacResource(0, ID) + default: + r.Resource = rType + "/" + func() string { + if ID == 0 { + return "*" + } + return parts[2] + }() + } + + } else { + r.Resource = rType + "/" + } + + return +} diff --git a/pkg/provision/migrations_202106_rbac_test.go b/pkg/provision/migrations_202106_rbac_test.go new file mode 100644 index 000000000..47fad3da4 --- /dev/null +++ b/pkg/provision/migrations_202106_rbac_test.go @@ -0,0 +1,32 @@ +package provision + +import ( + "testing" + + "github.com/cortezaproject/corteza-server/pkg/rbac" + "github.com/stretchr/testify/require" +) + +func Test_migratePre202106RbacRule(t *testing.T) { + tcc := []struct { + wantOp int + rule *rbac.Rule + wantRule *rbac.Rule + }{ + {-1, rbac.AllowRule(0, "messaging", "whatever"), nil}, + {-1, rbac.AllowRule(0, "foo:automation-script", "whatever"), nil}, + {1, rbac.AllowRule(0, "abc", "whatever"), rbac.AllowRule(0, "corteza::abc/", "whatever")}, + {1, rbac.AllowRule(0, "federation:module", "map"), rbac.AllowRule(0, "corteza::federation:shared-module/", "map")}, + {1, rbac.AllowRule(0, "federation:module", "manage"), rbac.AllowRule(0, "corteza::federation:exposed-module/", "manage")}, + {1, rbac.AllowRule(0, "compose:module:234", "record.read"), rbac.AllowRule(0, "corteza::compose:record/*/234/*", "read")}, + {1, rbac.AllowRule(0, "compose:module-field:234", "op"), rbac.AllowRule(0, "corteza::compose:module-field/*/*/234", "op")}, + } + for _, tc := range tcc { + t.Run(tc.rule.String(), func(t *testing.T) { + require.Equal(t, tc.wantOp, migratePre202106RbacRule(tc.rule)) + if tc.wantRule != nil { + require.Equal(t, tc.wantRule.String(), tc.rule.String()) + } + }) + } +} diff --git a/pkg/provision/migrations_202106_roles.go b/pkg/provision/migrations_202106_roles.go new file mode 100644 index 000000000..2237b4a13 --- /dev/null +++ b/pkg/provision/migrations_202106_roles.go @@ -0,0 +1,68 @@ +package provision + +import ( + "context" + + "github.com/cortezaproject/corteza-server/pkg/id" + "github.com/cortezaproject/corteza-server/store" + "go.uber.org/zap" +) + +func migratePre202106Roles(ctx context.Context, log *zap.Logger, s store.Storer) (err error) { + const ( + obsoleteEveryoneID uint64 = 1 + obsoleteAdminsID uint64 = 2 + ) + + log.Info("migrating pre-2021.6 roles") + m, err := loadRoles(ctx, s) + if err != nil { + return + } + + // let's see if everyone role is still here: + if m["everyone"] != nil && m["everyone"].ID == obsoleteEveryoneID { + log.Info("migrating 'everyone' role to new ID") + + // everyone role still present and it is using "hardcoded" ID + // we can remove it + if err = store.DeleteRoleByID(ctx, s, obsoleteEveryoneID); err != nil { + return + } + + // transfer all rbac rules + if err = s.TransferRbacRules(ctx, obsoleteEveryoneID, m["authenticated"].ID); err != nil { + return + } + } + + // let's see if admin role is still here: + if m["admins"] != nil && m["admins"].ID == obsoleteAdminsID { + log.Info("migrating 'admins' role to new ID") + + // everyone role still present and it is using "hardcoded" ID + // we can remove it + m["admins"].ID = id.Next() + m["admins"].UpdatedAt = now() + + if err = store.DeleteRoleByID(ctx, s, obsoleteAdminsID); err != nil { + return + } + + if err = store.CreateRole(ctx, s, m["admins"]); err != nil { + return + } + + // transfer all rbac rules + if err = s.TransferRoleMembers(ctx, obsoleteAdminsID, m["admins"].ID); err != nil { + return + } + + // transfer all rbac rules + if err = s.TransferRbacRules(ctx, obsoleteAdminsID, m["admins"].ID); err != nil { + return + } + } + + return +} diff --git a/pkg/provision/partial.go b/pkg/provision/partial.go index 4a8cb186c..0e10bb7e5 100644 --- a/pkg/provision/partial.go +++ b/pkg/provision/partial.go @@ -28,7 +28,7 @@ func provisionPartialAuthClients(ctx context.Context, s store.Storer, log *zap.L } for _, r := range set { - if r.Resource == types.AuthClientRbacResourceSchema { + if rbac.ResourceType(r.Resource) == types.AuthClientResourceType { return false } } diff --git a/pkg/provision/provision.go b/pkg/provision/provision.go index df49d9143..b634f3f35 100644 --- a/pkg/provision/provision.go +++ b/pkg/provision/provision.go @@ -13,6 +13,14 @@ import ( "go.uber.org/zap" ) +var ( + // wrapper around time.Now() that will aid service testing + now = func() *time.Time { + c := time.Now().Round(time.Second) + return &c + } +) + func Run(ctx context.Context, log *zap.Logger, s store.Storer, provisionOpt options.ProvisionOpt, authOpt options.AuthOpt) error { log = log.Named("provision") @@ -20,6 +28,8 @@ func Run(ctx context.Context, log *zap.Logger, s store.Storer, provisionOpt opti // Migrations: func() error { return migrateApplications(ctx, s) }, func() error { return migrateEmailTemplates(ctx, log.Named("email-templates"), s) }, + func() error { return migratePre202106Roles(ctx, log.Named("pre-202106-roles"), s) }, + func() error { return migratePre202106RbacRules(ctx, log.Named("pre-202106-rbac-rules"), s) }, // Config (full & partial) func() error { return importConfig(ctx, log.Named("config"), s, provisionOpt.Path) }, @@ -67,7 +77,7 @@ func defaultAuthClient(ctx context.Context, log *zap.Logger, s store.AuthClients Trusted: true, Security: &types.AuthClientSecurity{}, Labels: nil, - CreatedAt: time.Now(), + CreatedAt: *now(), } _, err := store.LookupAuthClientByHandle(ctx, s, c.Handle) diff --git a/pkg/provision/roles.go b/pkg/provision/roles.go index 5a82ac7c5..dfd0703a2 100644 --- a/pkg/provision/roles.go +++ b/pkg/provision/roles.go @@ -3,7 +3,6 @@ package provision import ( "context" "fmt" - "time" "github.com/cortezaproject/corteza-server/pkg/filter" "github.com/cortezaproject/corteza-server/pkg/id" @@ -14,21 +13,12 @@ import ( // SystemRoles creates system roles func SystemRoles(ctx context.Context, log *zap.Logger, s store.Storer) (rr []*types.Role, err error) { - const ( - obsoleteEveryoneID uint64 = 1 - obsoleteAdminsID uint64 = 2 - ) - - var ( - now = time.Now().Round(time.Second) - ) - rr = types.RoleSet{ &types.Role{ Name: "Super administrator", Handle: "super-admin", Meta: &types.RoleMeta{ - Description: "Super admin is a 'bypass' role that allows all actions it's members", + Description: "Super admin is a 'bypass' role that auto-allows all operations to it's members", Context: nil, }, }, @@ -62,7 +52,7 @@ func SystemRoles(ctx context.Context, log *zap.Logger, s store.Storer) (rr []*ty log.Info("creating role", zap.String("handle", r.Handle)) // this is a new role r.ID = id.Next() - r.CreatedAt = now + r.CreatedAt = *now() m[r.Handle] = r } else { @@ -82,50 +72,6 @@ func SystemRoles(ctx context.Context, log *zap.Logger, s store.Storer) (rr []*ty return nil, fmt.Errorf("failed to provision roles: %w", err) } - // let's see if everyone role is still here: - if m["everyone"] != nil && m["everyone"].ID == obsoleteEveryoneID { - log.Info("migrating 'everyone' role") - - // everyone role still present and it is using "hardcoded" ID - // we can remove it - if err = store.DeleteRoleByID(ctx, s, obsoleteEveryoneID); err != nil { - return - } - - // transfer all rbac rules - if err = s.TransferRbacRules(ctx, obsoleteEveryoneID, m["authenticated"].ID); err != nil { - return - } - } - - // let's see if everyone role is still here: - if m["admins"] != nil && m["admins"].ID == obsoleteAdminsID { - log.Info("migrating 'admins' role") - - // everyone role still present and it is using "hardcoded" ID - // we can remove it - m["admins"].ID = id.Next() - m["admins"].UpdatedAt = &now - - if err = store.DeleteRoleByID(ctx, s, obsoleteAdminsID); err != nil { - return - } - - if err = store.CreateRole(ctx, s, m["admins"]); err != nil { - return - } - - // transfer all rbac rules - if err = s.TransferRoleMembers(ctx, obsoleteAdminsID, m["admins"].ID); err != nil { - return - } - - // transfer all rbac rules - if err = s.TransferRbacRules(ctx, obsoleteAdminsID, m["admins"].ID); err != nil { - return - } - } - return } diff --git a/pkg/provision/users.go b/pkg/provision/users.go index 98b701072..e0932ab03 100644 --- a/pkg/provision/users.go +++ b/pkg/provision/users.go @@ -3,7 +3,6 @@ package provision import ( "context" "fmt" - "time" "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/filter" @@ -15,10 +14,6 @@ import ( // SystemUsers creates or updates system users func SystemUsers(ctx context.Context, log *zap.Logger, s store.Users) (uu []*types.User, err error) { - var ( - now = time.Now().Round(time.Second) - ) - uu = types.UserSet{ &types.User{ Email: "provision@corteza.local", @@ -40,7 +35,7 @@ func SystemUsers(ctx context.Context, log *zap.Logger, s store.Users) (uu []*typ }, } - m, err := loadUsers(ctx, s) + m, err := loadSystemUsers(ctx, s) if err != nil { return } @@ -51,15 +46,25 @@ func SystemUsers(ctx context.Context, log *zap.Logger, s store.Users) (uu []*typ log.Info("creating user", zap.String("handle", u.Handle)) // this is a new user u.ID = id.Next() - u.CreatedAt = now + u.CreatedAt = *now() if err := store.UpsertUser(ctx, s, u); err != nil { return nil, fmt.Errorf("failed to provision user %s: %w", u.Handle, err) } } else { + // There is no need to update system users if they are unchanged + if m[u.Handle].UpdatedAt == nil && + m[u.Handle].SuspendedAt == nil && + m[u.Handle].DeletedAt == nil { + continue + } + + // Make sure all values are as they should be u.ID = m[u.Handle].ID + u.CreatedAt = m[u.Handle].CreatedAt u.Email = m[u.Handle].Email u.Name = m[u.Handle].Name + u.UpdatedAt = nil u.SuspendedAt = nil u.DeletedAt = nil @@ -73,11 +78,12 @@ func SystemUsers(ctx context.Context, log *zap.Logger, s store.Users) (uu []*typ return } -func loadUsers(ctx context.Context, s store.Users) (m map[string]*types.User, err error) { +func loadSystemUsers(ctx context.Context, s store.Users) (m map[string]*types.User, err error) { var ( f = types.UserFilter{ Suspended: filter.StateInclusive, Deleted: filter.StateInclusive, + Kind: types.SystemUser, } ) diff --git a/pkg/rbac/resource.go b/pkg/rbac/resource.go index 235a3304a..4852ba6bf 100644 --- a/pkg/rbac/resource.go +++ b/pkg/rbac/resource.go @@ -15,18 +15,29 @@ type ( } ) -func ResourceSchema(r string) string { - i := strings.Index(r, ":") - if i < 0 { - return "" - } +const ( + nsSep = "::" + pathSep = "/" + wildcard = "*" +) - return r[:i] +// ResourceType extracts 1st part of the resource +// +// ns::cmp:res/c returns ns::cmp:res +// ns::cmp:res/ returns ns::cmp:res +// ns::cmp:res returns ns::cmp:res +func ResourceType(r string) string { + if p := strings.Index(r, pathSep); p > 0 { + return r[:p] + } else { + return r + } } func matchResource(matcher, resource string) (m bool) { - if level(matcher) == 0 { - return matcher == resource + if matcher == resource { + // if resources match make sure no wildcards are resent + return strings.Index(resource, wildcard) == -1 } m, _ = path.Match(matcher, resource) @@ -37,6 +48,23 @@ func matchResource(matcher, resource string) (m bool) { // In a nutshell, level indicates number of wildcard characters // // More defined resources use less wildcards and are on a lower level -func level(r string) int { - return strings.Count(r, string("*")) +func level(r string) (score int) { + var nl bool + for l := len(r) - 1; l > strings.Index(r, pathSep); l-- { + switch r[l] { + case wildcard[0]: + // nop + case pathSep[0]: + // found next resource reference level + score *= 10 + nl = false + default: + if !nl { + score += 1 + nl = true + } + } + } + + return } diff --git a/pkg/rbac/resource_test.go b/pkg/rbac/resource_test.go index c5b544916..c5abe78a7 100644 --- a/pkg/rbac/resource_test.go +++ b/pkg/rbac/resource_test.go @@ -1,10 +1,34 @@ package rbac import ( - "github.com/stretchr/testify/require" "testing" + + "github.com/stretchr/testify/require" ) +func TestResourceType(t *testing.T) { + var ( + tcc = []struct { + in string + exp string + }{ + {"a:b/c/d", "a:b"}, + {"a:b/c", "a:b"}, + {"a:b/", "a:b"}, + {"a:b", "a:b"}, + {"a/", "a"}, + {"a", "a"}, + {"", ""}, + } + ) + + for _, tc := range tcc { + t.Run(tc.in, func(t *testing.T) { + require.Equal(t, tc.exp, ResourceType(tc.in)) + }) + } +} + func TestResourceMatch(t *testing.T) { var ( tcc = []struct { @@ -12,11 +36,11 @@ func TestResourceMatch(t *testing.T) { r string e bool }{ - {"a:b:c", "a:b:c", true}, - {"a:b:*", "a:b:c", true}, - {"a:*:*", "a:b:c", true}, - {"*:*:*", "a:b:c", true}, - {"a:*:*", "1:2:3", false}, + {"::corteza:test/a/b/c", "::corteza:test/a/b/c", true}, + {"::corteza:test/a/b/*", "::corteza:test/a/b/c", true}, + {"::corteza:test/a/*/*", "::corteza:test/a/b/c", true}, + {"::corteza:test/*/*/*", "::corteza:test/a/b/c", true}, + {"::corteza:test/a/*/*", "::corteza:test/1/2/3", false}, } ) @@ -26,3 +50,47 @@ func TestResourceMatch(t *testing.T) { }) } } + +//cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz +//Benchmark_MatchResource100-16 7353837 157.0 ns/op +//Benchmark_MatchResource1000-16 6868928 166.0 ns/op +//Benchmark_MatchResource10000-16 7373701 164.8 ns/op +//Benchmark_MatchResource100000-16 7556944 156.5 ns/op +//Benchmark_MatchResource1000000-16 7445456 157.8 ns/op +func benchmarkMatchResource(b *testing.B, c int) { + b.StartTimer() + + for n := 0; n < b.N; n++ { + matchResource("corteza::test/a/1/1/1", "corteza::test/a/1/1/1") + matchResource("corteza::test/a/*/*/1", "corteza::test/a/1/1/1") + } + + b.StopTimer() +} + +func Benchmark_MatchResource100(b *testing.B) { benchmarkMatchResource(b, 100) } +func Benchmark_MatchResource1000(b *testing.B) { benchmarkMatchResource(b, 1000) } +func Benchmark_MatchResource10000(b *testing.B) { benchmarkMatchResource(b, 10000) } +func Benchmark_MatchResource100000(b *testing.B) { benchmarkMatchResource(b, 100000) } +func Benchmark_MatchResource1000000(b *testing.B) { benchmarkMatchResource(b, 1000000) } + +func TestLevel(t *testing.T) { + var ( + tcc = []struct { + r string + l int + }{ + {"corteza::test/a/b/c", 111}, + {"corteza::test/a/b/*", 11}, + {"corteza::test/a/*/*", 1}, + {"corteza::test/*/*/*", 0}, + {"corteza::test/a/*/123", 101}, + } + ) + + for _, tc := range tcc { + t.Run(tc.r, func(t *testing.T) { + require.Equal(t, tc.l, level(tc.r)) + }) + } +} diff --git a/pkg/rbac/roles.go b/pkg/rbac/roles.go index 959974c1f..623983b10 100644 --- a/pkg/rbac/roles.go +++ b/pkg/rbac/roles.go @@ -2,6 +2,11 @@ package rbac import ( "github.com/cortezaproject/corteza-server/pkg/slice" + "go.uber.org/zap" +) + +const ( + roleKinds = 5 ) type ( @@ -19,11 +24,14 @@ type ( kind roleKind check ctxRoleCheckFn + + // compatible resource types + crtypes map[string]bool } roleKind int - partRoles []map[uint64]bool + partRoles [roleKinds]map[uint64]bool ) const ( @@ -34,6 +42,23 @@ const ( BypassRole ) +func (k roleKind) String() string { + switch k { + case BypassRole: + return "bypass" + case ContextRole: + return "context" + case CommonRole: + return "common" + case AuthenticatedRole: + return "authenticated" + case AnonymousRole: + return "anonymous" + default: + panic("unknown role kind") + } +} + func (k roleKind) Make(id uint64, handle string) *Role { return &Role{ kind: k, @@ -42,18 +67,19 @@ func (k roleKind) Make(id uint64, handle string) *Role { } } -func MakeContextRole(id uint64, handle string, fn ctxRoleCheckFn) *Role { +func MakeContextRole(id uint64, handle string, fn ctxRoleCheckFn, tt ...string) *Role { return &Role{ - kind: ContextRole, - id: id, - handle: handle, - check: fn, + kind: ContextRole, + id: id, + handle: handle, + check: fn, + crtypes: slice.ToStringBoolMap(tt), } } // partitions roles by kind func partitionRoles(rr ...*Role) partRoles { - out := make([]map[uint64]bool, len(roleKindsByPriority())) + out := [roleKinds]map[uint64]bool{} for _, r := range rr { if out[r.kind] == nil { out[r.kind] = make(map[uint64]bool) @@ -65,6 +91,20 @@ func partitionRoles(rr ...*Role) partRoles { return out } +func (p partRoles) LogFields() (ff []zap.Field) { + for _, k := range []roleKind{BypassRole, ContextRole, CommonRole, AuthenticatedRole, AnonymousRole} { + ii := make([]uint64, 0, len(p[k])) + for r := range p[k] { + ii = append(ii, r) + } + + ff = append(ff, zap.Uint64s(k.String(), ii)) + } + + return +} + +// counts roles per type func statRoles(rr ...*Role) (stats map[roleKind]int) { stats = make(map[roleKind]int) for _, r := range rr { @@ -74,47 +114,35 @@ func statRoles(rr ...*Role) (stats map[roleKind]int) { return } -// Returns slice of role types by priority -// -// Priority is important here. We want to have -// stable RBAC check behaviour and ability -// to override allow/deny depending on how niche the role (type) is: -// - bypass always stake precedence -// - context (eg owners) are more niche than common -func roleKindsByPriority() []roleKind { - return []roleKind{ - BypassRole, - ContextRole, - CommonRole, - AuthenticatedRole, - AnonymousRole, - } -} - // compare list of session roles (ids) with preloaded roles and calculate the final list -func getContextRoles(sRoles []uint64, res Resource, preloadedRoles []*Role) (out partRoles) { +func getContextRoles(s Session, res Resource, preloadedRoles []*Role) (out partRoles) { var ( - mm = slice.ToUint64BoolMap(sRoles) - attr = make(map[string]interface{}) + mm = slice.ToUint64BoolMap(s.Roles()) + scope = make(map[string]interface{}) ) - if ar, ok := res.(resourceDicter); ok { + if d, ok := res.(resourceDicter); ok { // if resource implements Dict() fn, we can use it to - // collect attributes, used for expr. evaluation and contextual role gathering - attr = ar.Dict() + // collect attributes, used for expression evaluation and contextual role gathering + scope["resource"] = d.Dict() } - attr["userID"] = 0 // @todo RBACv2 + scope["userID"] = s.Identity() - out = make([]map[uint64]bool, len(roleKindsByPriority())) + out = [roleKinds]map[uint64]bool{} for _, r := range preloadedRoles { if r.kind == ContextRole { + if len(r.crtypes) == 0 || !r.crtypes[ResourceType(res.RbacResource())] { + // resource type not compatible with this contextual role + continue + } + if r.check == nil { // expression not defined, skip contextual role continue } - if !r.check(attr) { + if !r.check(scope) { // add role to the list ONLY of expression evaluated true continue } diff --git a/pkg/rbac/roles_test.go b/pkg/rbac/roles_test.go index 7554049e3..2fd04cd1e 100644 --- a/pkg/rbac/roles_test.go +++ b/pkg/rbac/roles_test.go @@ -1,10 +1,19 @@ package rbac import ( - "github.com/stretchr/testify/require" "testing" + + "github.com/stretchr/testify/require" ) +type ( + testResource string +) + +func (t testResource) RbacResource() string { + return string(t) +} + func Test_partitionRoles(t *testing.T) { var ( req = require.New(t) @@ -40,6 +49,8 @@ func Test_getContextRoles(t *testing.T) { } } + tres = testResource("testResource") + tcc = []struct { name string sessionRoles []uint64 @@ -50,25 +61,26 @@ func Test_getContextRoles(t *testing.T) { { "existing role", []uint64{1}, - nil, + tres, []*Role{{id: 1, kind: BypassRole}}, []*Role{{id: 1, kind: BypassRole}}, }, { "missing role", []uint64{2}, - nil, + tres, []*Role{{id: 1, kind: BypassRole}}, []*Role{}, }, { "dynamic role", []uint64{1, 2}, - nil, + tres, []*Role{ {id: 1, kind: BypassRole}, - {id: 2, kind: ContextRole, check: dyCheck(true)}, - {id: 3, kind: ContextRole, check: dyCheck(false)}, + {id: 2, kind: ContextRole, check: dyCheck(true), crtypes: map[string]bool{tres.RbacResource(): true}}, + {id: 3, kind: ContextRole, check: dyCheck(false), crtypes: map[string]bool{tres.RbacResource(): true}}, + {id: 4, kind: ContextRole, check: dyCheck(true)}, }, []*Role{{id: 1, kind: BypassRole}, {id: 2, kind: ContextRole}}, }, @@ -81,7 +93,7 @@ func Test_getContextRoles(t *testing.T) { req = require.New(t) ) - req.Equal(partitionRoles(tc.output...), getContextRoles(tc.sessionRoles, tc.res, tc.preloadRoles)) + req.Equal(partitionRoles(tc.output...), getContextRoles(&session{rr: tc.sessionRoles}, tc.res, tc.preloadRoles)) }) } } diff --git a/pkg/rbac/rule.go b/pkg/rbac/rule.go index d0376e762..69b6ba7f0 100644 --- a/pkg/rbac/rule.go +++ b/pkg/rbac/rule.go @@ -18,6 +18,7 @@ type ( RuleSet []*Rule + // OptRuleSet RBAC rule index (operation / role ID / rules) OptRuleSet map[string]map[uint64]RuleSet ) @@ -49,30 +50,10 @@ func indexRules(rules []*Rule) OptRuleSet { return i } -func filterRules(rules []*Rule, roles map[uint64]bool, op string) (out []*Rule) { - if len(roles) == 0 { - return - } - - for _, r := range rules { - if !roles[r.RoleID] { - continue - } - - if op != r.Operation { - continue - } - - out = append(out, r) - } - - return -} - func (set RuleSet) Len() int { return len(set) } func (set RuleSet) Swap(i, j int) { set[i], set[j] = set[j], set[i] } func (set RuleSet) Less(i, j int) bool { - return level(set[i].Resource) < level(set[j].Resource) + return level(set[i].Resource) > level(set[j].Resource) } // AllowRule helper func to create allow rule diff --git a/pkg/rbac/rule_test.go b/pkg/rbac/rule_test.go new file mode 100644 index 000000000..11db1a2c5 --- /dev/null +++ b/pkg/rbac/rule_test.go @@ -0,0 +1,42 @@ +package rbac + +import ( + "sort" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/stretchr/testify/require" +) + +func TestRuleSetSort(t *testing.T) { + var ( + req = require.New(t) + rr = RuleSet{ + {Resource: ":::/*/*/*"}, + {Resource: ":::/1/2/3"}, + {Resource: ":::/1/2/*"}, + {Resource: ":::/1/*/*"}, + {Resource: ":::/1/*/3"}, + {Resource: ":::/*/*/3"}, + {Resource: ":::/*/2/*"}, + } + + c int = 0 + i = func() int { + c++ + return c - 1 + } + ) + + req.NotNil(rr) + sort.Sort(rr) + spew.Dump(rr) + c = i() + req.Equal(":::/1/2/3", rr[i()].Resource) + req.Equal(":::/1/*/3", rr[i()].Resource) + req.Equal(":::/*/*/3", rr[i()].Resource) + req.Equal(":::/1/2/*", rr[i()].Resource) + req.Equal(":::/*/2/*", rr[i()].Resource) + req.Equal(":::/1/*/*", rr[i()].Resource) + req.Equal(":::/*/*/*", rr[i()].Resource) +} diff --git a/pkg/rbac/ruleset_checks.go b/pkg/rbac/ruleset_checks.go index 5d08a1257..6d0034134 100644 --- a/pkg/rbac/ruleset_checks.go +++ b/pkg/rbac/ruleset_checks.go @@ -1,33 +1,30 @@ package rbac -func (set RuleSet) Check(rolesByKind partRoles, res, op string) Access { - for _, kind := range roleKindsByPriority() { - if len(rolesByKind[kind]) == 0 { - continue - } - - if kind == BypassRole { - return Allow - } - - access := checkRulesByResource(filterRules(set, rolesByKind[kind], op), res, op) - if access != Inherit { - return access - } +func check(indexedRules OptRuleSet, rolesByKind partRoles, op, res string) Access { + if member(rolesByKind, AnonymousRole) && len(rolesByKind) > 1 { + // Integrity check; when user is member of anonymous role + // should not be member of any other type of role + return Deny } - return Inherit -} + if member(rolesByKind, BypassRole) { + // if user has at least one bypass role, we allow access + return Allow + } -func checkOptimised(indexedRules OptRuleSet, rolesByKind partRoles, res, op string) Access { - if len(rolesByKind) == 0 || len(indexedRules) == 0 { + if len(indexedRules) == 0 { + // no rules no access return Inherit } var rules []*Rule - // looping through all role kinds - for _, kind := range roleKindsByPriority() { + // Priority is important here. We want to have + // stable RBAC check behaviour and ability + // to override allow/deny depending on how niche the role (type) is: + // - context (eg owners) are more niche than common + // - rules for common roles are more important than + for _, kind := range []roleKind{ContextRole, CommonRole, AuthenticatedRole, AnonymousRole} { // no roles if this kind if len(rolesByKind[kind]) == 0 { continue @@ -46,7 +43,7 @@ func checkOptimised(indexedRules OptRuleSet, rolesByKind partRoles, res, op stri rules = append(rules, r...) } - access := checkRulesByResource(rules, res, op) + access := checkRulesByResource(rules, op, res) if access != Inherit { return access } @@ -58,9 +55,9 @@ func checkOptimised(indexedRules OptRuleSet, rolesByKind partRoles, res, op stri // Check given resource match and operation on all given rules // // Function expects rules, sorted by level! -func checkRulesByResource(set []*Rule, res, op string) Access { +func checkRulesByResource(set []*Rule, op, res string) Access { for _, r := range set { - if !matchResource(res, r.Resource) { + if !matchResource(r.Resource, res) { continue } @@ -75,3 +72,14 @@ func checkRulesByResource(set []*Rule, res, op string) Access { return Inherit } + +// at least one of the roles must be set to true +func member(r partRoles, k roleKind) bool { + for _, is := range r[k] { + if is { + return true + } + } + + return false +} diff --git a/pkg/rbac/ruleset_checks_test.go b/pkg/rbac/ruleset_checks_test.go index 571c92909..876ecaff3 100644 --- a/pkg/rbac/ruleset_checks_test.go +++ b/pkg/rbac/ruleset_checks_test.go @@ -61,11 +61,17 @@ func Test_check(t *testing.T) { for _, c := range cc { t.Run(c.name, func(t *testing.T) { - require.Equal(t, c.exp, c.set.Check(partitionRoles(c.rr...), c.res, c.op)) + require.Equal(t, c.exp.String(), check(indexRules(c.set), partitionRoles(c.rr...), c.op, c.res).String()) }) } } +//cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz +//Benchmark_Check100-16 15626196 88.85 ns/op +//Benchmark_Check1000-16 15976252 74.09 ns/op +//Benchmark_Check10000-16 15025586 78.12 ns/op +//Benchmark_Check100000-16 13760616 84.70 ns/op +//Benchmark_Check1000000-16 2602420 415.8 ns/op func benchmarkCheck(b *testing.B, c int) { var ( // resting with 50 roles @@ -75,9 +81,9 @@ func benchmarkCheck(b *testing.B, c int) { &Role{id: 1, kind: CommonRole}, &Role{id: 2, kind: CommonRole}, &Role{id: 3, kind: CommonRole}, - &Role{id: 4, kind: CommonRole}, - &Role{id: 5, kind: CommonRole}, - &Role{id: 6, kind: CommonRole}, + &Role{id: 4, kind: ContextRole}, + &Role{id: 5, kind: ContextRole}, + &Role{id: 6, kind: AuthenticatedRole}, ) ) @@ -95,7 +101,7 @@ func benchmarkCheck(b *testing.B, c int) { b.StartTimer() for n := 0; n < b.N; n++ { - checkOptimised(iRules, pr, "res-0", "op-0") + check(iRules, pr, "res-0", "op-0") } b.StopTimer() @@ -117,200 +123,23 @@ func Test_checkRulesByResource(t *testing.T) { }{ {Inherit, "", "", nil}, {Inherit, "res", "op", nil}, - {Allow, "res", "op", []*Rule{ + {Allow, "res/1", "op", []*Rule{ {Resource: "---", Operation: "--", Access: Deny}, - {Resource: "res", Operation: "op", Access: Allow}, + {Resource: "res/1", Operation: "op", Access: Allow}, + }}, + {Allow, "res/2", "op", []*Rule{ + {Resource: "res/*", Operation: "op", Access: Allow}, + }}, + {Allow, "res/3", "op", []*Rule{ + {Resource: "res/3", Operation: "op", Access: Allow}, + {Resource: "res/*", Operation: "op", Access: Deny}, }}, } ) for _, c := range cc { - t.Run("", func(t *testing.T) { - require.Equal(t, c.exp, checkRulesByResource(c.set, c.res, c.op)) + t.Run(c.res, func(t *testing.T) { + require.Equal(t, c.exp.String(), checkRulesByResource(c.set, c.op, c.res).String()) }) } } - -//// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// // -//// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// // -//// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// // -// -//const ( -// role1 uint64 = 10001 -// role2 uint64 = 10002 -// -// resService1 = Resource("service1") -// resService2 = Resource("service2") -// -// resThingWc = Resource("some:answer:*") -// resThing13 = Resource("some:answer:13") -// resThing42 = Resource("some:answer:42") -// -// opAccess = "access" -// opRead = "read" -// opWrite = "write" -//) -// -//func TestRuleSet_check(t *testing.T) { -// var ( -// req = require.New(t) -// -// rr = RuleSet{ -// AllowRule(role1, resThing42, opRead), -// DenyRule(role1, resThing13, opWrite), -// AllowRule(role2, resThing13, opWrite), -// } -// -// sCases = []struct { -// roles []uint64 -// res Resource -// op Operation -// expected Access -// }{ -// {[]uint64{role1}, resThing42, opRead, Allow}, -// {[]uint64{role1}, resThing42, opWrite, Inherit}, -// {[]uint64{role1}, resThing13, opWrite, Deny}, -// {[]uint64{role2}, resThing13, opWrite, Allow}, -// {[]uint64{role1, role2}, resThing13, opWrite, Deny}, -// {[]uint64{role1, role2}, resThing42, opRead, Allow}, -// } -// ) -// -// for c, sc := range sCases { -// v := rr.check(sc.res, sc.op, sc.roles...) -// req.Equalf(sc.expected, v, "Check test #%d failed, expected %s, got %s", c, sc.expected, v) -// } -//} -// -//// Test resource inheritance -//func TestRuleSet_checkResource(t *testing.T) { -// const ( -// role1 uint64 = 10001 -// -// resService1 = Resource("service1") -// resService2 = Resource("service2") -// -// resThingWc = Resource("some:answer:*") -// resThing13 = Resource("some:answer:13") -// resThing42 = Resource("some:answer:42") -// -// opAccess = "access" -// ) -// -// var ( -// r = require.New(t) -// -// sCases = []struct { -// rr RuleSet -// roles []uint64 -// res Resource -// op Operation -// expected Access -// }{ -// { -// RuleSet{ -// AllowRule(role1, resService1, opAccess), -// }, -// []uint64{role1}, -// resService1, -// opAccess, -// Allow, -// }, -// { -// RuleSet{ -// AllowRule(role1, resThingWc, opAccess), -// }, -// []uint64{role1}, -// resThing42, -// opAccess, -// Allow, -// }, -// { // deny wc and explictly allow 42 -// RuleSet{ -// DenyRule(role1, resThingWc, opAccess), -// AllowRule(role1, resThing42, opAccess), -// }, -// []uint64{role1}, -// resThing42, -// opAccess, -// Allow, -// }, -// { // deny wc and explictly allow 42 -// RuleSet{ -// DenyRule(role1, resThingWc, opAccess), -// AllowRule(role1, resThing42, opAccess), -// }, -// []uint64{role1}, -// resThing13, -// opAccess, -// Deny, -// }, -// { // deny wc and and check if wc is denied -// RuleSet{ -// DenyRule(role1, resThingWc, opAccess), -// AllowRule(role1, resThing42, opAccess), -// }, -// []uint64{role1}, -// resThingWc, -// opAccess, -// Deny, -// }, -// { // allow wc and and check if wc is allowed -// RuleSet{ -// AllowRule(role1, resThingWc, opAccess), -// DenyRule(role1, resThing42, opAccess), -// }, -// []uint64{role1}, -// resThingWc, -// opAccess, -// Allow, -// }, -// } -// ) -// -// for c, sc := range sCases { -// v := sc.rr.checkResource(sc.res, sc.op, sc.roles...) -// r.Equalf(sc.expected, v, "Check test #%d failed, expected %s, got %s", c, sc.expected, v) -// } -//} -// -//// Test role inheritance -//func TestRuleSet_Check(t *testing.T) { -// var ( -// rr = RuleSet{ -// // 1st level -// AllowRule(role1, resService1, opAccess), -// DenyRule(role2, resService1, opAccess), -// // 2nd level -// DenyRule(EveryoneRoleID, resService2, opAccess), -// AllowRule(EveryoneRoleID, resThing13, opAccess), -// AllowRule(role1, resService2, opAccess), -// // 3rd level -// DenyRule(EveryoneRoleID, resThingWc, opAccess), -// AllowRule(role1, resThing42, opAccess), -// } -// -// r = require.New(t) -// -// sCases = []struct { -// roles []uint64 -// res Resource -// op Operation -// expected Access -// }{ -// {[]uint64{role1}, resService1, opAccess, Allow}, -// {[]uint64{role2}, resService1, opAccess, Deny}, -// {[]uint64{role1}, resService2, opAccess, Allow}, -// {[]uint64{role2}, resService2, opAccess, Deny}, -// {[]uint64{role1}, resThing42, opAccess, Allow}, -// {[]uint64{role2}, resThing42, opAccess, Deny}, -// {[]uint64{}, resThing42, opAccess, Deny}, -// {[]uint64{}, resThing13, opAccess, Allow}, -// } -// ) -// -// for c, sc := range sCases { -// v := rr.Check(sc.res, sc.op, sc.roles...) -// r.Equalf(sc.expected, v, "Check test #%d failed, expected %s, got %s", c, sc.expected, v) -// } -//} diff --git a/pkg/rbac/ruleset_utils.go b/pkg/rbac/ruleset_utils.go index 0e49a2bb5..5a35addcd 100644 --- a/pkg/rbac/ruleset_utils.go +++ b/pkg/rbac/ruleset_utils.go @@ -62,16 +62,21 @@ func ruleByRole(base RuleSet, roleID uint64) (out RuleSet) { } // Dirty returns list of deleted (Access==Inherit) and changed (dirty) rules -func flushable(set RuleSet) (inherited, rest RuleSet) { - inherited, rest = RuleSet{}, RuleSet{} +func flushable(set RuleSet) (deletable, updatable, final RuleSet) { + deletable, updatable, final = RuleSet{}, RuleSet{}, RuleSet{} for _, r := range set { var c = *r if r.Access == Inherit { - inherited = append(inherited, &c) - } else if r.dirty { - rest = append(rest, &c) + deletable = append(deletable, &c) + continue } + + if r.dirty { + updatable = append(updatable, &c) + } + + final = append(final, &c) } return diff --git a/pkg/rbac/ruleset_utils_test.go b/pkg/rbac/ruleset_utils_test.go index d513b0993..0f21e80fb 100644 --- a/pkg/rbac/ruleset_utils_test.go +++ b/pkg/rbac/ruleset_utils_test.go @@ -9,8 +9,6 @@ import ( // Test role inheritance func TestRuleSet_merge(t *testing.T) { var ( - req = require.New(t) - role1 uint64 = 1 role2 uint64 = 2 role3 uint64 = 3 @@ -22,34 +20,39 @@ func TestRuleSet_merge(t *testing.T) { resThingWc = "*" sCases = []struct { - old RuleSet - new RuleSet - del RuleSet - upd RuleSet + old RuleSet + new RuleSet + deletable RuleSet + updatable RuleSet + final RuleSet }{ { RuleSet{AllowRule(role1, resService1, opAccess)}, RuleSet{AllowRule(role1, resService1, opAccess)}, RuleSet{}, RuleSet{}, + RuleSet{AllowRule(role1, resService1, opAccess)}, }, { RuleSet{AllowRule(role1, resService1, opAccess)}, RuleSet{DenyRule(role1, resService1, opAccess)}, RuleSet{}, RuleSet{DenyRule(role1, resService1, opAccess)}, + RuleSet{DenyRule(role1, resService1, opAccess)}, }, { RuleSet{AllowRule(role1, resService1, opAccess)}, RuleSet{InheritRule(role1, resService1, opAccess)}, RuleSet{InheritRule(role1, resService1, opAccess)}, RuleSet{}, + RuleSet{}, }, { RuleSet{AllowRule(role1, resService1, opAccess)}, RuleSet{AllowRule(role1, resService1, opAccess)}, RuleSet{}, RuleSet{}, + RuleSet{AllowRule(role1, resService1, opAccess)}, }, { RuleSet{ @@ -69,10 +72,14 @@ func TestRuleSet_merge(t *testing.T) { InheritRule(role2, resThing42, opAccess), }, RuleSet{ - // AllowRule(role1, resService1, opAccess), - // DenyRule(role2, resService1, opAccess), - // DenyRule(EveryoneRoleID, resService2, opAccess), - // AllowRule(role1, resService2, opAccess), + DenyRule(role3, resThingWc, opAccess), + AllowRule(role1, resThing42, opAccess), + }, + RuleSet{ + AllowRule(role1, resService1, opAccess), + DenyRule(role2, resService1, opAccess), + DenyRule(role3, resService2, opAccess), + AllowRule(role1, resService2, opAccess), DenyRule(role3, resThingWc, opAccess), AllowRule(role1, resThing42, opAccess), }, @@ -81,17 +88,27 @@ func TestRuleSet_merge(t *testing.T) { ) for _, sc := range sCases { - // Apply changed and get update candidates - mrg := merge(sc.old, sc.new...) - del, upd := flushable(mrg) + t.Run("", func(t *testing.T) { + var ( + req = require.New(t) - // Clear dirty flag so that we do not confuse DeepEqual - clear(del) - clear(upd) + // Apply changed and get update candidates + mrg = merge(sc.old, sc.new...) + deletable, updatable, final = flushable(mrg) + ) - req.Equal(len(sc.del), len(del)) - req.Equal(len(sc.upd), len(upd)) - req.Equal(sc.del, del) - req.Equal(sc.upd, upd) + req.Equal(len(sc.deletable), len(deletable), "rule count for deletable do not match") + req.Equal(len(sc.updatable), len(updatable), "rule count for updatable do not match") + req.Equal(len(sc.final), len(final), "rule count for final do not match") + + // Clear dirty flag so that we do not confuse comparison test + clear(deletable) + clear(updatable) + clear(final) + + req.Equal(sc.deletable, deletable, "deletable rules do not match") + req.Equal(sc.updatable, updatable, "updatable rules do not match") + req.Equal(sc.final, final, "final rules do not match") + }) } } diff --git a/pkg/rbac/service.go b/pkg/rbac/service.go index 0ddaa9ae1..a24d1bdf1 100644 --- a/pkg/rbac/service.go +++ b/pkg/rbac/service.go @@ -2,6 +2,9 @@ package rbac import ( "context" + "fmt" + "strconv" + "strings" "sync" "time" @@ -11,7 +14,7 @@ import ( type ( service struct { - l *sync.Mutex + l *sync.RWMutex logger *zap.Logger // service will flush values on TRUE or just reload on FALSE @@ -28,17 +31,6 @@ type ( // RuleFilter is a dummy struct to satisfy store codegen RuleFilter struct{} - Controller interface { - Can(ses Session, op string, res Resource) bool - Check(ses Session, op string, res Resource) (v Access) - Grant(ctx context.Context, rules ...*Rule) (err error) - Watch(ctx context.Context) - FindRulesByRoleID(roleID uint64) (rr RuleSet) - Rules() (rr RuleSet) - Reload(ctx context.Context) - UpdateRoles(rr ...*Role) - } - RoleSettings struct { Bypass []uint64 Authenticated []uint64 @@ -48,7 +40,7 @@ type ( var ( // Global RBAC service - gRBAC Controller + gRBAC *service ) const ( @@ -56,31 +48,22 @@ const ( ) // Global returns global RBAC service -func Global() Controller { +func Global() *service { return gRBAC } -func SetGlobal(svc Controller) { +// SetGlobal re-sets global service +func SetGlobal(svc *service) { gRBAC = svc } -func Initialize(logger *zap.Logger, s rbacRulesStore) error { - if gRBAC != nil { - // Prevent multiple initializations - return nil - } - - SetGlobal(NewService(logger, s)) - return nil -} - // NewService initializes service{} struct // // service{} struct preloads, checks, grants and flushes privileges to and from store // It acts as a caching layer func NewService(logger *zap.Logger, s rbacRulesStore) (svc *service) { svc = &service{ - l: &sync.Mutex{}, + l: &sync.RWMutex{}, f: make(chan bool), store: s, @@ -100,25 +83,32 @@ func NewService(logger *zap.Logger, s rbacRulesStore) (svc *service) { // iterate over all fallback functions // // System user is always allowed to do everything -// -// When not explicitly allowed through rules or fallbacks, function will return FALSE. -func (svc service) Can(ses Session, op string, res Resource) bool { - return true - //return svc.Check(ses, op, res) == Allow +func (svc *service) Can(ses Session, op string, res Resource) bool { + return svc.Check(ses, op, res) == Allow } // Check verifies if role has access to perform an operation on a resource // // See RuleSet's Check() func for details -func (svc service) Check(ses Session, op string, res Resource) (v Access) { - svc.l.Lock() - defer svc.l.Unlock() - return checkOptimised( - svc.indexed, - getContextRoles(ses.Roles(), res, svc.roles), - op, - res.RbacResource(), +func (svc *service) Check(ses Session, op string, res Resource) (v Access) { + svc.l.RLock() + defer svc.l.RUnlock() + + var ( + fRoles = getContextRoles(ses, res, svc.roles) + access = check(svc.indexed, fRoles, op, res.RbacResource()) ) + + svc.logger.Debug(access.String()+" "+op+" for "+res.RbacResource(), + append( + fRoles.LogFields(), + zap.Uint64("identity", ses.Identity()), + zap.Any("indexed", len(svc.indexed)), + zap.Any("rules", len(svc.rules)), + )..., + ) + + return access } // Grant appends and/or overwrites internal rules slice @@ -128,18 +118,21 @@ func (svc *service) Grant(ctx context.Context, rules ...*Rule) (err error) { svc.l.Lock() defer svc.l.Unlock() - svc.grant(rules...) + for _, r := range rules { + svc.logger.Debug(r.Access.String() + " " + r.Operation + " on " + r.Resource + " to " + strconv.FormatUint(r.RoleID, 10)) + } + svc.grant(rules...) return svc.flush(ctx) } func (svc *service) grant(rules ...*Rule) { svc.rules = merge(svc.rules, rules...) - // @todo reindex + svc.indexed = indexRules(svc.rules) } -// Watches for changes -func (svc service) Watch(ctx context.Context) { +// Watch reloads RBAC rules in intervals and on request +func (svc *service) Watch(ctx context.Context) { go func() { defer sentry.Recover() @@ -160,25 +153,36 @@ func (svc service) Watch(ctx context.Context) { svc.logger.Debug("watcher initialized") } -func (svc service) FindRulesByRoleID(roleID uint64) (rr RuleSet) { - svc.l.Lock() - defer svc.l.Unlock() +// FindRulesByRoleID returns all RBAC rules that belong to a role +func (svc *service) FindRulesByRoleID(roleID uint64) (rr RuleSet) { + svc.l.RLock() + defer svc.l.RUnlock() return ruleByRole(svc.rules, roleID) } -func (svc service) Rules() (rr RuleSet) { - svc.l.Lock() - defer svc.l.Unlock() +// Rules return all roles +func (svc *service) Rules() (rr RuleSet) { + svc.l.RLock() + defer svc.l.RUnlock() return svc.rules } +// Reload store rules func (svc *service) Reload(ctx context.Context) { svc.l.Lock() defer svc.l.Unlock() svc.reloadRules(ctx) } +// Clear removes all access control rules +func (svc *service) Clear() { + svc.l.Lock() + defer svc.l.Unlock() + svc.rules = RuleSet{} + svc.indexed = OptRuleSet{} +} + func (svc *service) reloadRules(ctx context.Context) { rr, _, err := svc.store.SearchRbacRules(ctx, RuleFilter{}) svc.logger.Debug( @@ -190,9 +194,11 @@ func (svc *service) reloadRules(ctx context.Context) { if err == nil { svc.rules = rr + svc.indexed = indexRules(rr) } } +// UpdateRoles updates RBAC roles func (svc *service) UpdateRoles(rr ...*Role) { svc.l.Lock() defer svc.l.Unlock() @@ -200,7 +206,7 @@ func (svc *service) UpdateRoles(rr ...*Role) { stats := statRoles(rr...) svc.logger.Debug( "updating roles", - zap.Int("before", len(svc.rules)), + zap.Int("before", len(svc.roles)), zap.Int("after", len(rr)), zap.Int("bypass", stats[BypassRole]), zap.Int("context", stats[ContextRole]), @@ -211,24 +217,63 @@ func (svc *service) UpdateRoles(rr ...*Role) { svc.roles = rr } -func (svc service) flush(ctx context.Context) (err error) { - d, u := flushable(svc.rules) +// flush pushes all changed rules to the store (if service is configured with one) +func (svc *service) flush(ctx context.Context) (err error) { + if svc.store == nil { + svc.logger.Debug("rule flushing disabled (no store)") + return + } - err = svc.store.DeleteRbacRule(ctx, d...) + deletable, updatable, final := flushable(svc.rules) + + err = svc.store.DeleteRbacRule(ctx, deletable...) if err != nil { return } - err = svc.store.UpsertRbacRule(ctx, u...) + err = svc.store.UpsertRbacRule(ctx, updatable...) if err != nil { return } - clear(u) - svc.rules = u - svc.logger.Debug("flushed rules", - zap.Int("updated", len(u)), - zap.Int("deleted", len(d))) + clear(final) + svc.rules = final + svc.logger.Debug( + "flushed rules", + zap.Int("deleted", len(deletable)), + zap.Int("updated", len(updatable)), + zap.Int("final", len(final)), + ) + + return +} + +func (svc service) String() (out string) { + tpl := "%-5v %-20s to %-20s %-30s\n" + out += strings.Repeat("-", 120) + "\n" + + role := func(id uint64) string { + for _, r := range svc.roles { + if r.id == id { + if r.handle != "" { + return fmt.Sprintf("%s [%d]", r.handle, r.kind) + } + return fmt.Sprintf("%d [%d]", id, r.kind) + } + } + + return fmt.Sprintf("%d [?]", id) + } + + for _, byRole := range svc.indexed { + for _, rr := range byRole { + for _, r := range rr { + out += fmt.Sprintf(tpl, r.Access, r.Operation, role(r.RoleID), r.Resource) + } + } + } + + out += strings.Repeat("-", 120) + "\n" return } diff --git a/pkg/rbac/service_alt.go b/pkg/rbac/service_alt.go index 32e7d9d9c..0ba330de1 100644 --- a/pkg/rbac/service_alt.go +++ b/pkg/rbac/service_alt.go @@ -2,21 +2,13 @@ package rbac import ( "context" - "fmt" - "go.uber.org/zap" - "strings" - "sync" ) type ( - ServiceAllowAll struct{} - ServiceDenyAll struct{} - TestService struct { - service - } + ServiceAllowAll struct{ *service } ) -func (ServiceAllowAll) Can([]uint64, string, Resource) bool { +func (ServiceAllowAll) Can(Session, string, Resource) bool { return true } @@ -30,52 +22,3 @@ func (ServiceAllowAll) FindRulesByRoleID(uint64) (rr RuleSet) { func (ServiceAllowAll) Grant(context.Context, ...*Rule) error { return nil } - -func (ServiceDenyAll) Can([]uint64, string, string) bool { - return false -} - -func (ServiceDenyAll) Check(string, string, ...uint64) (v Access) { - return Deny -} - -func (ServiceDenyAll) Grant(context.Context, ...*Rule) error { - return nil -} - -func (ServiceDenyAll) FindRulesByRoleID(uint64) (rr RuleSet) { - return -} - -func (svc *TestService) ClearGrants() { - _ = svc.store.TruncateRbacRules(context.Background()) - svc.rules = RuleSet{} -} - -func (svc *TestService) String() (out string) { - tpl := "%20v\t%-30s\t%-30s\t%v\n" - out = fmt.Sprintf(tpl, "role", "res", "op", "access") - out += strings.Repeat("-", 120) + "\n" - - for _, r := range svc.rules { - out += fmt.Sprintf(tpl, r.RoleID, r.Resource, r.Operation, r.Access) - } - - out += strings.Repeat("-", 120) + "\n" - - return -} - -func NewTestService(logger *zap.Logger, s rbacRulesStore) (svc *TestService) { - svc = &TestService{ - service: service{ - l: &sync.Mutex{}, - f: make(chan bool), - - logger: logger.Named("rbac-test"), - store: s, - }, - } - - return -} diff --git a/pkg/rbac/session.go b/pkg/rbac/session.go index 2d23d489c..f73052624 100644 --- a/pkg/rbac/session.go +++ b/pkg/rbac/session.go @@ -2,10 +2,12 @@ package rbac import ( "context" + "github.com/cortezaproject/corteza-server/pkg/auth" ) type ( + // Security/RBAC session Session interface { // Identity of the subject Identity() uint64 diff --git a/pkg/websocket/session_test.go b/pkg/websocket/session_test.go index aacc193b5..188bd68da 100644 --- a/pkg/websocket/session_test.go +++ b/pkg/websocket/session_test.go @@ -1,13 +1,14 @@ package websocket import ( + "testing" + "time" + "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/logger" "github.com/cortezaproject/corteza-server/pkg/options" "github.com/stretchr/testify/require" "go.uber.org/zap" - "testing" - "time" ) func TestSession_procRawMessage(t *testing.T) { @@ -28,7 +29,7 @@ func TestSession_procRawMessage(t *testing.T) { req.NoError(err) s.server.accessToken = jwtHandler - jwt := jwtHandler.Encode(auth.NewIdentity(userID, 456, 789)) + jwt := jwtHandler.Encode(auth.Authenticated(userID, 456, 789)) req.EqualError(s.procRawMessage([]byte("{}")), "unauthenticated session") req.Nil(s.identity) @@ -44,14 +45,14 @@ func TestSession_procRawMessage(t *testing.T) { req.Equal(userID, s.identity.Identity()) // Repeat with the same user - jwt = jwtHandler.Encode(auth.NewIdentity(userID, 456, 789)) + jwt = jwtHandler.Encode(auth.Authenticated(userID, 456, 789)) req.NoError(s.procRawMessage([]byte(`{"@type": "credentials", "@value": {"accessToken": "` + jwt + `"}}`))) req.NotNil(s.identity) req.Equal(userID, s.identity.Identity()) // Try to authenticate on an existing authenticated session as a different user - jwt = jwtHandler.Encode(auth.NewIdentity(userID+1, 456, 789)) + jwt = jwtHandler.Encode(auth.Authenticated(userID+1, 456, 789)) req.EqualError(s.procRawMessage([]byte(`{"@type": "credentials", "@value": {"accessToken": "`+jwt+`"}}`)), "unauthorized: identity does not match") diff --git a/provision/000_base/compose_access_control.yaml b/provision/000_base/compose_access_control.yaml index 9ca278a50..9d6aa5501 100644 --- a/provision/000_base/compose_access_control.yaml +++ b/provision/000_base/compose_access_control.yaml @@ -1,29 +1,25 @@ allow: authenticated: - compose: - - access - - compose:namespace: + corteza::compose:namespace/*: - read - compose:module: + corteza::compose:module/*/*: - read - compose:page: + corteza::compose:page/*/*: - read - compose:chart: + corteza::compose:chart/*/*: - read admins: - compose: - - access + corteza::compose/: - grant - settings.read - settings.manage - namespace.create - compose:namespace: + corteza::compose:namespace/*/*: - read - update - delete @@ -32,21 +28,68 @@ allow: - module.create - chart.create - compose:module: + corteza::compose:module/*/*: - read - update - delete - record.create - - record.read - - record.update - - record.delete - compose:chart: + corteza::compose:module-field/*/*/*: + - record.value.read + - record.value.update + + corteza::compose:record/*/*/*: - read - update - delete - compose:page: + corteza::compose:chart/*/*: + - read + - update + - delete + + corteza::compose:page/*/*: + - read + - update + - delete + + low_code_admins: + corteza::compose/: + - grant + - settings.read + - settings.manage + - namespace.create + + corteza::compose:namespace/*/*: + - read + - update + - delete + - manage + - page.create + - module.create + - chart.create + + corteza::compose:module/*/*: + - read + - update + - delete + - record.create + + corteza::compose:module-field/*/*/*: + - record.value.read + - record.value.update + + corteza::compose:record/*/*/*: + - read + - update + - delete + + corteza::compose:chart/*/*: + - read + - update + - delete + + corteza::compose:page/*/*: - read - update - delete diff --git a/provision/000_base/roles.yaml b/provision/000_base/roles.yaml index bae8a5fa4..2cdca525f 100644 --- a/provision/000_base/roles.yaml +++ b/provision/000_base/roles.yaml @@ -1,6 +1,66 @@ roles: - # Placeholders for programmatically created roles. - # These are created in provision/roles.go + # bypass and other system roles are + # programmatically (re)created on server start admins: name: Administrators + security_admins: + name: Security administrators + + low_code_admins: + name: Low code administrators + + owners: + name: Owners + meta: + description: |- + Automatically assinged to resource owner + Applickable to compose records, auth clients and workflows + + context: + expr: resource.createdBy == userID + resourceType: + - corteza::compose:record + - corteza::system:authClient + - corteza::automation:workflow + + creators: + name: Creators + meta: + description: |- + Automatically assinged to user created the resource. + Applickable on records, auth clients and workflows + context: + expr: resource.createdBy == userID + resourceType: + - corteza::compose:record + - corteza::system:authClient + - corteza::automation:workflow + + updaters: + name: Updaters + meta: + description: |- + Automatically assinged to user that was the last to update the resource. + Applickable to compose records, auth clients and workflows + + context: + expr: resource.updatedBy == userID + resourceType: + - corteza::compose:record + - corteza::system:authClient + - corteza::automation:workflow + + deleters: + name: Deleters + meta: + description: |- + Automatically assinged to user that was the last to update the resource. + Applickable to compose records, auth clients and workflows + + context: + expr: resource.updatedBy == userID + resourceType: + - corteza::compose:record + - corteza::system:authClient + - corteza::automation:workflow diff --git a/provision/000_base/system_access_control.yaml b/provision/000_base/system_access_control.yaml index 1a1f555b2..b4c779c03 100644 --- a/provision/000_base/system_access_control.yaml +++ b/provision/000_base/system_access_control.yaml @@ -1,65 +1,111 @@ allow: authenticated: - system:user: + corteza::system:user/*: - read - - unmask.email - - unmask.name + - email.unmask + - name.unmask - system:application: + corteza::system:application/*: - read - system:role: + corteza::system:role/*: - read - system:template: + corteza::system:template/*: + - read - render - system:messagebus-queue: + corteza::system:queue/*: - queue.read admins: - system: - - access + corteza::system/: - grant - settings.read - settings.manage - application.create - - authClient.create + - auth-client.create - user.create - template.create - role.create - reminder.assign - - messagebus-queue.create + - queue.create - system:application: + corteza::system:application/*: - read - update - delete - system:user: + corteza::system:user/*: - read - update - suspend - unsuspend - delete - - unmask.email - - unmask.name + - email.unmask + - name.unmask - system:role: + corteza::system:role/*: - read - update - delete - members.manage - system:template: + corteza::system:template/*: - read - update - delete - render - system:messagebus-queue: + corteza::system:queue/*: - read - update - delete - queue.read - queue.write + + security_admins: + corteza::compose/: + - grant + - settings.read + - settings.manage + + corteza::automation/: + - grant + - settings.read + - settings.manage + + corteza::federation/: + - grant + - settings.read + - settings.manage + + corteza::system/: + - grant + - settings.read + - settings.manage + - application.create + - auth-client.create + - user.create + - template.create + - role.create + - reminder.assign + - queue.create + + corteza::system:application/*: + - read + - update + - delete + + corteza::system:user/*: + - read + - update + - suspend + - unsuspend + - delete + + corteza::system:role/*: + - read + - update + - delete + - members.manage diff --git a/provision/003_auth/auth_client_access_control.yaml b/provision/003_auth/auth_client_access_control.yaml index f478dfa9a..598171933 100644 --- a/provision/003_auth/auth_client_access_control.yaml +++ b/provision/003_auth/auth_client_access_control.yaml @@ -1,10 +1,10 @@ allow: authenticated: - system:auth-client: + corteza::system:auth-client/*: - authorize admins: - system:auth-client: + corteza::system:auth-client/*: - read - update - delete diff --git a/provision/200_federation/2000_access_control.yaml b/provision/200_federation/2000_access_control.yaml index a30f8dd64..df8a3d34b 100644 --- a/provision/200_federation/2000_access_control.yaml +++ b/provision/200_federation/2000_access_control.yaml @@ -1,34 +1,36 @@ allow: admins: - federation: + corteza::federation/: - grant - - access - pair - settings.read - settings.manage - node.create - - federation:node: + + corteza::federation:node/*: - manage - module.create - federation:module: + corteza::federation:exposed-module/*: - manage + + corteza::federation:shared-module/*: - map - + federation: - federation: + corteza::federation/: - grant - - access - pair - settings.read - settings.manage - node.create - - federation:node: + + corteza::federation:node/*: - manage - module.create - federation:module: + corteza::federation:exposed-module/*/*: - manage + + corteza::federation:shared-module/*/*: - map diff --git a/provision/300_automation/2000_access_control.yaml b/provision/300_automation/2000_access_control.yaml index 6fb674c8f..9d7bdb77b 100644 --- a/provision/300_automation/2000_access_control.yaml +++ b/provision/300_automation/2000_access_control.yaml @@ -1,13 +1,12 @@ allow: admins: - automation: - - access + corteza::automation/: - grant - workflow.create - triggers.search - sessions.search - automation:workflow: + corteza::automation:workflow/*: - read - update - delete @@ -16,14 +15,13 @@ allow: - sessions.manage developers: - automation: - - access + corteza::automation/: - grant - workflow.create - triggers.search - sessions.search - automation:workflow: + corteza::automation:workflow/*: - read - update - delete diff --git a/store/rdbms/actionlog.gen.go b/store/rdbms/actionlog.gen.go index 17ddf8fc4..30f640b4e 100644 --- a/store/rdbms/actionlog.gen.go +++ b/store/rdbms/actionlog.gen.go @@ -52,6 +52,7 @@ func (s Store) QueryActionlogs( check func(*actionlog.Action) (bool, error), ) ([]*actionlog.Action, error) { var ( + tmp = make([]*actionlog.Action, 0, DefaultSliceCapacity) set = make([]*actionlog.Action, 0, DefaultSliceCapacity) res *actionlog.Action @@ -73,10 +74,15 @@ func (s Store) QueryActionlogs( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + set = append(set, res) } - return set, rows.Err() + return set, nil } // CreateActionlog creates one or more rows in actionlog table diff --git a/store/rdbms/applications.gen.go b/store/rdbms/applications.gen.go index 1e1f83ce4..58e928070 100644 --- a/store/rdbms/applications.gen.go +++ b/store/rdbms/applications.gen.go @@ -238,6 +238,7 @@ func (s Store) QueryApplications( check func(*types.Application) (bool, error), ) ([]*types.Application, error) { var ( + tmp = make([]*types.Application, 0, DefaultSliceCapacity) set = make([]*types.Application, 0, DefaultSliceCapacity) res *types.Application @@ -259,6 +260,11 @@ func (s Store) QueryApplications( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + // check fn set, call it and see if it passed the test // if not, skip the item if check != nil { @@ -272,7 +278,7 @@ func (s Store) QueryApplications( set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupApplicationByID searches for application by ID diff --git a/store/rdbms/attachments.gen.go b/store/rdbms/attachments.gen.go index 2343d9691..fabd588c6 100644 --- a/store/rdbms/attachments.gen.go +++ b/store/rdbms/attachments.gen.go @@ -52,6 +52,7 @@ func (s Store) QueryAttachments( check func(*types.Attachment) (bool, error), ) ([]*types.Attachment, error) { var ( + tmp = make([]*types.Attachment, 0, DefaultSliceCapacity) set = make([]*types.Attachment, 0, DefaultSliceCapacity) res *types.Attachment @@ -73,6 +74,11 @@ func (s Store) QueryAttachments( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + // check fn set, call it and see if it passed the test // if not, skip the item if check != nil { @@ -86,7 +92,7 @@ func (s Store) QueryAttachments( set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupAttachmentByID searches for attachment by its ID diff --git a/store/rdbms/auth_clients.gen.go b/store/rdbms/auth_clients.gen.go index 245546d90..b6dc7815d 100644 --- a/store/rdbms/auth_clients.gen.go +++ b/store/rdbms/auth_clients.gen.go @@ -238,6 +238,7 @@ func (s Store) QueryAuthClients( check func(*types.AuthClient) (bool, error), ) ([]*types.AuthClient, error) { var ( + tmp = make([]*types.AuthClient, 0, DefaultSliceCapacity) set = make([]*types.AuthClient, 0, DefaultSliceCapacity) res *types.AuthClient @@ -259,6 +260,11 @@ func (s Store) QueryAuthClients( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + // check fn set, call it and see if it passed the test // if not, skip the item if check != nil { @@ -272,7 +278,7 @@ func (s Store) QueryAuthClients( set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupAuthClientByID searches for auth client by ID diff --git a/store/rdbms/auth_confirmed_clients.gen.go b/store/rdbms/auth_confirmed_clients.gen.go index e81acfb81..2a6d2cccd 100644 --- a/store/rdbms/auth_confirmed_clients.gen.go +++ b/store/rdbms/auth_confirmed_clients.gen.go @@ -52,6 +52,7 @@ func (s Store) QueryAuthConfirmedClients( check func(*types.AuthConfirmedClient) (bool, error), ) ([]*types.AuthConfirmedClient, error) { var ( + tmp = make([]*types.AuthConfirmedClient, 0, DefaultSliceCapacity) set = make([]*types.AuthConfirmedClient, 0, DefaultSliceCapacity) res *types.AuthConfirmedClient @@ -73,10 +74,15 @@ func (s Store) QueryAuthConfirmedClients( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupAuthConfirmedClientByUserIDClientID diff --git a/store/rdbms/auth_oa2tokens.gen.go b/store/rdbms/auth_oa2tokens.gen.go index 3c26f25b6..0049c6ccb 100644 --- a/store/rdbms/auth_oa2tokens.gen.go +++ b/store/rdbms/auth_oa2tokens.gen.go @@ -52,6 +52,7 @@ func (s Store) QueryAuthOa2tokens( check func(*types.AuthOa2token) (bool, error), ) ([]*types.AuthOa2token, error) { var ( + tmp = make([]*types.AuthOa2token, 0, DefaultSliceCapacity) set = make([]*types.AuthOa2token, 0, DefaultSliceCapacity) res *types.AuthOa2token @@ -73,10 +74,15 @@ func (s Store) QueryAuthOa2tokens( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupAuthOa2tokenByCode diff --git a/store/rdbms/auth_sessions.gen.go b/store/rdbms/auth_sessions.gen.go index 0c7c6692a..457f72dd5 100644 --- a/store/rdbms/auth_sessions.gen.go +++ b/store/rdbms/auth_sessions.gen.go @@ -52,6 +52,7 @@ func (s Store) QueryAuthSessions( check func(*types.AuthSession) (bool, error), ) ([]*types.AuthSession, error) { var ( + tmp = make([]*types.AuthSession, 0, DefaultSliceCapacity) set = make([]*types.AuthSession, 0, DefaultSliceCapacity) res *types.AuthSession @@ -73,10 +74,15 @@ func (s Store) QueryAuthSessions( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupAuthSessionByID diff --git a/store/rdbms/automation_sessions.gen.go b/store/rdbms/automation_sessions.gen.go index eba157512..b0ae99939 100644 --- a/store/rdbms/automation_sessions.gen.go +++ b/store/rdbms/automation_sessions.gen.go @@ -238,6 +238,7 @@ func (s Store) QueryAutomationSessions( check func(*types.Session) (bool, error), ) ([]*types.Session, error) { var ( + tmp = make([]*types.Session, 0, DefaultSliceCapacity) set = make([]*types.Session, 0, DefaultSliceCapacity) res *types.Session @@ -259,6 +260,11 @@ func (s Store) QueryAutomationSessions( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + // check fn set, call it and see if it passed the test // if not, skip the item if check != nil { @@ -272,7 +278,7 @@ func (s Store) QueryAutomationSessions( set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupAutomationSessionByID searches for session by ID diff --git a/store/rdbms/automation_triggers.gen.go b/store/rdbms/automation_triggers.gen.go index 6c0fd05fa..3bd1414aa 100644 --- a/store/rdbms/automation_triggers.gen.go +++ b/store/rdbms/automation_triggers.gen.go @@ -238,6 +238,7 @@ func (s Store) QueryAutomationTriggers( check func(*types.Trigger) (bool, error), ) ([]*types.Trigger, error) { var ( + tmp = make([]*types.Trigger, 0, DefaultSliceCapacity) set = make([]*types.Trigger, 0, DefaultSliceCapacity) res *types.Trigger @@ -259,6 +260,11 @@ func (s Store) QueryAutomationTriggers( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + // check fn set, call it and see if it passed the test // if not, skip the item if check != nil { @@ -272,7 +278,7 @@ func (s Store) QueryAutomationTriggers( set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupAutomationTriggerByID searches for trigger by ID diff --git a/store/rdbms/automation_workflows.gen.go b/store/rdbms/automation_workflows.gen.go index 7cea8bd3e..89c01ad61 100644 --- a/store/rdbms/automation_workflows.gen.go +++ b/store/rdbms/automation_workflows.gen.go @@ -238,6 +238,7 @@ func (s Store) QueryAutomationWorkflows( check func(*types.Workflow) (bool, error), ) ([]*types.Workflow, error) { var ( + tmp = make([]*types.Workflow, 0, DefaultSliceCapacity) set = make([]*types.Workflow, 0, DefaultSliceCapacity) res *types.Workflow @@ -259,6 +260,11 @@ func (s Store) QueryAutomationWorkflows( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + // check fn set, call it and see if it passed the test // if not, skip the item if check != nil { @@ -272,7 +278,7 @@ func (s Store) QueryAutomationWorkflows( set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupAutomationWorkflowByID searches for workflow by ID diff --git a/store/rdbms/compose_attachments.gen.go b/store/rdbms/compose_attachments.gen.go index 529fc8370..05e47edcb 100644 --- a/store/rdbms/compose_attachments.gen.go +++ b/store/rdbms/compose_attachments.gen.go @@ -52,6 +52,7 @@ func (s Store) QueryComposeAttachments( check func(*types.Attachment) (bool, error), ) ([]*types.Attachment, error) { var ( + tmp = make([]*types.Attachment, 0, DefaultSliceCapacity) set = make([]*types.Attachment, 0, DefaultSliceCapacity) res *types.Attachment @@ -73,6 +74,11 @@ func (s Store) QueryComposeAttachments( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + // check fn set, call it and see if it passed the test // if not, skip the item if check != nil { @@ -86,7 +92,7 @@ func (s Store) QueryComposeAttachments( set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupComposeAttachmentByID searches for attachment by its ID diff --git a/store/rdbms/compose_charts.gen.go b/store/rdbms/compose_charts.gen.go index 2f7bc75cd..69199ed45 100644 --- a/store/rdbms/compose_charts.gen.go +++ b/store/rdbms/compose_charts.gen.go @@ -238,6 +238,7 @@ func (s Store) QueryComposeCharts( check func(*types.Chart) (bool, error), ) ([]*types.Chart, error) { var ( + tmp = make([]*types.Chart, 0, DefaultSliceCapacity) set = make([]*types.Chart, 0, DefaultSliceCapacity) res *types.Chart @@ -259,6 +260,11 @@ func (s Store) QueryComposeCharts( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + // check fn set, call it and see if it passed the test // if not, skip the item if check != nil { @@ -272,7 +278,7 @@ func (s Store) QueryComposeCharts( set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupComposeChartByID searches for compose chart by ID diff --git a/store/rdbms/compose_module_fields.gen.go b/store/rdbms/compose_module_fields.gen.go index 8e1e566c7..b66f852b9 100644 --- a/store/rdbms/compose_module_fields.gen.go +++ b/store/rdbms/compose_module_fields.gen.go @@ -52,6 +52,7 @@ func (s Store) QueryComposeModuleFields( check func(*types.ModuleField) (bool, error), ) ([]*types.ModuleField, error) { var ( + tmp = make([]*types.ModuleField, 0, DefaultSliceCapacity) set = make([]*types.ModuleField, 0, DefaultSliceCapacity) res *types.ModuleField @@ -73,10 +74,15 @@ func (s Store) QueryComposeModuleFields( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupComposeModuleFieldByModuleIDName searches for compose module field by name (case-insensitive) diff --git a/store/rdbms/compose_modules.gen.go b/store/rdbms/compose_modules.gen.go index c7720d757..0a3478ce9 100644 --- a/store/rdbms/compose_modules.gen.go +++ b/store/rdbms/compose_modules.gen.go @@ -238,6 +238,7 @@ func (s Store) QueryComposeModules( check func(*types.Module) (bool, error), ) ([]*types.Module, error) { var ( + tmp = make([]*types.Module, 0, DefaultSliceCapacity) set = make([]*types.Module, 0, DefaultSliceCapacity) res *types.Module @@ -259,6 +260,11 @@ func (s Store) QueryComposeModules( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + // check fn set, call it and see if it passed the test // if not, skip the item if check != nil { @@ -272,7 +278,7 @@ func (s Store) QueryComposeModules( set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupComposeModuleByNamespaceIDHandle searches for compose module by handle (case-insensitive) diff --git a/store/rdbms/compose_namespaces.gen.go b/store/rdbms/compose_namespaces.gen.go index c31bf81b2..ca1dae1f2 100644 --- a/store/rdbms/compose_namespaces.gen.go +++ b/store/rdbms/compose_namespaces.gen.go @@ -238,6 +238,7 @@ func (s Store) QueryComposeNamespaces( check func(*types.Namespace) (bool, error), ) ([]*types.Namespace, error) { var ( + tmp = make([]*types.Namespace, 0, DefaultSliceCapacity) set = make([]*types.Namespace, 0, DefaultSliceCapacity) res *types.Namespace @@ -259,6 +260,11 @@ func (s Store) QueryComposeNamespaces( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + // check fn set, call it and see if it passed the test // if not, skip the item if check != nil { @@ -272,7 +278,7 @@ func (s Store) QueryComposeNamespaces( set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupComposeNamespaceBySlug searches for namespace by slug (case-insensitive) diff --git a/store/rdbms/compose_pages.gen.go b/store/rdbms/compose_pages.gen.go index 15ec4699f..8f5fe59e4 100644 --- a/store/rdbms/compose_pages.gen.go +++ b/store/rdbms/compose_pages.gen.go @@ -238,6 +238,7 @@ func (s Store) QueryComposePages( check func(*types.Page) (bool, error), ) ([]*types.Page, error) { var ( + tmp = make([]*types.Page, 0, DefaultSliceCapacity) set = make([]*types.Page, 0, DefaultSliceCapacity) res *types.Page @@ -259,6 +260,11 @@ func (s Store) QueryComposePages( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + // check fn set, call it and see if it passed the test // if not, skip the item if check != nil { @@ -272,7 +278,7 @@ func (s Store) QueryComposePages( set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupComposePageByNamespaceIDHandle searches for page by handle (case-insensitive) diff --git a/store/rdbms/compose_record_values.gen.go b/store/rdbms/compose_record_values.gen.go index e83de9164..ca2978201 100644 --- a/store/rdbms/compose_record_values.gen.go +++ b/store/rdbms/compose_record_values.gen.go @@ -52,6 +52,7 @@ func (s Store) QueryComposeRecordValues( check func(*types.RecordValue) (bool, error), ) ([]*types.RecordValue, error) { var ( + tmp = make([]*types.RecordValue, 0, DefaultSliceCapacity) set = make([]*types.RecordValue, 0, DefaultSliceCapacity) res *types.RecordValue @@ -73,10 +74,15 @@ func (s Store) QueryComposeRecordValues( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + set = append(set, res) } - return set, rows.Err() + return set, nil } // createComposeRecordValue creates one or more rows in compose_record_value table diff --git a/store/rdbms/compose_records.gen.go b/store/rdbms/compose_records.gen.go index 42acad827..29861303d 100644 --- a/store/rdbms/compose_records.gen.go +++ b/store/rdbms/compose_records.gen.go @@ -11,6 +11,7 @@ package rdbms import ( "context" "database/sql" + "github.com/Masterminds/squirrel" "github.com/cortezaproject/corteza-server/compose/types" "github.com/cortezaproject/corteza-server/pkg/errors" @@ -165,6 +166,7 @@ func (s Store) QueryComposeRecords( check func(*types.Record) (bool, error), ) ([]*types.Record, error) { var ( + tmp = make([]*types.Record, 0, DefaultSliceCapacity) set = make([]*types.Record, 0, DefaultSliceCapacity) res *types.Record @@ -186,6 +188,15 @@ func (s Store) QueryComposeRecords( return nil, err } + tmp = append(tmp, res) + } + + if err = s.composeRecordPostLoadProcessor(ctx, _mod, tmp...); err != nil { + return nil, err + } + + for _, res = range tmp { + // check fn set, call it and see if it passed the test // if not, skip the item if check != nil { @@ -199,11 +210,7 @@ func (s Store) QueryComposeRecords( set = append(set, res) } - if err = s.composeRecordPostLoadProcessor(ctx, _mod, set...); err != nil { - return nil, err - } - - return set, rows.Err() + return set, nil } // lookupComposeRecordByID searches for compose record by ID diff --git a/store/rdbms/compose_records.go b/store/rdbms/compose_records.go index 63e20fc07..3595bd2f1 100644 --- a/store/rdbms/compose_records.go +++ b/store/rdbms/compose_records.go @@ -108,7 +108,7 @@ func (s Store) SearchComposeRecords(ctx context.Context, m *types.Module, f type // Prevent sorting over multi-value fields // - // Due to how values are currently storred, this causes duplication. + // Due to how values are currently stored, this causes duplication. // For now, we'll prevent this and address in future releases. for _, s := range f.Sort { f := m.Fields.FindByName(s.Column) @@ -300,8 +300,8 @@ func (s Store) composeRecordsPageNavigation( // LookupComposeRecordByID searches for compose record by ID // It returns compose record even if deleted -func (s Store) LookupComposeRecordByID(ctx context.Context, _ *types.Module, id uint64) (res *types.Record, err error) { - res, err = s.lookupComposeRecordByID(ctx, nil, id) +func (s Store) LookupComposeRecordByID(ctx context.Context, m *types.Module, id uint64) (res *types.Record, err error) { + res, err = s.lookupComposeRecordByID(ctx, m, id) if err != nil { return } @@ -317,7 +317,7 @@ func (s Store) CreateComposeRecord(ctx context.Context, m *types.Module, rr ...* for _, res := range rr { - err = s.createComposeRecord(ctx, nil, res) + err = s.createComposeRecord(ctx, m, res) if err != nil { return } @@ -325,7 +325,7 @@ func (s Store) CreateComposeRecord(ctx context.Context, m *types.Module, rr ...* // Make sure all record-values are linked to the record res.Values.SetRecordID(res.ID) - err = s.createComposeRecordValue(ctx, nil, res.Values...) + err = s.createComposeRecordValue(ctx, m, res.Values...) if err != nil { return } @@ -341,7 +341,7 @@ func (s Store) UpdateComposeRecord(ctx context.Context, m *types.Module, rr ...* } for _, res := range rr { - err = s.updateComposeRecord(ctx, nil, res) + err = s.updateComposeRecord(ctx, m, res) if err != nil { return } @@ -365,7 +365,7 @@ func (s Store) UpdateComposeRecord(ctx context.Context, m *types.Module, rr ...* // Make sure all record-values are linked to the record res.Values.SetRecordID(res.ID) - err = s.createComposeRecordValue(ctx, nil, res.Values.GetClean()...) + err = s.createComposeRecordValue(ctx, m, res.Values.GetClean()...) } } @@ -414,8 +414,8 @@ func (s Store) UpsertComposeRecord(ctx context.Context, m *types.Module, rr ...* } // DeleteComposeRecordByID Deletes ComposeRecord from store -func (s Store) DeleteComposeRecordByID(ctx context.Context, _ *types.Module, ID uint64) (err error) { - err = s.deleteComposeRecordByID(ctx, nil, ID) +func (s Store) DeleteComposeRecordByID(ctx context.Context, m *types.Module, ID uint64) (err error) { + err = s.deleteComposeRecordByID(ctx, m, ID) if err != nil { return } @@ -429,13 +429,13 @@ func (s Store) DeleteComposeRecordByID(ctx context.Context, _ *types.Module, ID } // TruncateComposeRecords Deletes all ComposeRecords from store -func (s Store) TruncateComposeRecords(ctx context.Context, _ *types.Module) (err error) { - err = s.truncateComposeRecords(ctx, nil) +func (s Store) TruncateComposeRecords(ctx context.Context, m *types.Module) (err error) { + err = s.truncateComposeRecords(ctx, m) if err != nil { return } - err = s.truncateComposeRecordValues(ctx, nil) + err = s.truncateComposeRecordValues(ctx, m) if err != nil { return } @@ -599,15 +599,18 @@ func (s Store) composeRecordPostLoadProcessor(ctx context.Context, m *types.Modu var ( rvs types.RecordValueSet ) + rvs, _, err = s.searchComposeRecordValues(ctx, nil, types.RecordValueFilter{ RecordID: types.RecordSet(set).IDs(), Deleted: filter.StateInclusive, }) + if err != nil { return } for r := range set { + set[r].SetModule(m) set[r].Values = rvs.FilterByRecordID(set[r].ID) } } diff --git a/store/rdbms/credentials.gen.go b/store/rdbms/credentials.gen.go index e45e80996..14537915a 100644 --- a/store/rdbms/credentials.gen.go +++ b/store/rdbms/credentials.gen.go @@ -52,6 +52,7 @@ func (s Store) QueryCredentials( check func(*types.Credentials) (bool, error), ) ([]*types.Credentials, error) { var ( + tmp = make([]*types.Credentials, 0, DefaultSliceCapacity) set = make([]*types.Credentials, 0, DefaultSliceCapacity) res *types.Credentials @@ -73,10 +74,15 @@ func (s Store) QueryCredentials( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupCredentialsByID searches for credentials by ID diff --git a/store/rdbms/federation_exposed_modules.gen.go b/store/rdbms/federation_exposed_modules.gen.go index a4f7a2715..99f1e0364 100644 --- a/store/rdbms/federation_exposed_modules.gen.go +++ b/store/rdbms/federation_exposed_modules.gen.go @@ -238,6 +238,7 @@ func (s Store) QueryFederationExposedModules( check func(*types.ExposedModule) (bool, error), ) ([]*types.ExposedModule, error) { var ( + tmp = make([]*types.ExposedModule, 0, DefaultSliceCapacity) set = make([]*types.ExposedModule, 0, DefaultSliceCapacity) res *types.ExposedModule @@ -259,6 +260,11 @@ func (s Store) QueryFederationExposedModules( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + // check fn set, call it and see if it passed the test // if not, skip the item if check != nil { @@ -272,7 +278,7 @@ func (s Store) QueryFederationExposedModules( set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupFederationExposedModuleByID searches for federation module by ID diff --git a/store/rdbms/federation_module_mappings.gen.go b/store/rdbms/federation_module_mappings.gen.go index aac117d76..25a9768c1 100644 --- a/store/rdbms/federation_module_mappings.gen.go +++ b/store/rdbms/federation_module_mappings.gen.go @@ -250,6 +250,7 @@ func (s Store) QueryFederationModuleMappings( check func(*types.ModuleMapping) (bool, error), ) ([]*types.ModuleMapping, error) { var ( + tmp = make([]*types.ModuleMapping, 0, DefaultSliceCapacity) set = make([]*types.ModuleMapping, 0, DefaultSliceCapacity) res *types.ModuleMapping @@ -271,6 +272,11 @@ func (s Store) QueryFederationModuleMappings( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + // check fn set, call it and see if it passed the test // if not, skip the item if check != nil { @@ -284,7 +290,7 @@ func (s Store) QueryFederationModuleMappings( set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupFederationModuleMappingByFederationModuleIDComposeModuleIDComposeNamespaceID searches for module mapping by federation module id and compose module id diff --git a/store/rdbms/federation_nodes.gen.go b/store/rdbms/federation_nodes.gen.go index 042fa9623..350d797df 100644 --- a/store/rdbms/federation_nodes.gen.go +++ b/store/rdbms/federation_nodes.gen.go @@ -52,6 +52,7 @@ func (s Store) QueryFederationNodes( check func(*types.Node) (bool, error), ) ([]*types.Node, error) { var ( + tmp = make([]*types.Node, 0, DefaultSliceCapacity) set = make([]*types.Node, 0, DefaultSliceCapacity) res *types.Node @@ -73,6 +74,11 @@ func (s Store) QueryFederationNodes( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + // check fn set, call it and see if it passed the test // if not, skip the item if check != nil { @@ -86,7 +92,7 @@ func (s Store) QueryFederationNodes( set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupFederationNodeByID searches for federation node by ID diff --git a/store/rdbms/federation_nodes_sync.gen.go b/store/rdbms/federation_nodes_sync.gen.go index b1a3cb47a..643c62de6 100644 --- a/store/rdbms/federation_nodes_sync.gen.go +++ b/store/rdbms/federation_nodes_sync.gen.go @@ -238,6 +238,7 @@ func (s Store) QueryFederationNodesSyncs( check func(*types.NodeSync) (bool, error), ) ([]*types.NodeSync, error) { var ( + tmp = make([]*types.NodeSync, 0, DefaultSliceCapacity) set = make([]*types.NodeSync, 0, DefaultSliceCapacity) res *types.NodeSync @@ -259,6 +260,11 @@ func (s Store) QueryFederationNodesSyncs( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + // check fn set, call it and see if it passed the test // if not, skip the item if check != nil { @@ -272,7 +278,7 @@ func (s Store) QueryFederationNodesSyncs( set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupFederationNodesSyncByNodeID searches for sync activity by node ID diff --git a/store/rdbms/federation_shared_modules.gen.go b/store/rdbms/federation_shared_modules.gen.go index 06c499949..2170f79cb 100644 --- a/store/rdbms/federation_shared_modules.gen.go +++ b/store/rdbms/federation_shared_modules.gen.go @@ -238,6 +238,7 @@ func (s Store) QueryFederationSharedModules( check func(*types.SharedModule) (bool, error), ) ([]*types.SharedModule, error) { var ( + tmp = make([]*types.SharedModule, 0, DefaultSliceCapacity) set = make([]*types.SharedModule, 0, DefaultSliceCapacity) res *types.SharedModule @@ -259,6 +260,11 @@ func (s Store) QueryFederationSharedModules( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + // check fn set, call it and see if it passed the test // if not, skip the item if check != nil { @@ -272,7 +278,7 @@ func (s Store) QueryFederationSharedModules( set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupFederationSharedModuleByID searches for shared federation module by ID diff --git a/store/rdbms/flags.gen.go b/store/rdbms/flags.gen.go index 66a74e354..7f748abb8 100644 --- a/store/rdbms/flags.gen.go +++ b/store/rdbms/flags.gen.go @@ -52,6 +52,7 @@ func (s Store) QueryFlags( check func(*types.Flag) (bool, error), ) ([]*types.Flag, error) { var ( + tmp = make([]*types.Flag, 0, DefaultSliceCapacity) set = make([]*types.Flag, 0, DefaultSliceCapacity) res *types.Flag @@ -73,10 +74,15 @@ func (s Store) QueryFlags( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupFlagByKindResourceIDName Flag lookup by kind, resource, name diff --git a/store/rdbms/generic_upgrades.go b/store/rdbms/generic_upgrades.go index aa8ba564c..62ce67d72 100644 --- a/store/rdbms/generic_upgrades.go +++ b/store/rdbms/generic_upgrades.go @@ -70,6 +70,10 @@ func (g genericUpgrades) Upgrade(ctx context.Context, t *ddl.Table) error { g.AlterUsersDropOrganisation, g.AlterUsersDropRelatedUser, ) + case "roles": + return g.all(ctx, + g.AddRolesMetaField, + ) case "compose_module": return g.all(ctx, g.AlterComposeModuleRenameJsonToMeta, @@ -254,6 +258,17 @@ func (g genericUpgrades) AddWeightField(ctx context.Context) error { return err } +func (g genericUpgrades) AddRolesMetaField(ctx context.Context) error { + _, err := g.u.AddColumn(ctx, "roles", &ddl.Column{ + Name: "meta", + Type: ddl.ColumnType{Type: ddl.ColumnTypeJson}, + IsNull: false, + DefaultValue: "'{}'", + }) + + return err +} + func (g genericUpgrades) RenameReminders(ctx context.Context) error { return g.RenameTable(ctx, "sys_reminder", "reminders") } diff --git a/store/rdbms/labels.gen.go b/store/rdbms/labels.gen.go index e71d84497..7bee89951 100644 --- a/store/rdbms/labels.gen.go +++ b/store/rdbms/labels.gen.go @@ -52,6 +52,7 @@ func (s Store) QueryLabels( check func(*types.Label) (bool, error), ) ([]*types.Label, error) { var ( + tmp = make([]*types.Label, 0, DefaultSliceCapacity) set = make([]*types.Label, 0, DefaultSliceCapacity) res *types.Label @@ -73,10 +74,15 @@ func (s Store) QueryLabels( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupLabelByKindResourceIDName Label lookup by kind, resource, name diff --git a/store/rdbms/messagebus_queue_message.gen.go b/store/rdbms/messagebus_queue_message.gen.go index b0e410f92..65aaeb575 100644 --- a/store/rdbms/messagebus_queue_message.gen.go +++ b/store/rdbms/messagebus_queue_message.gen.go @@ -238,6 +238,7 @@ func (s Store) QueryMessagebusQueueMessages( check func(*messagebus.QueueMessage) (bool, error), ) ([]*messagebus.QueueMessage, error) { var ( + tmp = make([]*messagebus.QueueMessage, 0, DefaultSliceCapacity) set = make([]*messagebus.QueueMessage, 0, DefaultSliceCapacity) res *messagebus.QueueMessage @@ -259,10 +260,15 @@ func (s Store) QueryMessagebusQueueMessages( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + set = append(set, res) } - return set, rows.Err() + return set, nil } // CreateMessagebusQueueMessage creates one or more rows in queue_messages table diff --git a/store/rdbms/messagebus_queue_settings.gen.go b/store/rdbms/messagebus_queue_settings.gen.go index edea59901..2d04e09cd 100644 --- a/store/rdbms/messagebus_queue_settings.gen.go +++ b/store/rdbms/messagebus_queue_settings.gen.go @@ -233,6 +233,7 @@ func (s Store) QueryMessagebusQueueSettings( check func(*messagebus.QueueSettings) (bool, error), ) ([]*messagebus.QueueSettings, error) { var ( + tmp = make([]*messagebus.QueueSettings, 0, DefaultSliceCapacity) set = make([]*messagebus.QueueSettings, 0, DefaultSliceCapacity) res *messagebus.QueueSettings @@ -254,10 +255,15 @@ func (s Store) QueryMessagebusQueueSettings( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupMessagebusQueueSettingByID searches for queue by ID diff --git a/store/rdbms/rbac_rules.gen.go b/store/rdbms/rbac_rules.gen.go index fb9266ad3..0b9f48759 100644 --- a/store/rdbms/rbac_rules.gen.go +++ b/store/rdbms/rbac_rules.gen.go @@ -49,6 +49,7 @@ func (s Store) QueryRbacRules( check func(*rbac.Rule) (bool, error), ) ([]*rbac.Rule, error) { var ( + tmp = make([]*rbac.Rule, 0, DefaultSliceCapacity) set = make([]*rbac.Rule, 0, DefaultSliceCapacity) res *rbac.Rule @@ -70,10 +71,15 @@ func (s Store) QueryRbacRules( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + set = append(set, res) } - return set, rows.Err() + return set, nil } // CreateRbacRule creates one or more rows in rbac_rules table diff --git a/store/rdbms/rdbms_schema.go b/store/rdbms/rdbms_schema.go index 2cc41baba..dbcd95ea1 100644 --- a/store/rdbms/rdbms_schema.go +++ b/store/rdbms/rdbms_schema.go @@ -202,6 +202,7 @@ func (Schema) Roles() *Table { ID, ColumnDef("name", ColumnTypeText), ColumnDef("handle", ColumnTypeVarchar, ColumnTypeLength(handleLength)), + ColumnDef("meta", ColumnTypeJson), ColumnDef("archived_at", ColumnTypeTimestamp, Null), CUDTimestamps, diff --git a/store/rdbms/reminders.gen.go b/store/rdbms/reminders.gen.go index 3e5463467..a6320fa42 100644 --- a/store/rdbms/reminders.gen.go +++ b/store/rdbms/reminders.gen.go @@ -238,6 +238,7 @@ func (s Store) QueryReminders( check func(*types.Reminder) (bool, error), ) ([]*types.Reminder, error) { var ( + tmp = make([]*types.Reminder, 0, DefaultSliceCapacity) set = make([]*types.Reminder, 0, DefaultSliceCapacity) res *types.Reminder @@ -259,6 +260,11 @@ func (s Store) QueryReminders( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + // check fn set, call it and see if it passed the test // if not, skip the item if check != nil { @@ -272,7 +278,7 @@ func (s Store) QueryReminders( set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupReminderByID searches for reminder by its ID diff --git a/store/rdbms/role_members.gen.go b/store/rdbms/role_members.gen.go index a9f1a3608..827c0fe0f 100644 --- a/store/rdbms/role_members.gen.go +++ b/store/rdbms/role_members.gen.go @@ -52,6 +52,7 @@ func (s Store) QueryRoleMembers( check func(*types.RoleMember) (bool, error), ) ([]*types.RoleMember, error) { var ( + tmp = make([]*types.RoleMember, 0, DefaultSliceCapacity) set = make([]*types.RoleMember, 0, DefaultSliceCapacity) res *types.RoleMember @@ -73,10 +74,15 @@ func (s Store) QueryRoleMembers( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + set = append(set, res) } - return set, rows.Err() + return set, nil } // CreateRoleMember creates one or more rows in role_members table diff --git a/store/rdbms/roles.gen.go b/store/rdbms/roles.gen.go index ed13004d3..290e1e0fc 100644 --- a/store/rdbms/roles.gen.go +++ b/store/rdbms/roles.gen.go @@ -238,6 +238,7 @@ func (s Store) QueryRoles( check func(*types.Role) (bool, error), ) ([]*types.Role, error) { var ( + tmp = make([]*types.Role, 0, DefaultSliceCapacity) set = make([]*types.Role, 0, DefaultSliceCapacity) res *types.Role @@ -259,6 +260,11 @@ func (s Store) QueryRoles( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + // check fn set, call it and see if it passed the test // if not, skip the item if check != nil { @@ -272,7 +278,7 @@ func (s Store) QueryRoles( set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupRoleByID searches for role by ID @@ -458,6 +464,7 @@ func (s Store) internalRoleRowScanner(row rowScanner) (res *types.Role, err erro &res.ID, &res.Name, &res.Handle, + &res.Meta, &res.CreatedAt, &res.UpdatedAt, &res.ArchivedAt, @@ -504,6 +511,7 @@ func (Store) roleColumns(aa ...string) []string { alias + "id", alias + "name", alias + "handle", + alias + "meta", alias + "created_at", alias + "updated_at", alias + "archived_at", @@ -538,6 +546,7 @@ func (s Store) internalRoleEncoder(res *types.Role) store.Payload { "id": res.ID, "name": res.Name, "handle": res.Handle, + "meta": res.Meta, "created_at": res.CreatedAt, "updated_at": res.UpdatedAt, "archived_at": res.ArchivedAt, diff --git a/store/rdbms/settings.gen.go b/store/rdbms/settings.gen.go index a9d8d17ed..f92009266 100644 --- a/store/rdbms/settings.gen.go +++ b/store/rdbms/settings.gen.go @@ -52,6 +52,7 @@ func (s Store) QuerySettings( check func(*types.SettingValue) (bool, error), ) ([]*types.SettingValue, error) { var ( + tmp = make([]*types.SettingValue, 0, DefaultSliceCapacity) set = make([]*types.SettingValue, 0, DefaultSliceCapacity) res *types.SettingValue @@ -73,6 +74,11 @@ func (s Store) QuerySettings( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + // check fn set, call it and see if it passed the test // if not, skip the item if check != nil { @@ -86,7 +92,7 @@ func (s Store) QuerySettings( set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupSettingByNameOwnedBy searches for settings by name and owner diff --git a/store/rdbms/templates.gen.go b/store/rdbms/templates.gen.go index 7b340141e..2be48202b 100644 --- a/store/rdbms/templates.gen.go +++ b/store/rdbms/templates.gen.go @@ -238,6 +238,7 @@ func (s Store) QueryTemplates( check func(*types.Template) (bool, error), ) ([]*types.Template, error) { var ( + tmp = make([]*types.Template, 0, DefaultSliceCapacity) set = make([]*types.Template, 0, DefaultSliceCapacity) res *types.Template @@ -259,6 +260,11 @@ func (s Store) QueryTemplates( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + // check fn set, call it and see if it passed the test // if not, skip the item if check != nil { @@ -272,7 +278,7 @@ func (s Store) QueryTemplates( set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupTemplateByID searches for template by ID diff --git a/store/rdbms/users.gen.go b/store/rdbms/users.gen.go index 7e71f64a1..bc1c192ca 100644 --- a/store/rdbms/users.gen.go +++ b/store/rdbms/users.gen.go @@ -238,6 +238,7 @@ func (s Store) QueryUsers( check func(*types.User) (bool, error), ) ([]*types.User, error) { var ( + tmp = make([]*types.User, 0, DefaultSliceCapacity) set = make([]*types.User, 0, DefaultSliceCapacity) res *types.User @@ -259,6 +260,11 @@ func (s Store) QueryUsers( return nil, err } + tmp = append(tmp, res) + } + + for _, res = range tmp { + // check fn set, call it and see if it passed the test // if not, skip the item if check != nil { @@ -272,7 +278,7 @@ func (s Store) QueryUsers( set = append(set, res) } - return set, rows.Err() + return set, nil } // LookupUserByID searches for user by ID diff --git a/store/rdbms/users.go b/store/rdbms/users.go index 888ca13bf..d1aed3196 100644 --- a/store/rdbms/users.go +++ b/store/rdbms/users.go @@ -59,9 +59,11 @@ func (s Store) convertUserFilter(f types.UserFilter) (query squirrel.SelectBuild query = query.Where(squirrel.Eq{"usr.handle": f.Handle}) } - //if f.Kind != "" { - // query = query.Where(squirrel.Eq{"usr.kind": f.Kind}) - //} + if !f.AllKinds { + // When not explicitly requested to search all kids of users, + // always limit to one kind + query = query.Where(squirrel.Eq{"usr.kind": f.Kind}) + } return } diff --git a/store/roles.yaml b/store/roles.yaml index 58b74b1b3..fd58d783f 100644 --- a/store/roles.yaml +++ b/store/roles.yaml @@ -5,6 +5,7 @@ fields: - { field: ID } - { field: Name, sortable: true } - { field: Handle, sortable: true, unique: true, lookupFilterPreprocessor: lower } + - { field: Meta, type: '*AuthClientMeta' } - { field: CreatedAt, sortable: true } - { field: UpdatedAt, sortable: true } - { field: ArchivedAt, sortable: true } diff --git a/store/tests/compose_records_test.go b/store/tests/compose_records_test.go index a6866893f..a3f7fd84d 100644 --- a/store/tests/compose_records_test.go +++ b/store/tests/compose_records_test.go @@ -1171,7 +1171,7 @@ func testComposeRecords(t *testing.T, s store.ComposeRecords) { if fields[f] == "updatedAt" { v := set[r].UpdatedAt if v != nil && !v.IsZero() { - out += v.Format(time.RFC3339) + out += v.UTC().Format(time.RFC3339) } else { out += "" } diff --git a/system/commands/rbac.go b/system/commands/rbac.go index 68d2560cd..5649c31ca 100644 --- a/system/commands/rbac.go +++ b/system/commands/rbac.go @@ -2,323 +2,245 @@ package commands import ( "context" + "sort" + "strconv" + "strings" + "github.com/cortezaproject/corteza-server/pkg/cli" + "github.com/cortezaproject/corteza-server/pkg/rbac" + "github.com/cortezaproject/corteza-server/pkg/slice" + "github.com/cortezaproject/corteza-server/store" + "github.com/cortezaproject/corteza-server/system/types" "github.com/spf13/cobra" ) -// Temporary solution, highly unstable, will change in the future! -//type ( -// rbacRoleOps map[string][]string -// -// rbacModule struct { -// res *cmptyp.Module -// rules rbac.RuleSet -// -// Allow rbacRoleOps `yaml:"allow"` -// Deny rbacRoleOps `yaml:"deny"` -// } -// -// rbacNamespace struct { -// res *cmptyp.Namespace -// rules rbac.RuleSet -// -// Allow rbacRoleOps `yaml:"allow"` -// Deny rbacRoleOps `yaml:"deny"` -// -// Modules map[string]*rbacModule `yaml:"modules"` -// } -// -// rbacRoot struct { -// Namespaces map[string]*rbacNamespace `yaml:"namespaces"` -// } -// -// //rbacRules map[string]permissions.RuleSet -// -// rbacPreloads struct { -// roles systyp.RoleSet -// namespaces cmptyp.NamespaceSet -// modules cmptyp.ModuleSet -// } -//) - -func RBAC(ctx context.Context, app serviceInitializer) *cobra.Command { +func RBAC(ctx context.Context, storeInit func(ctx context.Context) (store.Storer, error)) *cobra.Command { cmd := &cobra.Command{ Use: "rbac", Short: "RBAC tools", Long: "Check and manipulates permissions", } - //cmd.AddCommand(rbacCheck(app)) + cmd.AddCommand(rbacList(ctx, storeInit)) - //cmd.Flags().String("namespace", "", "Import into namespace (by ID or string)") + // @todo command that can grant/revoke/reset-all permissions + // in a similar format(s) as we do listing so that users can + // copy-paste the whole output, modify it and import it back return cmd } -//func rbacCheck(app serviceInitializer) *cobra.Command { -// return &cobra.Command{ -// Use: "check", -// Short: "Check applied permissions against given file (only supports compose permissions for now)", -// PreRunE: commandPreRunInitService(app), -// Run: func(cmd *cobra.Command, args []string) { -// var ( -// ctx = auth.SetSuperUserContext(cli.Context()) -// fh *os.File -// err error -// -// r = &rbacRoot{} -// -// p = rbacPreloads{} -// -// currentRules = rbac.Global().Rules() -// ) -// -// if len(args) > 0 { -// fh, err = os.Open(args[0]) -// cli.HandleError(err) -// defer fh.Close() -// } else { -// fh = os.Stdin -// } -// -// cli.HandleError(yaml.NewDecoder(fh).Decode(r)) -// -// p.roles, _, err = syssvc.DefaultRole.Find(ctx, systyp.RoleFilter{}) -// cli.HandleError(err) -// p.namespaces, _, err = cmpsvc.DefaultNamespace.Find(ctx, cmptyp.NamespaceFilter{}) -// cli.HandleError(err) -// p.modules, _, err = cmpsvc.DefaultModule.Find(ctx, cmptyp.ModuleFilter{}) -// cli.HandleError(err) -// -// fmt.Printf("Preloaded %d roles(s)\n", len(p.roles)) -// fmt.Printf("Preloaded %d namespaces(s)\n", len(p.namespaces)) -// fmt.Printf("Preloaded %d module(s)\n", len(p.modules)) -// fmt.Printf("Preloaded %d RBAC rule(s)\n", len(currentRules)) -// -// cli.HandleError(r.Resolve(p)) -// -// r.diagnose(currentRules, p) -// }, -// } -//} -// -////func (rr rbacRules) Merge(new rbacRules) rbacRules { -//// var out = rr -//// -//// for role, rules := range new { -//// if _, has := out[role]; has { -//// out[role] = append(out[role], rules...) -//// } else { -//// out[role] = rules -//// } -//// } -//// -//// // @todo implementation -//// return nil -////} -// -////func (rr rbacRules) Update(resource permissions.Resource, access permissions.Access) { -//// for _, rules := range rr { -//// for _, rule := range rules { -//// rule.Access = access -//// } -//// } -////} -// -////func (r rbacRoot) CollectRbacRules() rbacRules { -//// rr := rbacRules{} -//// -//// for _, ns := range r.Namespaces { -//// rr.Merge(ns.CollectRbacRules()) -//// } -//// -//// return rr -////} -// -//// Tranverses nodes and resolves references -//func (r *rbacRoot) Resolve(p rbacPreloads) (err error) { -// for handle, ns := range r.Namespaces { -// err = ns.Resolve(handle, p) -// if err != nil { -// return -// } -// } -// -// return nil -//} -// -////func (ns rbacNamespace) CollectRbacRules() rbacRules { -//// var ( -//// rr = rbacRules{} -//// -//// a = ns.Allow.CollectRbacRules() -//// d = ns.Deny.CollectRbacRules() -//// ) -//// -//// a.Update(cmptyp.NamespaceRBACResource, permissions.Allow) -//// d.Update(cmptyp.NamespaceRBACResource, permissions.Deny) -//// -//// rr = rr.Merge(a).Merge(d) -//// -//// for _, m := range ns.Modules { -//// rr = rr.Merge(m.CollectRbacRules()) -//// } -//// -//// return rr -////} -// -//func (ns *rbacNamespace) Resolve(nsHandle string, p rbacPreloads) error { -// ns.res = p.namespaces.FindByHandle(nsHandle) -// if ns.res == nil { -// return fmt.Errorf("could not find namespace by handle: %q", nsHandle) -// } -// -// for mHandle, m := range ns.Modules { -// if err := m.Resolve(mHandle, p); err != nil { -// return fmt.Errorf("failed to resolve module on namespace %q: %w", nsHandle, err) -// } -// } -// -// ns.rules = rbac.RuleSet{} -// -// if allows, err := ns.Allow.Resolve(ns.res.RBACResource(), rbac.Allow, p); err != nil { -// return fmt.Errorf("failed to resolve allow rules on namespace %q: %w", nsHandle, err) -// } else { -// ns.rules = append(ns.rules, allows...) -// } -// -// if allows, err := ns.Deny.Resolve(ns.res.RBACResource(), rbac.Allow, p); err != nil { -// return fmt.Errorf("failed to resolve deny rules on namespace %q: %w", nsHandle, err) -// } else { -// ns.rules = append(ns.rules, allows...) -// } -// -// return nil -//} -// -//func (ns *rbacNamespace) SortedModuleHandles() []string { -// out := []string{} -// for h := range ns.Modules { -// out = append(out, h) -// } -// -// sort.Strings(out) -// return out -//} -// -//func (m *rbacModule) Resolve(handle string, p rbacPreloads) error { -// var permRes rbac.Resource -// if handle != "*" { -// m.res = p.modules.FindByHandle(handle) -// if m.res == nil { -// return fmt.Errorf("could not find module by handle: %q", handle) -// } -// -// permRes = m.res.RBACResource() -// } else { -// permRes = cmptyp.ModuleRbacResource(0) -// } -// -// m.rules = rbac.RuleSet{} -// -// if allows, err := m.Allow.Resolve(permRes, rbac.Allow, p); err != nil { -// return fmt.Errorf("failed to resolve allow rules on module %q: %w", handle, err) -// } else { -// m.rules = append(m.rules, allows...) -// } -// -// if allows, err := m.Deny.Resolve(permRes, rbac.Allow, p); err != nil { -// return fmt.Errorf("failed to resolve deny rules on module %q: %w", handle, err) -// } else { -// m.rules = append(m.rules, allows...) -// } -// -// return nil -//} -// -//func (m *rbacModule) diagnose(currentRules rbac.RuleSet, p rbacPreloads) { -// // all modules -// var ( -// res = cmptyp.ModuleRbacResource(0) -// ) -// -// if m.res != nil { -// // specific module -// res = m.res.RBACResource() -// } -// -// // all rules that belong to the module -// currentRules = currentRules.ByResource(res) -// -// printRuleDiffs(currentRules, m.rules, rbac.Allow, p) -// printRuleDiffs(currentRules, m.rules, rbac.Deny, p) -// -//} -// -//func (rules rbacRoleOps) Resolve(res rbac.Resource, access rbac.Access, p rbacPreloads) (rbac.RuleSet, error) { -// prs := rbac.RuleSet{} -// -// for roleHandle, ops := range rules { -// role := p.roles.FindByHandle(roleHandle) -// if role == nil { -// return nil, fmt.Errorf("could not find role by handle: %q", roleHandle) -// } -// -// for _, op := range ops { -// prs = append(prs, &rbac.Rule{ -// RoleID: role.ID, -// Resource: res, -// Operation: rbac.Operation(op), -// Access: access, -// }) -// } -// } -// -// return prs, nil -//} -// -//func (r *rbacRoot) diagnose(c rbac.RuleSet, p rbacPreloads) { -// for _, ns := range r.Namespaces { -// fmt.Printf("=> [%d] %s\n", ns.res.ID, ns.res.Slug) -// fmt.Printf(" checking with %d module(s) from YAML\n", len(ns.Modules)) -// -// if all, has := ns.Modules["*"]; has { -// fmt.Printf(" => ** all modules **\n") -// all.diagnose(c, p) -// } -// -// for _, handle := range ns.SortedModuleHandles() { -// if handle == "*" { -// continue -// } -// -// m := ns.Modules[handle] -// -// if m.res == nil { -// fmt.Printf(" !! \033[33munresolved module with handle %q\033[39m\n", handle) -// continue -// } -// -// fmt.Printf(" => [%d] %s\n", m.res.ID, m.res.Handle) -// m.diagnose(c, p) -// } -// } -//} -// -//func printRuleDiffs(current, required rbac.RuleSet, a rbac.Access, p rbacPreloads) { -// diff := required.ByAccess(a).Diff(current.ByAccess(a)) -// -// if len(diff) > 0 { -// fmt.Printf(" \033[32mmissing %s rules (%d):\033[39m\n", a, len(diff)) -// for _, roleID := range diff.Roles() { -// role := p.roles.FindByID(roleID) -// fmt.Printf(" - [%d] %-20s: ", role.ID, role.Handle) -// for _, r := range diff.ByRole(roleID) { -// fmt.Printf(" %s", r.Operation) -// } -// fmt.Println() -// } -// } else { -// fmt.Printf(" \033[32mno missing %s rules\033[39m\n", a) -// } -//} +func rbacList(ctx context.Context, storeInit func(ctx context.Context) (store.Storer, error)) (cmd *cobra.Command) { + var ( + resources []string + roles []string + operations []string + groupBy string + allow, deny bool + + matchResources = func(r *rbac.Rule) bool { + if len(resources) == 0 { + return true + } + + for _, res := range resources { + if strings.HasPrefix(r.Resource, res) { + return true + } + } + + return false + } + + // makes a simple utiliy function for matching rules according to the used flags + ruleMatcher = func(s store.Storer) (map[uint64]*types.Role, func(r *rbac.Rule) bool) { + var ( + opsMap = slice.ToStringBoolMap(operations) + rr []*types.Role + rolAuxMap = slice.ToStringBoolMap(roles) + rolMap = make(map[uint64]*types.Role) + rolMatch = make(map[uint64]bool) + err error + ) + + rr, _, err = store.SearchRoles(ctx, s, types.RoleFilter{}) + for _, r := range rr { + rolMap[r.ID] = r + if rolAuxMap[r.Name] || rolAuxMap[r.Handle] || rolAuxMap[strconv.FormatUint(r.ID, 10)] { + rolMatch[r.ID] = true + } + } + + cli.HandleError(err) + + return rolMap, func(r *rbac.Rule) bool { + if !matchResources(r) { + return false + } + if len(opsMap) > 0 && !opsMap[r.Operation] { + return false + } + + // this use of rolAuxMap to check if there are roles specified in the flags + // and rolMap for checking if for actual role map is intentional! + if len(rolAuxMap) > 0 && !rolMatch[r.RoleID] { + return false + } + + if allow && r.Access != rbac.Allow { + return false + } + + if deny && r.Access != rbac.Deny { + return false + } + + return true + } + } + + ruleSorter = func(rr []*rbac.Rule) { + sort.SliceStable(rr, func(i, j int) bool { + switch groupBy { + case "role", "rMap": + return rr[i].RoleID < rr[j].RoleID + case "res", "resource", "resources": + return strings.Compare(rr[i].Resource, rr[i].Resource) < 0 + case "op", "ops", "operation", "operations": + return strings.Compare(rr[i].Operation, rr[j].Operation) < 0 + } + + return rr[i].Access < rr[j].Access + }) + } + + roleDisplayName = func(rMap map[uint64]*types.Role, r *rbac.Rule) (role string) { + if rMap[r.RoleID] != nil { + role = rMap[r.RoleID].Name + if role == "" { + role = rMap[r.RoleID].Handle + } + } + + if role == "" { + return strconv.FormatUint(r.RoleID, 10) + } + + return + } + + lengths = func(rr []*rbac.Rule, rMap map[uint64]*types.Role) (op, res, role int) { + for _, r := range rr { + if len(r.Operation) > op { + op = len(r.Operation) + } + if len(r.Resource) > res { + res = len(r.Resource) + } + } + for _, r := range rMap { + if len(r.Name) > role { + role = len(r.Name) + } + if len(r.Handle) > role { + role = len(r.Handle) + } + } + + return + } + ) + + cmd = &cobra.Command{ + Use: "list", + Short: "Check applied permissions against given file (only supports compose permissions for now)", + Run: func(cmd *cobra.Command, args []string) { + var ( + rr []*rbac.Rule + s, err = storeInit(ctx) + role string + gBucket string + ) + + cli.HandleError(err) + + rr, _, err = store.SearchRbacRules(cli.Context(), s, rbac.RuleFilter{}) + cli.HandleError(err) + + ruleSorter(rr) + rMap, matchRule := ruleMatcher(s) + + longestOp, longestRes, longestRole := lengths(rr, rMap) + + hr := strings.Repeat("-", longestOp+longestRes+longestRole+20) + + for i, r := range rr { + if !matchRule(r) { + continue + } + + r.Operation = r.Operation + strings.Repeat(" ", longestOp-len(r.Operation)) + r.Resource = r.Resource + strings.Repeat(" ", longestRes-len(r.Resource)) + + role = roleDisplayName(rMap, r) + role = role + strings.Repeat(" ", longestRole-len(role)) + + bOp := strings.Repeat(" ", longestOp) + bRes := strings.Repeat(" ", longestRes) + bRole := strings.Repeat(" ", longestRole) + + switch groupBy { + case "role", "roles": + if gBucket != role { + if i != 0 { + cmd.Println(hr) + } + cmd.Printf("%s %7s %s on %s\n", role, r.Access, r.Operation, r.Resource) + } else { + cmd.Printf("%s %7s %s on %s\n", bRole, r.Access, r.Operation, r.Resource) + } + + gBucket = role + + case "res", "resource", "resources": + if gBucket != r.Resource { + if i != 0 { + cmd.Println(hr) + } + + cmd.Printf("on %s %7s %s to %s\n", r.Resource, r.Access, role, r.Operation) + } else { + cmd.Printf(" %s %7s %s to %s\n", bRes, r.Access, role, r.Operation) + } + + gBucket = r.Resource + + case "op", "ops", "operation", "operations": + if gBucket != r.Operation { + if i != 0 { + cmd.Println(hr) + } + + cmd.Printf("%s %7s %s to %s\n", r.Operation, r.Access, role, r.Resource) + } else { + cmd.Printf("%s %7s %s to %s\n", bOp, r.Access, role, r.Resource) + } + + gBucket = r.Operation + default: + cmd.Printf("%7s %s to %s on %s\n", r.Access, role, r.Operation, r.Resource) + } + } + + }, + } + + cmd.Flags().StringArrayVarP(&resources, "resource", "r", nil, "Filter by resource (by prefix)") + cmd.Flags().StringArrayVarP(&roles, "role", "", nil, "Filter by role (handle or ID)") + cmd.Flags().StringArrayVarP(&operations, "operation", "o", nil, "Filter by operation") + cmd.Flags().BoolVarP(&deny, "deny", "d", false, "Show only deny") + cmd.Flags().BoolVarP(&allow, "allow", "a", false, "Show only allows") + cmd.Flags().StringVarP(&groupBy, "group", "g", "", "Group rules on output") + + return +} diff --git a/system/commands/roles.go b/system/commands/roles.go index 058e368cc..39124f9e6 100644 --- a/system/commands/roles.go +++ b/system/commands/roles.go @@ -4,6 +4,7 @@ import ( "context" "github.com/cortezaproject/corteza-server/pkg/cli" + "github.com/cortezaproject/corteza-server/store" "github.com/cortezaproject/corteza-server/system/service" "github.com/cortezaproject/corteza-server/system/types" "github.com/spf13/cobra" @@ -11,12 +12,21 @@ import ( func Roles(ctx context.Context, app serviceInitializer) *cobra.Command { cmd := &cobra.Command{ - Use: "roles", - Short: "Role management", + Use: "roles", + Aliases: []string{"role"}, + Short: "Role management", } - addUserCmd := &cobra.Command{ + cmd.AddCommand(rolesAddUser(ctx, app)) + cmd.AddCommand(rolesList(ctx, app)) + + return cmd +} + +func rolesAddUser(ctx context.Context, app serviceInitializer) *cobra.Command { + return &cobra.Command{ Use: "useradd [role-ID-or-name-or-handle] [user-ID-or-email]", + Aliases: []string{"assign", "add-user", "adduser", "user-add", "user"}, Short: "Add user to role", Args: cobra.ExactArgs(2), PreRunE: commandPreRunInitService(app), @@ -36,14 +46,33 @@ func Roles(ctx context.Context, app serviceInitializer) *cobra.Command { user, err = service.DefaultUser.FindByAny(ctx, userStr) cli.HandleError(err) - err = service.DefaultRole.MemberAdd(ctx, role.ID, user.ID) - cli.HandleError(err) + cli.HandleError(store.CreateRoleMember(ctx, service.DefaultStore, &types.RoleMember{ + RoleID: role.ID, + UserID: user.ID, + })) cmd.Printf("Added user [%d] %q to [%d] %q role\n", user.ID, user.Email, role.ID, role.Name) }, } - - cmd.AddCommand(addUserCmd) - - return cmd +} + +func rolesList(ctx context.Context, app serviceInitializer) *cobra.Command { + return &cobra.Command{ + Use: "list", + Short: "List all roles", + PreRunE: commandPreRunInitService(app), + Run: func(cmd *cobra.Command, args []string) { + f := types.RoleFilter{Query: ""} + if len(args) > 0 { + f.Query = args[0] + } + + rr, _, err := store.SearchRoles(ctx, service.DefaultStore, f) + cli.HandleError(err) + + for _, r := range rr { + cmd.Printf("%-20d %-30s %s\n", r.ID, r.Handle, r.Name) + } + }, + } } diff --git a/system/rest.yaml b/system/rest.yaml index c7bf918ce..c4032b328 100644 --- a/system/rest.yaml +++ b/system/rest.yaml @@ -42,7 +42,7 @@ endpoints: type: string - name: deleted required: false - title: Exclude (0, default), include (1) or return only (2) deleted roles + title: Exclude (0, default), include (1) or return only (2) deleted clients type: uint - type: map[string]string name: labels @@ -219,6 +219,7 @@ endpoints: - Session ID imports: - github.com/cortezaproject/corteza-server/pkg/label + - github.com/cortezaproject/corteza-server/system/types apis: - name: list method: GET @@ -226,80 +227,37 @@ endpoints: path: "/" parameters: get: - - type: string - name: query - required: false - title: Search query - - name: deleted - required: false - title: Exclude (0, default), include (1) or return only (2) deleted roles - type: uint - - name: archived - required: false - title: Exclude (0, default), include (1) or return only (2) achived roles - type: uint - - type: map[string]string - name: labels - title: Labels - parser: label.ParseStrings - - type: uint - name: limit - title: Limit - - type: string - name: pageCursor - title: Page cursor - - type: string - name: sort - title: Sort items + - { type: "string", name: "query", title: "Search query" } + - { type: "uint", name: "deleted", title: "Exclude (0, default), include (1) or return only (2) deleted roles" } + - { type: "uint", name: "archived", title: "Exclude (0, default), include (1) or return only (2) archived roles" } + - { type: "map[string]string", name: "labels", title: "Labels", parser: "label.ParseStrings" } + - { type: "uint", name: "limit", title: "Limit" } + - { type: "string", name: "pageCursor", title: "Page cursor" } + - { type: "string", name: "sort", title: "Sort items" } - name: create method: POST title: Update role details path: "/" parameters: post: - - type: string - name: name - required: true - title: Name of Role - - type: string - name: handle - required: true - title: Handle for Role - - type: "[]string" - name: members - required: false - title: Role member IDs - - type: map[string]string - name: labels - title: Labels - parser: label.ParseStrings + - { type: "string", name: "name", required: true, title: "Name of role" } + - { type: "string", name: "handle", required: true, title: "Handle for role" } + - { type: "[]string", name: "members", title: "role member IDs" } + - { type: "*types.RoleMeta", name: "meta", title: "Meta", parser: "types.ParseRoleMeta" } + - { type: "map[string]string", name: "labels", title: "Labels", parser: "label.ParseStrings" } - name: update method: PUT title: Update role details path: "/{roleID}" parameters: path: - - type: uint64 - name: roleID - required: true - title: Role ID + - { type: "uint64", name: "roleID", required: true, title: Role ID } post: - - type: string - name: name - required: false - title: Name of Role - - type: string - name: handle - required: false - title: Handle for Role - - type: "[]string" - name: members - required: false - title: Role member IDs - - type: map[string]string - name: labels - title: Labels - parser: label.ParseStrings + - { type: "string", name: "name", required: false, title: "Name of role" } + - { type: "string", name: "handle", required: false, title: "Handle for role" } + - { type: "[]string", name: "members", required: false, title: "role member IDs" } + - { type: "*types.RoleMeta", name: "meta", title: "Meta", parser: "types.ParseRoleMeta" } + - { type: "map[string]string", name: "labels", title: "Labels", parser: "label.ParseStrings" } - name: read method: GET title: Read role details and memberships diff --git a/system/rest/application.go b/system/rest/application.go index 32506e5f0..374c11ff4 100644 --- a/system/rest/application.go +++ b/system/rest/application.go @@ -33,6 +33,8 @@ type ( Delete(ctx context.Context, ID uint64) (err error) Undelete(ctx context.Context, ID uint64) (err error) Reorder(ctx context.Context, order []uint64) (err error) + Flag(ctx context.Context, app *types.Application, ownedBy uint64, f string) error + Unflag(ctx context.Context, app *types.Application, ownedBy uint64, f string) error } applicationAccessController interface { @@ -209,18 +211,8 @@ func (ctrl *Application) FlagCreate(ctx context.Context, r *request.ApplicationF return nil, err } - // @todo RBACv2 - //if r.OwnedBy == 0 { - // if !service.DefaultAccessControl.CanGlobalFlagApplication(ctx) { - // return nil, service.ApplicationErrNotAllowedToManageFlagGlobal() - // } - //} else { - // if !service.DefaultAccessControl.CanSelfFlagApplication(ctx) { - // return nil, service.ApplicationErrNotAllowedToManageFlag() - // } - //} + return api.OK(), ctrl.application.Flag(ctx, app, r.OwnedBy, r.Flag) - return api.OK(), flag.Create(ctx, service.DefaultStore, app, r.OwnedBy, r.Flag) } func (ctrl *Application) FlagDelete(ctx context.Context, r *request.ApplicationFlagDelete) (interface{}, error) { @@ -229,18 +221,7 @@ func (ctrl *Application) FlagDelete(ctx context.Context, r *request.ApplicationF return nil, err } - // @todo RBACv2 - // if r.OwnedBy == 0 { - // if !service.DefaultAccessControl.CanGlobalFlagApplication(ctx) { - // return nil, service.ApplicationErrNotAllowedToManageFlagGlobal() - // } - // } else { - // if !service.DefaultAccessControl.CanSelfFlagApplication(ctx) { - // return nil, service.ApplicationErrNotAllowedToManageFlag() - // } - // } - - return api.OK(), flag.Delete(ctx, service.DefaultStore, app, r.OwnedBy, r.Flag) + return api.OK(), ctrl.application.Unflag(ctx, app, r.OwnedBy, r.Flag) } func (ctrl Application) makePayload(ctx context.Context, m *types.Application, err error) (*applicationPayload, error) { diff --git a/system/rest/request/authClient.go b/system/rest/request/authClient.go index 880eefc94..c22fac9a5 100644 --- a/system/rest/request/authClient.go +++ b/system/rest/request/authClient.go @@ -44,7 +44,7 @@ type ( // Deleted GET parameter // - // Exclude (0, default), include (1) or return only (2) deleted roles + // Exclude (0, default), include (1) or return only (2) deleted clients Deleted uint // Labels GET parameter diff --git a/system/rest/request/role.go b/system/rest/request/role.go index 44ba8d397..449b123cb 100644 --- a/system/rest/request/role.go +++ b/system/rest/request/role.go @@ -13,6 +13,7 @@ import ( "fmt" "github.com/cortezaproject/corteza-server/pkg/label" "github.com/cortezaproject/corteza-server/pkg/payload" + "github.com/cortezaproject/corteza-server/system/types" "github.com/go-chi/chi" "io" "mime/multipart" @@ -47,7 +48,7 @@ type ( // Archived GET parameter // - // Exclude (0, default), include (1) or return only (2) achived roles + // Exclude (0, default), include (1) or return only (2) archived roles Archived uint // Labels GET parameter @@ -74,19 +75,24 @@ type ( RoleCreate struct { // Name POST parameter // - // Name of Role + // Name of role Name string // Handle POST parameter // - // Handle for Role + // Handle for role Handle string // Members POST parameter // - // Role member IDs + // role member IDs Members []string + // Meta POST parameter + // + // Meta + Meta *types.RoleMeta + // Labels POST parameter // // Labels @@ -101,19 +107,24 @@ type ( // Name POST parameter // - // Name of Role + // Name of role Name string // Handle POST parameter // - // Handle for Role + // Handle for role Handle string // Members POST parameter // - // Role member IDs + // role member IDs Members []string + // Meta POST parameter + // + // Meta + Meta *types.RoleMeta + // Labels POST parameter // // Labels @@ -346,6 +357,7 @@ func (r RoleCreate) Auditable() map[string]interface{} { "name": r.Name, "handle": r.Handle, "members": r.Members, + "meta": r.Meta, "labels": r.Labels, } } @@ -365,6 +377,11 @@ func (r RoleCreate) GetMembers() []string { return r.Members } +// Auditable returns all auditable/loggable parameters +func (r RoleCreate) GetMeta() *types.RoleMeta { + return r.Meta +} + // Auditable returns all auditable/loggable parameters func (r RoleCreate) GetLabels() map[string]string { return r.Labels @@ -412,6 +429,18 @@ func (r *RoleCreate) Fill(req *http.Request) (err error) { // } //} + if val, ok := req.Form["meta[]"]; ok { + r.Meta, err = types.ParseRoleMeta(val) + if err != nil { + return err + } + } else if val, ok := req.Form["meta"]; ok { + r.Meta, err = types.ParseRoleMeta(val) + if err != nil { + return err + } + } + if val, ok := req.Form["labels[]"]; ok { r.Labels, err = label.ParseStrings(val) if err != nil { @@ -440,6 +469,7 @@ func (r RoleUpdate) Auditable() map[string]interface{} { "name": r.Name, "handle": r.Handle, "members": r.Members, + "meta": r.Meta, "labels": r.Labels, } } @@ -464,6 +494,11 @@ func (r RoleUpdate) GetMembers() []string { return r.Members } +// Auditable returns all auditable/loggable parameters +func (r RoleUpdate) GetMeta() *types.RoleMeta { + return r.Meta +} + // Auditable returns all auditable/loggable parameters func (r RoleUpdate) GetLabels() map[string]string { return r.Labels @@ -511,6 +546,18 @@ func (r *RoleUpdate) Fill(req *http.Request) (err error) { // } //} + if val, ok := req.Form["meta[]"]; ok { + r.Meta, err = types.ParseRoleMeta(val) + if err != nil { + return err + } + } else if val, ok := req.Form["meta"]; ok { + r.Meta, err = types.ParseRoleMeta(val) + if err != nil { + return err + } + } + if val, ok := req.Form["labels[]"]; ok { r.Labels, err = label.ParseStrings(val) if err != nil { diff --git a/system/rest/role.go b/system/rest/role.go index ce3f9b14c..e7733302a 100644 --- a/system/rest/role.go +++ b/system/rest/role.go @@ -4,6 +4,7 @@ import ( "context" "github.com/cortezaproject/corteza-server/pkg/api" + "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/corredor" "github.com/cortezaproject/corteza-server/pkg/filter" "github.com/cortezaproject/corteza-server/pkg/payload" @@ -27,14 +28,20 @@ type ( CanUpdateRole(context.Context, *types.Role) bool CanDeleteRole(context.Context, *types.Role) bool + CanManageMembersOnRole(context.Context, *types.Role) bool } rolePayload struct { *types.Role - CanGrant bool `json:"canGrant"` - CanUpdateRole bool `json:"canUpdateRole"` - CanDeleteRole bool `json:"canDeleteRole"` + IsSystem bool `json:"isSystem"` + IsBypass bool `json:"isBypass"` + IsClosed bool `json:"isClosed"` + + CanGrant bool `json:"canGrant"` + CanUpdateRole bool `json:"canUpdateRole"` + CanDeleteRole bool `json:"canDeleteRole"` + CanManageMembersOnRole bool `json:"canManageMembersOnRole"` } roleSetPayload struct { @@ -86,6 +93,7 @@ func (ctrl Role) Create(ctx context.Context, r *request.RoleCreate) (interface{} Name: r.Name, Handle: r.Handle, Labels: r.Labels, + Meta: r.Meta, } ) @@ -111,6 +119,7 @@ func (ctrl Role) Update(ctx context.Context, r *request.RoleUpdate) (interface{} Name: r.Name, Handle: r.Handle, Labels: r.Labels, + Meta: r.Meta, } ) @@ -202,18 +211,22 @@ func (ctrl *Role) TriggerScript(ctx context.Context, r *request.RoleTriggerScrip return role, err } -func (ctrl Role) makePayload(ctx context.Context, m *types.Role, err error) (*rolePayload, error) { - if err != nil || m == nil { +func (ctrl Role) makePayload(ctx context.Context, r *types.Role, err error) (*rolePayload, error) { + if err != nil || r == nil { return nil, err } return &rolePayload{ - Role: m, + Role: r, - CanGrant: ctrl.ac.CanGrant(ctx), + CanGrant: ctrl.ac.CanGrant(ctx), + CanUpdateRole: ctrl.ac.CanUpdateRole(ctx, r), + CanDeleteRole: ctrl.ac.CanDeleteRole(ctx, r), + CanManageMembersOnRole: ctrl.ac.CanManageMembersOnRole(ctx, r), - CanUpdateRole: ctrl.ac.CanUpdateRole(ctx, m), - CanDeleteRole: ctrl.ac.CanDeleteRole(ctx, m), + IsSystem: ctrl.role.IsSystem(r), + IsClosed: ctrl.role.IsClosed(r), + IsBypass: auth.BypassRoles().FindByID(r.ID) != nil, }, nil } diff --git a/system/service/access_control.gen.go b/system/service/access_control.gen.go index 15fb422f6..583e2569f 100644 --- a/system/service/access_control.gen.go +++ b/system/service/access_control.gen.go @@ -17,11 +17,12 @@ package service import ( "context" "fmt" + "strings" + "github.com/cortezaproject/corteza-server/pkg/actionlog" "github.com/cortezaproject/corteza-server/pkg/rbac" "github.com/cortezaproject/corteza-server/system/types" "github.com/spf13/cast" - "strings" ) type ( @@ -60,43 +61,191 @@ func (svc accessControl) Effective(ctx context.Context, rr ...rbac.Resource) (ee } func (svc accessControl) List() (out []map[string]string) { - return []map[string]string{ - {"resource": "corteza+system.application", "operation": "read"}, - {"resource": "corteza+system.application", "operation": "update"}, - {"resource": "corteza+system.application", "operation": "delete"}, - {"resource": "corteza+system.auth-client", "operation": "read"}, - {"resource": "corteza+system.auth-client", "operation": "update"}, - {"resource": "corteza+system.auth-client", "operation": "delete"}, - {"resource": "corteza+system.auth-client", "operation": "authorize"}, - {"resource": "corteza+system.role", "operation": "read"}, - {"resource": "corteza+system.role", "operation": "update"}, - {"resource": "corteza+system.role", "operation": "delete"}, - {"resource": "corteza+system.role", "operation": "members.manage"}, - {"resource": "corteza+system.template", "operation": "read"}, - {"resource": "corteza+system.template", "operation": "update"}, - {"resource": "corteza+system.template", "operation": "delete"}, - {"resource": "corteza+system.template", "operation": "render"}, - {"resource": "corteza+system.user", "operation": "read"}, - {"resource": "corteza+system.user", "operation": "update"}, - {"resource": "corteza+system.user", "operation": "delete"}, - {"resource": "corteza+system.user", "operation": "suspend"}, - {"resource": "corteza+system.user", "operation": "unsuspend"}, - {"resource": "corteza+system.user", "operation": "email.unmask"}, - {"resource": "corteza+system.user", "operation": "name.unmask"}, - {"resource": "corteza+system.user", "operation": "impersonate"}, - {"resource": "corteza+system", "operation": "grant"}, - {"resource": "corteza+system", "operation": "settings.read"}, - {"resource": "corteza+system", "operation": "settings.manage"}, - {"resource": "corteza+system", "operation": "auth-client.create"}, - {"resource": "corteza+system", "operation": "role.create"}, - {"resource": "corteza+system", "operation": "user.create"}, - {"resource": "corteza+system", "operation": "application.create"}, - {"resource": "corteza+system", "operation": "application.flag.self"}, - {"resource": "corteza+system", "operation": "application.flag.global"}, - {"resource": "corteza+system", "operation": "template.create"}, - {"resource": "corteza+system", "operation": "reminder.assign"}, - {"resource": "corteza+system", "operation": "messagebus-queue.create"}, + def := []map[string]string{ + { + "type": types.ApplicationResourceType, + "any": types.ApplicationRbacResource(0), + "op": "read", + }, + { + "type": types.ApplicationResourceType, + "any": types.ApplicationRbacResource(0), + "op": "update", + }, + { + "type": types.ApplicationResourceType, + "any": types.ApplicationRbacResource(0), + "op": "delete", + }, + { + "type": types.AuthClientResourceType, + "any": types.AuthClientRbacResource(0), + "op": "read", + }, + { + "type": types.AuthClientResourceType, + "any": types.AuthClientRbacResource(0), + "op": "update", + }, + { + "type": types.AuthClientResourceType, + "any": types.AuthClientRbacResource(0), + "op": "delete", + }, + { + "type": types.AuthClientResourceType, + "any": types.AuthClientRbacResource(0), + "op": "authorize", + }, + { + "type": types.RoleResourceType, + "any": types.RoleRbacResource(0), + "op": "read", + }, + { + "type": types.RoleResourceType, + "any": types.RoleRbacResource(0), + "op": "update", + }, + { + "type": types.RoleResourceType, + "any": types.RoleRbacResource(0), + "op": "delete", + }, + { + "type": types.RoleResourceType, + "any": types.RoleRbacResource(0), + "op": "members.manage", + }, + { + "type": types.TemplateResourceType, + "any": types.TemplateRbacResource(0), + "op": "read", + }, + { + "type": types.TemplateResourceType, + "any": types.TemplateRbacResource(0), + "op": "update", + }, + { + "type": types.TemplateResourceType, + "any": types.TemplateRbacResource(0), + "op": "delete", + }, + { + "type": types.TemplateResourceType, + "any": types.TemplateRbacResource(0), + "op": "render", + }, + { + "type": types.UserResourceType, + "any": types.UserRbacResource(0), + "op": "read", + }, + { + "type": types.UserResourceType, + "any": types.UserRbacResource(0), + "op": "update", + }, + { + "type": types.UserResourceType, + "any": types.UserRbacResource(0), + "op": "delete", + }, + { + "type": types.UserResourceType, + "any": types.UserRbacResource(0), + "op": "suspend", + }, + { + "type": types.UserResourceType, + "any": types.UserRbacResource(0), + "op": "unsuspend", + }, + { + "type": types.UserResourceType, + "any": types.UserRbacResource(0), + "op": "email.unmask", + }, + { + "type": types.UserResourceType, + "any": types.UserRbacResource(0), + "op": "name.unmask", + }, + { + "type": types.UserResourceType, + "any": types.UserRbacResource(0), + "op": "impersonate", + }, + { + "type": types.ComponentResourceType, + "any": types.ComponentRbacResource(), + "op": "grant", + }, + { + "type": types.ComponentResourceType, + "any": types.ComponentRbacResource(), + "op": "settings.read", + }, + { + "type": types.ComponentResourceType, + "any": types.ComponentRbacResource(), + "op": "settings.manage", + }, + { + "type": types.ComponentResourceType, + "any": types.ComponentRbacResource(), + "op": "auth-client.create", + }, + { + "type": types.ComponentResourceType, + "any": types.ComponentRbacResource(), + "op": "role.create", + }, + { + "type": types.ComponentResourceType, + "any": types.ComponentRbacResource(), + "op": "user.create", + }, + { + "type": types.ComponentResourceType, + "any": types.ComponentRbacResource(), + "op": "application.create", + }, + { + "type": types.ComponentResourceType, + "any": types.ComponentRbacResource(), + "op": "application.flag.self", + }, + { + "type": types.ComponentResourceType, + "any": types.ComponentRbacResource(), + "op": "application.flag.global", + }, + { + "type": types.ComponentResourceType, + "any": types.ComponentRbacResource(), + "op": "template.create", + }, + { + "type": types.ComponentResourceType, + "any": types.ComponentRbacResource(), + "op": "reminder.assign", + }, + { + "type": types.ComponentResourceType, + "any": types.ComponentRbacResource(), + "op": "queue.create", + }, } + + func(svc interface{}) { + if svc, is := svc.(interface{}).(interface{ list() []map[string]string }); is { + def = append(def, svc.list()...) + } + }(svc) + + return def } // Grant applies one or more RBAC rules @@ -388,68 +537,68 @@ func (svc accessControl) CanAssignReminder(ctx context.Context) bool { return svc.can(ctx, "reminder.assign", &types.Component{}) } -// CanCreateMessagebusQueue checks if current user can create messagebus queues +// CanCreateQueue checks if current user can create messagebus queues // // This function is auto-generated -func (svc accessControl) CanCreateMessagebusQueue(ctx context.Context) bool { - return svc.can(ctx, "messagebus-queue.create", &types.Component{}) +func (svc accessControl) CanCreateQueue(ctx context.Context) bool { + return svc.can(ctx, "queue.create", &types.Component{}) } // rbacResourceValidator validates known component's resource by routing it to the appropriate validator // // This function is auto-generated func rbacResourceValidator(r string, oo ...string) error { - switch rbac.ResourceSchema(r) { - case "corteza+system.application": + switch rbac.ResourceType(r) { + case types.ApplicationResourceType: return rbacApplicationResourceValidator(r, oo...) - case "corteza+system.auth-client": + case types.AuthClientResourceType: return rbacAuthClientResourceValidator(r, oo...) - case "corteza+system.role": + case types.RoleResourceType: return rbacRoleResourceValidator(r, oo...) - case "corteza+system.template": + case types.TemplateResourceType: return rbacTemplateResourceValidator(r, oo...) - case "corteza+system.user": + case types.UserResourceType: return rbacUserResourceValidator(r, oo...) - case "corteza+system": + case types.ComponentResourceType: return rbacComponentResourceValidator(r, oo...) } - return fmt.Errorf("unknown resource schema '%q'", r) + return fmt.Errorf("unknown resource type '%q'", r) } // rbacResourceOperations returns defined operations for a requested resource // // This function is auto-generated func rbacResourceOperations(r string) map[string]bool { - switch rbac.ResourceSchema(r) { - case "corteza+system.application": + switch rbac.ResourceType(r) { + case types.ApplicationResourceType: return map[string]bool{ "read": true, "update": true, "delete": true, } - case "corteza+system.auth-client": + case types.AuthClientResourceType: return map[string]bool{ "read": true, "update": true, "delete": true, "authorize": true, } - case "corteza+system.role": + case types.RoleResourceType: return map[string]bool{ "read": true, "update": true, "delete": true, "members.manage": true, } - case "corteza+system.template": + case types.TemplateResourceType: return map[string]bool{ "read": true, "update": true, "delete": true, "render": true, } - case "corteza+system.user": + case types.UserResourceType: return map[string]bool{ "read": true, "update": true, @@ -460,7 +609,7 @@ func rbacResourceOperations(r string) map[string]bool { "name.unmask": true, "impersonate": true, } - case "corteza+system": + case types.ComponentResourceType: return map[string]bool{ "grant": true, "settings.read": true, @@ -473,7 +622,7 @@ func rbacResourceOperations(r string) map[string]bool { "application.flag.global": true, "template.create": true, "reminder.assign": true, - "messagebus-queue.create": true, + "queue.create": true, } } @@ -493,34 +642,38 @@ func rbacApplicationResourceValidator(r string, oo ...string) error { } } - if !strings.HasPrefix(r, types.ApplicationRbacResourceSchema+":/") { - return fmt.Errorf("invalid schema") - } - - pp := strings.Split(r[len(types.ApplicationRbacResourceSchema)+2:], "/") - if len(pp) != 1 { - return fmt.Errorf("invalid resource path") + if !strings.HasPrefix(r, types.ApplicationResourceType) { + // expecting resource to always include path + return fmt.Errorf("invalid resource type") } + const sep = "/" var ( - ppWildcard bool - pathElements = []string{ + specIdUsed = true + + pp = strings.Split(strings.Trim(r[len(types.ApplicationResourceType):], sep), sep) + prc = []string{ "ID", } ) + if len(pp) != len(prc) { + return fmt.Errorf("invalid resource path structure") + } + for i, p := range pp { if p == "*" { - ppWildcard = true + if !specIdUsed { + return fmt.Errorf("invalid resource path wildcard level (%d) for Application", i) + } + + specIdUsed = false continue } - if !ppWildcard { - return fmt.Errorf("invalid resource path wildcard level") - } - + specIdUsed = true if _, err := cast.ToUint64E(p); err != nil { - return fmt.Errorf("invalid ID for %s: '%s'", pathElements[i], p) + return fmt.Errorf("invalid reference for %s: '%s'", prc[i], p) } } @@ -540,34 +693,38 @@ func rbacAuthClientResourceValidator(r string, oo ...string) error { } } - if !strings.HasPrefix(r, types.AuthClientRbacResourceSchema+":/") { - return fmt.Errorf("invalid schema") - } - - pp := strings.Split(r[len(types.AuthClientRbacResourceSchema)+2:], "/") - if len(pp) != 1 { - return fmt.Errorf("invalid resource path") + if !strings.HasPrefix(r, types.AuthClientResourceType) { + // expecting resource to always include path + return fmt.Errorf("invalid resource type") } + const sep = "/" var ( - ppWildcard bool - pathElements = []string{ + specIdUsed = true + + pp = strings.Split(strings.Trim(r[len(types.AuthClientResourceType):], sep), sep) + prc = []string{ "ID", } ) + if len(pp) != len(prc) { + return fmt.Errorf("invalid resource path structure") + } + for i, p := range pp { if p == "*" { - ppWildcard = true + if !specIdUsed { + return fmt.Errorf("invalid resource path wildcard level (%d) for AuthClient", i) + } + + specIdUsed = false continue } - if !ppWildcard { - return fmt.Errorf("invalid resource path wildcard level") - } - + specIdUsed = true if _, err := cast.ToUint64E(p); err != nil { - return fmt.Errorf("invalid ID for %s: '%s'", pathElements[i], p) + return fmt.Errorf("invalid reference for %s: '%s'", prc[i], p) } } @@ -587,34 +744,38 @@ func rbacRoleResourceValidator(r string, oo ...string) error { } } - if !strings.HasPrefix(r, types.RoleRbacResourceSchema+":/") { - return fmt.Errorf("invalid schema") - } - - pp := strings.Split(r[len(types.RoleRbacResourceSchema)+2:], "/") - if len(pp) != 1 { - return fmt.Errorf("invalid resource path") + if !strings.HasPrefix(r, types.RoleResourceType) { + // expecting resource to always include path + return fmt.Errorf("invalid resource type") } + const sep = "/" var ( - ppWildcard bool - pathElements = []string{ + specIdUsed = true + + pp = strings.Split(strings.Trim(r[len(types.RoleResourceType):], sep), sep) + prc = []string{ "ID", } ) + if len(pp) != len(prc) { + return fmt.Errorf("invalid resource path structure") + } + for i, p := range pp { if p == "*" { - ppWildcard = true + if !specIdUsed { + return fmt.Errorf("invalid resource path wildcard level (%d) for Role", i) + } + + specIdUsed = false continue } - if !ppWildcard { - return fmt.Errorf("invalid resource path wildcard level") - } - + specIdUsed = true if _, err := cast.ToUint64E(p); err != nil { - return fmt.Errorf("invalid ID for %s: '%s'", pathElements[i], p) + return fmt.Errorf("invalid reference for %s: '%s'", prc[i], p) } } @@ -634,34 +795,38 @@ func rbacTemplateResourceValidator(r string, oo ...string) error { } } - if !strings.HasPrefix(r, types.TemplateRbacResourceSchema+":/") { - return fmt.Errorf("invalid schema") - } - - pp := strings.Split(r[len(types.TemplateRbacResourceSchema)+2:], "/") - if len(pp) != 1 { - return fmt.Errorf("invalid resource path") + if !strings.HasPrefix(r, types.TemplateResourceType) { + // expecting resource to always include path + return fmt.Errorf("invalid resource type") } + const sep = "/" var ( - ppWildcard bool - pathElements = []string{ + specIdUsed = true + + pp = strings.Split(strings.Trim(r[len(types.TemplateResourceType):], sep), sep) + prc = []string{ "ID", } ) + if len(pp) != len(prc) { + return fmt.Errorf("invalid resource path structure") + } + for i, p := range pp { if p == "*" { - ppWildcard = true + if !specIdUsed { + return fmt.Errorf("invalid resource path wildcard level (%d) for Template", i) + } + + specIdUsed = false continue } - if !ppWildcard { - return fmt.Errorf("invalid resource path wildcard level") - } - + specIdUsed = true if _, err := cast.ToUint64E(p); err != nil { - return fmt.Errorf("invalid ID for %s: '%s'", pathElements[i], p) + return fmt.Errorf("invalid reference for %s: '%s'", prc[i], p) } } @@ -681,34 +846,38 @@ func rbacUserResourceValidator(r string, oo ...string) error { } } - if !strings.HasPrefix(r, types.UserRbacResourceSchema+":/") { - return fmt.Errorf("invalid schema") - } - - pp := strings.Split(r[len(types.UserRbacResourceSchema)+2:], "/") - if len(pp) != 1 { - return fmt.Errorf("invalid resource path") + if !strings.HasPrefix(r, types.UserResourceType) { + // expecting resource to always include path + return fmt.Errorf("invalid resource type") } + const sep = "/" var ( - ppWildcard bool - pathElements = []string{ + specIdUsed = true + + pp = strings.Split(strings.Trim(r[len(types.UserResourceType):], sep), sep) + prc = []string{ "ID", } ) + if len(pp) != len(prc) { + return fmt.Errorf("invalid resource path structure") + } + for i, p := range pp { if p == "*" { - ppWildcard = true + if !specIdUsed { + return fmt.Errorf("invalid resource path wildcard level (%d) for User", i) + } + + specIdUsed = false continue } - if !ppWildcard { - return fmt.Errorf("invalid resource path wildcard level") - } - + specIdUsed = true if _, err := cast.ToUint64E(p); err != nil { - return fmt.Errorf("invalid ID for %s: '%s'", pathElements[i], p) + return fmt.Errorf("invalid reference for %s: '%s'", prc[i], p) } } @@ -728,8 +897,9 @@ func rbacComponentResourceValidator(r string, oo ...string) error { } } - if !strings.HasPrefix(r, types.ComponentRbacResourceSchema+":/") { - return fmt.Errorf("invalid schema") + if !strings.HasPrefix(r, types.ComponentResourceType) { + // expecting resource to always include path + return fmt.Errorf("invalid resource type") } return nil diff --git a/system/service/access_control.go b/system/service/access_control.go new file mode 100644 index 000000000..7560a6c64 --- /dev/null +++ b/system/service/access_control.go @@ -0,0 +1,54 @@ +package service + +import ( + "context" + + "github.com/cortezaproject/corteza-server/pkg/messagebus" +) + +// Addition to list of defined resouces +// we need that because messagebus resource is not compliant +func (svc accessControl) list() []map[string]string { + return []map[string]string{ + {"type": messagebus.QueueResourceType, "any": messagebus.QueueRbacResource(0), "op": "read"}, + {"type": messagebus.QueueResourceType, "any": messagebus.QueueRbacResource(0), "op": "update"}, + {"type": messagebus.QueueResourceType, "any": messagebus.QueueRbacResource(0), "op": "delete"}, + {"type": messagebus.QueueResourceType, "any": messagebus.QueueRbacResource(0), "op": "queue.read"}, + {"type": messagebus.QueueResourceType, "any": messagebus.QueueRbacResource(0), "op": "queue.write"}, + } +} + +// CanReadQueue checks if current user can read queue +// +// This function is auto-generated +func (svc accessControl) CanReadQueue(ctx context.Context, r *messagebus.QueueSettings) bool { + return svc.can(ctx, "read", r) +} + +// CanUpdateQueue checks if current user can update queue +// +// This function is auto-generated +func (svc accessControl) CanUpdateQueue(ctx context.Context, r *messagebus.QueueSettings) bool { + return svc.can(ctx, "update", r) +} + +// CanDeleteQueue checks if current user can delete queue +// +// This function is auto-generated +func (svc accessControl) CanDeleteQueue(ctx context.Context, r *messagebus.QueueSettings) bool { + return svc.can(ctx, "delete", r) +} + +// CanReadMessageOnQueue checks if current user can read from queue +// +// This function is auto-generated +func (svc accessControl) CanReadMessageOnQueue(ctx context.Context, r *messagebus.QueueSettings) bool { + return svc.can(ctx, "queue.read", r) +} + +// CanWriteMessageOnQueue checks if current user can write to queue +// +// This function is auto-generated +func (svc accessControl) CanWriteMessageOnQueue(ctx context.Context, r *messagebus.QueueSettings) bool { + return svc.can(ctx, "queue.write", r) +} diff --git a/system/service/application.go b/system/service/application.go index f8a802d3b..1126b3732 100644 --- a/system/service/application.go +++ b/system/service/application.go @@ -23,6 +23,8 @@ type ( applicationAccessController interface { CanCreateApplication(context.Context) bool + CanSelfApplicationFlag(context.Context) bool + CanGlobalApplicationFlag(context.Context) bool CanReadApplication(context.Context, *types.Application) bool CanUpdateApplication(context.Context, *types.Application) bool CanDeleteApplication(context.Context, *types.Application) bool @@ -309,6 +311,36 @@ func (svc *application) Undelete(ctx context.Context, ID uint64) (err error) { return svc.recordAction(ctx, aaProps, ApplicationActionUndelete, err) } +func (svc *application) Flag(ctx context.Context, app *types.Application, ownedBy uint64, f string) error { + if err := svc.checkFlag(ctx, ownedBy); err != nil { + return err + } + + return flag.Create(ctx, svc.store, app, ownedBy, f) +} + +func (svc *application) Unflag(ctx context.Context, app *types.Application, ownedBy uint64, f string) error { + if err := svc.checkFlag(ctx, ownedBy); err != nil { + return err + } + + return flag.Delete(ctx, svc.store, app, ownedBy, f) +} + +func (svc *application) checkFlag(ctx context.Context, ownedBy uint64) error { + if ownedBy == 0 { + if !svc.ac.CanGlobalApplicationFlag(ctx) { + return ApplicationErrNotAllowedToManageFlagGlobal() + } + } else { + if ownedBy != a.GetIdentityFromContext(ctx).Identity() || !svc.ac.CanSelfApplicationFlag(ctx) { + return ApplicationErrNotAllowedToManageFlag() + } + } + + return nil +} + func (svc *application) Reorder(ctx context.Context, order []uint64) (err error) { var ( aProps = &applicationActionProps{} diff --git a/system/service/auth.go b/system/service/auth.go index a7b795f7c..259afaf15 100644 --- a/system/service/auth.go +++ b/system/service/auth.go @@ -957,30 +957,32 @@ func (svc auth) createUserToken(ctx context.Context, u *types.User, kind string) return token, svc.recordAction(ctx, aam, AuthActionIssueToken, err) } -// Automatically promotes user to administrator if it is the first user in the database +// Automatically promotes user to super-administrator if it is the first non-system user in the database func (svc auth) autoPromote(ctx context.Context, u *types.User) (err error) { var ( - c uint - roleID uint64 = 2 - aam = &authActionProps{user: u, role: &types.Role{ID: roleID}} + c uint + aam = &authActionProps{user: u, role: &types.Role{}} ) - // @todo RBACv2 - return fmt.Errorf("failed to auto-promote user") + if c, err = store.CountUsers(ctx, svc.store, types.UserFilter{Kind: types.NormalUser}); err != nil { + return err + } - err = func() error { - if c, err = store.CountUsers(ctx, svc.store, types.UserFilter{}); err != nil { + if c > 1 || u.ID == 0 { + return nil + } + + for _, r := range internalAuth.BypassRoles() { + m := &types.RoleMember{UserID: u.ID, RoleID: r.ID} + if err = store.CreateRoleMember(ctx, svc.store, m); err != nil { return err } - if c > 1 || u.ID == 0 { - return nil - } + aam.role = r + _ = svc.recordAction(ctx, aam, AuthActionAutoPromote, nil) + } - return store.CreateRoleMember(ctx, svc.store, &types.RoleMember{RoleID: roleID, UserID: u.ID}) - }() - - return svc.recordAction(ctx, aam, AuthActionAutoPromote, err) + return nil } // ValidateTOTP checks given code with the current secret @@ -1323,7 +1325,7 @@ func (svc auth) LoadRoleMemberships(ctx context.Context, u *types.User) error { return err } - u.SetRoles(rr.IDs()) + u.SetRoles(rr.IDs()...) return nil } diff --git a/system/service/queue.go b/system/service/queue.go index 12cde53d7..8a3f51a81 100644 --- a/system/service/queue.go +++ b/system/service/queue.go @@ -16,19 +16,16 @@ type ( } queueAccessController interface { - CanCreateMessagebusQueue(ctx context.Context) bool - CanReadMessagebusQueue(ctx context.Context, c *messagebus.QueueSettings) bool - CanUpdateMessagebusQueue(ctx context.Context, c *messagebus.QueueSettings) bool - CanDeleteMessagebusQueue(ctx context.Context, c *messagebus.QueueSettings) bool - - CanReadFromMessagebusQueue(ctx context.Context, c *messagebus.QueueSettings) bool - CanWriteToMessagebusQueue(ctx context.Context, c *messagebus.QueueSettings) bool + CanCreateQueue(ctx context.Context) bool + CanReadQueue(ctx context.Context, c *messagebus.QueueSettings) bool + CanUpdateQueue(ctx context.Context, c *messagebus.QueueSettings) bool + CanDeleteQueue(ctx context.Context, c *messagebus.QueueSettings) bool } ) func Queue() *queue { return &queue{ - //ac: DefaultAccessControl, + ac: DefaultAccessControl, actionlog: DefaultActionlog, store: DefaultStore, } @@ -50,7 +47,7 @@ func (svc *queue) FindByID(ctx context.Context, ID uint64) (q *messagebus.QueueS qProps.setQueue(q) - if !svc.ac.CanReadMessagebusQueue(ctx, q) { + if !svc.ac.CanReadQueue(ctx, q) { return QueueErrNotAllowedToRead(qProps) } @@ -66,7 +63,7 @@ func (svc *queue) Create(ctx context.Context, new *messagebus.QueueSettings) (q ) err = func() (err error) { - if !svc.ac.CanCreateMessagebusQueue(ctx) { + if !svc.ac.CanCreateQueue(ctx) { return QueueErrNotAllowedToCreate(qProps) } @@ -101,7 +98,7 @@ func (svc *queue) Update(ctx context.Context, upd *messagebus.QueueSettings) (q ) err = func() (err error) { - if !svc.ac.CanUpdateMessagebusQueue(ctx, upd) { + if !svc.ac.CanUpdateQueue(ctx, upd) { return QueueErrNotAllowedToUpdate(qProps) } @@ -153,7 +150,7 @@ func (svc *queue) DeleteByID(ctx context.Context, ID uint64) (err error) { qProps.setQueue(q) - if !svc.ac.CanDeleteMessagebusQueue(ctx, q) { + if !svc.ac.CanDeleteQueue(ctx, q) { return QueueErrNotAllowedToDelete(qProps) } @@ -188,7 +185,7 @@ func (svc *queue) UndeleteByID(ctx context.Context, ID uint64) (err error) { qProps.setQueue(q) - if !svc.ac.CanDeleteMessagebusQueue(ctx, q) { + if !svc.ac.CanDeleteQueue(ctx, q) { return QueueErrNotAllowedToDelete(qProps) } @@ -213,7 +210,7 @@ func (svc *queue) Search(ctx context.Context, filter messagebus.QueueSettingsFil // For each fetched item, store backend will check if it is valid or not filter.Check = func(res *messagebus.QueueSettings) (bool, error) { - if !svc.ac.CanReadMessagebusQueue(ctx, res) { + if !svc.ac.CanReadQueue(ctx, res) { return false, nil } diff --git a/system/service/role.go b/system/service/role.go index 3654a8866..fc9525afd 100644 --- a/system/service/role.go +++ b/system/service/role.go @@ -19,7 +19,6 @@ import ( "github.com/cortezaproject/corteza-server/store" "github.com/cortezaproject/corteza-server/system/service/event" "github.com/cortezaproject/corteza-server/system/types" - "go.uber.org/zap" ) @@ -56,6 +55,9 @@ type ( FindByAny(ctx context.Context, identifier interface{}) (*types.Role, error) Find(context.Context, types.RoleFilter) (types.RoleSet, types.RoleFilter, error) + IsSystem(r *types.Role) bool + IsClosed(r *types.Role) bool + Create(ctx context.Context, role *types.Role) (*types.Role, error) Update(ctx context.Context, role *types.Role) (*types.Role, error) @@ -97,19 +99,27 @@ func Role() *role { // SetSystem sets list of handles for all system roles // // System roles can not be changed or deleted -func (svc role) SetSystem(hh ...string) { +func (svc *role) SetSystem(hh ...string) { svc.system = slice.ToStringBoolMap(hh) delete(svc.system, "") } +func (svc role) IsSystem(r *types.Role) bool { + return len(r.Handle) > 0 && svc.system[r.Handle] +} + // SetClosed sets list of handles for all closed roles // // Closed roles can not have members -func (svc role) SetClosed(hh ...string) { +func (svc *role) SetClosed(hh ...string) { svc.closed = slice.ToStringBoolMap(hh) delete(svc.closed, "") } +func (svc role) IsClosed(r *types.Role) bool { + return len(r.Handle) > 0 && svc.closed[r.Handle] +} + func (svc role) Find(ctx context.Context, filter types.RoleFilter) (rr types.RoleSet, f types.RoleFilter, err error) { var ( raProps = &roleActionProps{filter: &filter} @@ -285,6 +295,12 @@ func (svc role) Create(ctx context.Context, new *types.Role) (r *types.Role, err return } + if new.Meta != nil && new.Meta.Context != nil { + if err = svc.validateContext(ctx, new.Meta.Context); err != nil { + return + } + } + if err = svc.UniqueCheck(ctx, new); err != nil { return } @@ -332,7 +348,7 @@ func (svc role) Update(ctx context.Context, upd *types.Role) (r *types.Role, err return } - if svc.system[r.Handle] { + if svc.IsSystem(r) { return RoleErrNotAllowedToUpdate() } @@ -342,12 +358,19 @@ func (svc role) Update(ctx context.Context, upd *types.Role) (r *types.Role, err return } + if upd.Meta != nil && upd.Meta.Context != nil { + if err = svc.validateContext(ctx, upd.Meta.Context); err != nil { + return + } + } + if err = svc.UniqueCheck(ctx, upd); err != nil { return } r.Handle = upd.Handle r.Name = upd.Name + r.Meta = upd.Meta r.UpdatedAt = now() // Assign changed values @@ -393,6 +416,24 @@ func (svc role) UniqueCheck(ctx context.Context, r *types.Role) (err error) { return nil } +// validateContext validates role context expression +func (svc role) validateContext(ctx context.Context, r *types.RoleContext) error { + if len(strings.TrimSpace(r.Expr)) == 0 { + return nil + } + + _, err := expr.NewParser().Parse(r.Expr) + if err != nil { + return err + } + + // this is really as much as we can validate at this point + // any further validation of the expression through actual execution + // would require us to bring in information about resources from non-system components + // and figuring out the exact module structure when using this with record values + return nil +} + func (svc role) Delete(ctx context.Context, roleID uint64) (err error) { var ( r *types.Role @@ -404,7 +445,7 @@ func (svc role) Delete(ctx context.Context, roleID uint64) (err error) { return err } - if svc.system[r.Handle] { + if svc.IsSystem(r) { return RoleErrNotAllowedToDelete() } @@ -443,7 +484,7 @@ func (svc role) Undelete(ctx context.Context, roleID uint64) (err error) { return err } - if svc.system[r.Handle] { + if svc.IsSystem(r) { return RoleErrNotAllowedToUndelete() } @@ -476,7 +517,7 @@ func (svc role) Archive(ctx context.Context, roleID uint64) (err error) { return err } - if svc.system[r.Handle] { + if svc.IsSystem(r) { return RoleErrNotAllowedToArchive() } @@ -508,7 +549,7 @@ func (svc role) Unarchive(ctx context.Context, roleID uint64) (err error) { return err } - if svc.system[r.Handle] { + if svc.IsSystem(r) { return RoleErrNotAllowedToUnarchive() } @@ -548,14 +589,14 @@ func (svc role) MemberList(ctx context.Context, roleID uint64) (mm types.RoleMem return RoleErrInvalidID() } - if svc.closed[r.Handle] { - return RoleErrNotAllowedToManageMembers() - } - if r, err = svc.findByID(ctx, roleID); err != nil { return err } + if svc.IsClosed(r) { + return RoleErrNotAllowedToManageMembers() + } + if !svc.ac.CanReadRole(ctx, r) { return RoleErrNotAllowedToRead() } @@ -584,16 +625,16 @@ func (svc role) MemberAdd(ctx context.Context, roleID, memberID uint64) (err err return RoleErrInvalidID() } - if svc.closed[r.Handle] { - return RoleErrNotAllowedToManageMembers() - } - if r, err = svc.findByID(ctx, roleID); err != nil { return } raProps.setRole(r) + if svc.IsClosed(r) { + return RoleErrNotAllowedToManageMembers() + } + if m, err = svc.user.FindByID(ctx, memberID); err != nil { return } @@ -635,14 +676,14 @@ func (svc role) MemberRemove(ctx context.Context, roleID, memberID uint64) (err return RoleErrInvalidID() } - if svc.closed[r.Handle] { - return RoleErrNotAllowedToManageMembers() - } - if r, err = svc.findByID(ctx, roleID); err != nil { return } + if svc.IsClosed(r) { + return RoleErrNotAllowedToManageMembers() + } + raProps.setRole(r) if m, err = svc.user.FindByID(ctx, memberID); err != nil { @@ -686,9 +727,9 @@ func toLabeledRoles(set []*types.Role) []label.LabeledResource { return ll } -// Configures RBAC with roles +// Initializes roles to RBAC and default role service // -// Sets all closed & im +// Sets all closed & system roles func initRoles(ctx context.Context, log *zap.Logger, opt options.RBACOpt, eb eventbusRoleChangeRegistry, ru rbacRoleUpdater) (err error) { var ( // splits space separated string into map @@ -759,13 +800,17 @@ func initRoles(ctx context.Context, log *zap.Logger, opt options.RBACOpt, eb eve } } - DefaultRole.SetSystem(j(bypass, authenticated, anonymous)...) - DefaultRole.SetClosed(j(authenticated, anonymous)...) + tmp := j(bypass, authenticated, anonymous) + log.Debug("setting system roles", zap.Strings("roles", tmp)) + DefaultRole.SetSystem(tmp...) + + tmp = j(authenticated, anonymous) + log.Debug("setting closed roles", zap.Strings("roles", tmp)) + DefaultRole.SetClosed(tmp...) // Initial RBAC update - err = updateRbacRoles(ctx, log, ru, bypass, authenticated, anonymous) - if err != nil { - + if err = updateRbacRoles(ctx, log, ru, bypass, authenticated, anonymous); err != nil { + return } // Hook to role create, update & delete events and @@ -808,43 +853,53 @@ func updateRbacRoles(ctx context.Context, log *zap.Logger, ru rbacRoleUpdater, b case bypass[r.Handle]: countBypass++ rr = append(rr, rbac.BypassRole.Make(r.ID, r.Handle)) + log.Debug("bypass role added") case anonymous[r.Handle]: countAnony++ rr = append(rr, rbac.AnonymousRole.Make(r.ID, r.Handle)) + log.Debug("anonymous role added") case authenticated[r.Handle]: countAuth++ rr = append(rr, rbac.AuthenticatedRole.Make(r.ID, r.Handle)) + log.Debug("authenticated role added") case r.Meta != nil && r.Meta.Context != nil && len(r.Meta.Context.Expr) > 0: log := log.With(zap.String("expr", r.Meta.Context.Expr)) eval, err := p.Parse(r.Meta.Context.Expr) if err != nil { - log.Error("failed to parse role context expression", zap.Error(err)) + log.Warn("failed to parse role context expression, defaulting to deny", zap.Error(err)) + + rr = append(rr, rbac.MakeContextRole(r.ID, r.Handle, func(_ map[string]interface{}) bool { + log.Warn("role context expression not parsed, fallback to deny", zap.Error(err)) + return false + })) continue } - check := func(s map[string]interface{}) bool { - vars, err := expr.NewVars(s) + check := func(scope map[string]interface{}) bool { + vars, err := expr.NewVars(scope) if err != nil { - log.Error("failed to convert check scope to expr.Vars", zap.Error(err)) + log.Warn("failed to convert check scope to expr.Vars, fallback to deny", zap.Error(err)) return false } test, err := eval.Test(ctx, vars) if err != nil { - log.Error("failed to evaluate role context expression", zap.Error(err)) + log.Warn("failed to evaluate role context expression, fallback to deny", zap.Error(err)) return false } return test } - rr = append(rr, rbac.MakeContextRole(r.ID, r.Handle, check)) + rr = append(rr, rbac.MakeContextRole(r.ID, r.Handle, check, r.Meta.Context.Resource...)) + log.Debug("context role added") default: rr = append(rr, rbac.CommonRole.Make(r.ID, r.Handle)) + log.Debug("common role added") } } diff --git a/system/service/user.go b/system/service/user.go index bed13a6f5..0f8e357f9 100644 --- a/system/service/user.go +++ b/system/service/user.go @@ -235,7 +235,7 @@ func (svc user) FindByAny(ctx context.Context, identifier interface{}) (u *types return nil, err } - u.SetRoles(rr.IDs()) + u.SetRoles(rr.IDs()...) return } @@ -419,7 +419,6 @@ func (svc user) Update(ctx context.Context, upd *types.User) (u *types.User, err return UserErrNotAllowedToUpdateSystem() } - // @todo RBACv2 this could/should be solved via context roles if upd.ID != internalAuth.GetIdentityFromContext(ctx).Identity() { if !svc.ac.CanUpdateUser(ctx, u) { return UserErrNotAllowedToUpdate() diff --git a/system/service/user_test.go b/system/service/user_test.go index bb4d589e6..d61c6bc74 100644 --- a/system/service/user_test.go +++ b/system/service/user_test.go @@ -14,33 +14,6 @@ import ( "go.uber.org/zap" ) -// Mock auth service with nil for current time, dummy provider validator and mock db -func makeMockUserService() *user { - var ( - ctx = context.Background() - - mem, err = sqlite3.ConnectInMemory(ctx) - - svc = &user{ - settings: &types.AppSettings{}, - ac: &accessControl{rbac: rbac.NewService(zap.NewNop(), mem)}, - eventbus: eventbus.New(), - } - ) - - if err != nil { - panic(err) - } - - if err = store.Upgrade(ctx, zap.NewNop(), mem); err != nil { - panic(err) - } - - svc.store = mem - - return svc -} - func TestUser_ProtectedSearch(t *testing.T) { const testRoleID = 123 @@ -55,20 +28,37 @@ func TestUser_ProtectedSearch(t *testing.T) { err error testUser = &types.User{ID: 42} + + acRBAC = rbac.NewService(zap.NewNop(), nil) + + s store.Storer ) - testUser.SetRoles([]uint64{testRoleID}) + if s, err = sqlite3.ConnectInMemory(ctx); err != nil { + req.NoError(err) + } else if err = store.Upgrade(ctx, zap.NewNop(), s); err != nil { + req.NoError(err) + } + + acRBAC.UpdateRoles(rbac.CommonRole.Make(testRoleID, "test-role")) + req.NoError(acRBAC.Grant(ctx, + rbac.AllowRule(testRoleID, types.UserRbacResource(0), "read"), + rbac.DenyRule(testRoleID, masked.RbacResource(), "email.unmask"), + rbac.AllowRule(testRoleID, unmasked.RbacResource(), "email.unmask"), + rbac.DenyRule(testRoleID, masked.RbacResource(), "name.unmask"), + rbac.AllowRule(testRoleID, unmasked.RbacResource(), "name.unmask"), + )) + + testUser.SetRoles(testRoleID) ctx = a.SetIdentityToContext(ctx, testUser) - svc := makeMockUserService() + svc := &user{ + settings: &types.AppSettings{}, + ac: &accessControl{rbac: acRBAC}, + eventbus: eventbus.New(), + store: s, + } - svc.ac.(*accessControl).rbac.Grant(ctx, - rbac.AllowRule(testRoleID, (&types.User{}).RbacResource(), "read"), - rbac.DenyRule(testRoleID, masked.RbacResource(), "unmask.email"), - rbac.AllowRule(testRoleID, unmasked.RbacResource(), "unmask.email"), - rbac.DenyRule(testRoleID, masked.RbacResource(), "unmask.name"), - rbac.AllowRule(testRoleID, unmasked.RbacResource(), "unmask.name"), - ) req.NoError(store.CreateUser(ctx, svc.store, masked, unmasked)) t.Run("with disabled masking", func(t *testing.T) { diff --git a/system/types/applications.go b/system/types/applications.go index aa2b65664..1b3ba56e5 100644 --- a/system/types/applications.go +++ b/system/types/applications.go @@ -77,10 +77,6 @@ func (a *Application) Valid() bool { return a.ID > 0 && a.DeletedAt == nil } -func (r *Application) DynamicRoles(userID uint64) []uint64 { - return nil -} - func (au *ApplicationUnify) Scan(value interface{}) error { //lint:ignore S1034 This typecast is intentional, we need to get []byte out of a []uint8 switch value.(type) { diff --git a/system/types/auth_client.go b/system/types/auth_client.go index bc489fc0b..1ebafeb07 100644 --- a/system/types/auth_client.go +++ b/system/types/auth_client.go @@ -4,10 +4,11 @@ import ( "database/sql/driver" "encoding/json" "fmt" - "github.com/cortezaproject/corteza-server/pkg/filter" - "github.com/cortezaproject/corteza-server/pkg/slice" "strconv" "time" + + "github.com/cortezaproject/corteza-server/pkg/filter" + "github.com/cortezaproject/corteza-server/pkg/slice" ) type ( @@ -126,6 +127,29 @@ func (r *AuthClient) String() string { } } +func (r AuthClient) Dict() map[string]interface{} { + dict := map[string]interface{}{ + "ID": r.ID, + "labels": r.Labels, + "scope": r.Scope, + "grant": r.ValidGrant, + "redirectURI": r.RedirectURI, + "trusted": r.Trusted, + "enabled": r.Enabled, + "validFrom": r.ValidFrom, + "expiresAt": r.ExpiresAt, + "ownedBy": r.OwnedBy, + "createdAt": r.CreatedAt, + "createdBy": r.CreatedBy, + "updatedAt": r.UpdatedAt, + "updatedBy": r.UpdatedBy, + "deletedAt": r.DeletedAt, + "deletedBy": r.DeletedBy, + } + + return dict +} + // FindByHandle finds authClient by it's handle func (set AuthClientSet) FindByHandle(handle string) *AuthClient { for i := range set { diff --git a/system/types/parsers.go b/system/types/parsers.go index 4a950f11d..9ff071755 100644 --- a/system/types/parsers.go +++ b/system/types/parsers.go @@ -24,3 +24,8 @@ func ParseAuthClientSecurity(ss []string) (p *AuthClientSecurity, err error) { p = &AuthClientSecurity{} return p, parseStringsInput(ss, &p) } + +func ParseRoleMeta(ss []string) (p *RoleMeta, err error) { + p = &RoleMeta{} + return p, parseStringsInput(ss, &p) +} diff --git a/system/types/rbac.gen.go b/system/types/rbac.gen.go index c954e9898..1aaec5c74 100644 --- a/system/types/rbac.gen.go +++ b/system/types/rbac.gen.go @@ -15,6 +15,7 @@ package types // - system.yaml import ( + "fmt" "strconv" ) @@ -26,17 +27,17 @@ type ( ) const ( - ApplicationRbacResourceSchema = "corteza+system.application" - AuthClientRbacResourceSchema = "corteza+system.auth-client" - RoleRbacResourceSchema = "corteza+system.role" - TemplateRbacResourceSchema = "corteza+system.template" - UserRbacResourceSchema = "corteza+system.user" - ComponentRbacResourceSchema = "corteza+system" + ApplicationResourceType = "corteza::system:application" + AuthClientResourceType = "corteza::system:auth-client" + RoleResourceType = "corteza::system:role" + TemplateResourceType = "corteza::system:template" + UserResourceType = "corteza::system:user" + ComponentResourceType = "corteza::system" ) // RbacResource returns string representation of RBAC resource for Application by calling ApplicationRbacResource fn // -// RBAC resource is in the corteza+system.application:/... format +// RBAC resource is in the corteza::system:application/... format // // This function is auto-generated func (r Application) RbacResource() string { @@ -45,24 +46,29 @@ func (r Application) RbacResource() string { // ApplicationRbacResource returns string representation of RBAC resource for Application // -// RBAC resource is in the corteza+system.application:/... format +// RBAC resource is in the corteza::system:application/... format // // This function is auto-generated -func ApplicationRbacResource(iD uint64) string { - out := ApplicationRbacResourceSchema + ":" - out += "/" - - if iD != 0 { - out += strconv.FormatUint(iD, 10) +func ApplicationRbacResource(id uint64) string { + cpts := []interface{}{ApplicationResourceType} + if id != 0 { + cpts = append(cpts, strconv.FormatUint(id, 10)) } else { - out += "*" + cpts = append(cpts, "*") } - return out + + return fmt.Sprintf(ApplicationRbacResourceTpl(), cpts...) + +} + +// @todo template +func ApplicationRbacResourceTpl() string { + return "%s/%s" } // RbacResource returns string representation of RBAC resource for AuthClient by calling AuthClientRbacResource fn // -// RBAC resource is in the corteza+system.auth-client:/... format +// RBAC resource is in the corteza::system:auth-client/... format // // This function is auto-generated func (r AuthClient) RbacResource() string { @@ -71,24 +77,29 @@ func (r AuthClient) RbacResource() string { // AuthClientRbacResource returns string representation of RBAC resource for AuthClient // -// RBAC resource is in the corteza+system.auth-client:/... format +// RBAC resource is in the corteza::system:auth-client/... format // // This function is auto-generated -func AuthClientRbacResource(iD uint64) string { - out := AuthClientRbacResourceSchema + ":" - out += "/" - - if iD != 0 { - out += strconv.FormatUint(iD, 10) +func AuthClientRbacResource(id uint64) string { + cpts := []interface{}{AuthClientResourceType} + if id != 0 { + cpts = append(cpts, strconv.FormatUint(id, 10)) } else { - out += "*" + cpts = append(cpts, "*") } - return out + + return fmt.Sprintf(AuthClientRbacResourceTpl(), cpts...) + +} + +// @todo template +func AuthClientRbacResourceTpl() string { + return "%s/%s" } // RbacResource returns string representation of RBAC resource for Role by calling RoleRbacResource fn // -// RBAC resource is in the corteza+system.role:/... format +// RBAC resource is in the corteza::system:role/... format // // This function is auto-generated func (r Role) RbacResource() string { @@ -97,24 +108,29 @@ func (r Role) RbacResource() string { // RoleRbacResource returns string representation of RBAC resource for Role // -// RBAC resource is in the corteza+system.role:/... format +// RBAC resource is in the corteza::system:role/... format // // This function is auto-generated -func RoleRbacResource(iD uint64) string { - out := RoleRbacResourceSchema + ":" - out += "/" - - if iD != 0 { - out += strconv.FormatUint(iD, 10) +func RoleRbacResource(id uint64) string { + cpts := []interface{}{RoleResourceType} + if id != 0 { + cpts = append(cpts, strconv.FormatUint(id, 10)) } else { - out += "*" + cpts = append(cpts, "*") } - return out + + return fmt.Sprintf(RoleRbacResourceTpl(), cpts...) + +} + +// @todo template +func RoleRbacResourceTpl() string { + return "%s/%s" } // RbacResource returns string representation of RBAC resource for Template by calling TemplateRbacResource fn // -// RBAC resource is in the corteza+system.template:/... format +// RBAC resource is in the corteza::system:template/... format // // This function is auto-generated func (r Template) RbacResource() string { @@ -123,24 +139,29 @@ func (r Template) RbacResource() string { // TemplateRbacResource returns string representation of RBAC resource for Template // -// RBAC resource is in the corteza+system.template:/... format +// RBAC resource is in the corteza::system:template/... format // // This function is auto-generated -func TemplateRbacResource(iD uint64) string { - out := TemplateRbacResourceSchema + ":" - out += "/" - - if iD != 0 { - out += strconv.FormatUint(iD, 10) +func TemplateRbacResource(id uint64) string { + cpts := []interface{}{TemplateResourceType} + if id != 0 { + cpts = append(cpts, strconv.FormatUint(id, 10)) } else { - out += "*" + cpts = append(cpts, "*") } - return out + + return fmt.Sprintf(TemplateRbacResourceTpl(), cpts...) + +} + +// @todo template +func TemplateRbacResourceTpl() string { + return "%s/%s" } // RbacResource returns string representation of RBAC resource for User by calling UserRbacResource fn // -// RBAC resource is in the corteza+system.user:/... format +// RBAC resource is in the corteza::system:user/... format // // This function is auto-generated func (r User) RbacResource() string { @@ -149,24 +170,29 @@ func (r User) RbacResource() string { // UserRbacResource returns string representation of RBAC resource for User // -// RBAC resource is in the corteza+system.user:/... format +// RBAC resource is in the corteza::system:user/... format // // This function is auto-generated -func UserRbacResource(iD uint64) string { - out := UserRbacResourceSchema + ":" - out += "/" - - if iD != 0 { - out += strconv.FormatUint(iD, 10) +func UserRbacResource(id uint64) string { + cpts := []interface{}{UserResourceType} + if id != 0 { + cpts = append(cpts, strconv.FormatUint(id, 10)) } else { - out += "*" + cpts = append(cpts, "*") } - return out + + return fmt.Sprintf(UserRbacResourceTpl(), cpts...) + +} + +// @todo template +func UserRbacResourceTpl() string { + return "%s/%s" } // RbacResource returns string representation of RBAC resource for Component by calling ComponentRbacResource fn // -// RBAC resource is in the corteza+system:/... format +// RBAC resource is in the corteza::system/... format // // This function is auto-generated func (r Component) RbacResource() string { @@ -175,10 +201,15 @@ func (r Component) RbacResource() string { // ComponentRbacResource returns string representation of RBAC resource for Component // -// RBAC resource is in the corteza+system:/... format +// RBAC resource is in the corteza::system/ format // // This function is auto-generated func ComponentRbacResource() string { - out := ComponentRbacResourceSchema + ":" - return out + return ComponentResourceType + "/" + +} + +// @todo template +func ComponentRbacResourceTpl() string { + return "%s" } diff --git a/system/types/role.go b/system/types/role.go index 656a27266..c80b1cce4 100644 --- a/system/types/role.go +++ b/system/types/role.go @@ -30,7 +30,7 @@ type ( } RoleContext struct { - Resource []string `json:"resource,omitempty"` + Resource []string `json:"resourceTypes,omitempty"` Expr string `json:"expr,omitempty"` } @@ -72,8 +72,22 @@ type ( } ) -func (r *Role) DynamicRoles(userID uint64) []uint64 { - return nil +func (r *Role) Clone() *Role { + if r == nil { + return nil + } + + return &Role{ + ID: r.ID, + Name: r.Name, + Handle: r.Handle, + Meta: r.Meta, + Labels: r.Labels, + ArchivedAt: r.ArchivedAt, + CreatedAt: r.CreatedAt, + UpdatedAt: r.UpdatedAt, + DeletedAt: r.DeletedAt, + } } // FindByHandle finds role by it's handle diff --git a/system/types/user.go b/system/types/user.go index aa2b961bb..124e7d92a 100644 --- a/system/types/user.go +++ b/system/types/user.go @@ -69,6 +69,9 @@ type ( Handle string `json:"handle"` Kind UserKind `json:"kind"` + // Set to true if you want to get all kinds/types of users + AllKinds bool `json:"anyKind"` + LabeledIDs []uint64 `json:"-"` Labels map[string]string `json:"labels,omitempty"` @@ -124,11 +127,15 @@ func (u User) Roles() []uint64 { return u.roles } -func (u *User) SetRoles(rr []uint64) { +func (u *User) SetRoles(rr ...uint64) { u.roles = rr } func (u *User) Clone() *User { + if u == nil { + return nil + } + return &User{ ID: u.ID, Username: u.Username, diff --git a/tests/automation/main_test.go b/tests/automation/main_test.go index 53b8b6284..e548e0462 100644 --- a/tests/automation/main_test.go +++ b/tests/automation/main_test.go @@ -9,7 +9,6 @@ import ( "github.com/cortezaproject/corteza-server/app" "github.com/cortezaproject/corteza-server/automation/rest" "github.com/cortezaproject/corteza-server/automation/service" - "github.com/cortezaproject/corteza-server/automation/types" "github.com/cortezaproject/corteza-server/pkg/api/server" "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/cli" @@ -19,7 +18,6 @@ import ( ltype "github.com/cortezaproject/corteza-server/pkg/label/types" "github.com/cortezaproject/corteza-server/pkg/logger" "github.com/cortezaproject/corteza-server/pkg/rand" - "github.com/cortezaproject/corteza-server/pkg/rbac" "github.com/cortezaproject/corteza-server/store" "github.com/cortezaproject/corteza-server/store/sqlite3" sysTypes "github.com/cortezaproject/corteza-server/system/types" @@ -28,7 +26,6 @@ import ( _ "github.com/joho/godotenv/autoload" "github.com/steinfletcher/apitest" "github.com/stretchr/testify/require" - "go.uber.org/zap" ) type ( @@ -67,8 +64,6 @@ func InitTestApp() { ctx := cli.Context() testApp = helpers.NewIntegrationTestApp(ctx, func(app *app.CortezaApp) (err error) { - rbac.SetGlobal(rbac.NewTestService(zap.NewNop(), app.Store)) - service.DefaultStore, err = sqlite3.ConnectInMemory(ctx) if err != nil { return err @@ -102,14 +97,16 @@ func newHelper(t *testing.T) helper { }, } - h.cUser.SetRoles([]uint64{h.roleID}) - - rbac.Global().(*rbac.TestService).ClearGrants() - h.mockPermissionsWithAccess() + h.cUser.SetRoles(h.roleID) + helpers.UpdateRBAC(h.roleID) return h } +func (h helper) MyRole() uint64 { + return h.roleID +} + // Returns context w/ security details func (h helper) secCtx() context.Context { return auth.SetIdentityToContext(context.Background(), h.cUser) @@ -126,34 +123,6 @@ func (h helper) apiInit() *apitest.APITest { } -func (h helper) mockPermissions(rules ...*rbac.Rule) { - h.a.NoError(rbac.Global().Grant( - // TestService we use does not have any backend storage, - context.Background(), - rules..., - )) -} - -// Prepends allow access rule for messaging service for everyone -func (h helper) mockPermissionsWithAccess(rules ...*rbac.Rule) { - rules = append( - rules, - rbac.AllowRule(1, types.ComponentRbacResource(), "access"), - ) - - h.mockPermissions(rules...) -} - -// Set allow permision for test role -func (h helper) allow(r, o string) { - h.mockPermissions(rbac.AllowRule(h.roleID, r, o)) -} - -// set deny permission for test role -func (h helper) deny(r, o string) { - h.mockPermissions(rbac.DenyRule(h.roleID, r, o)) -} - // Unwraps error before it passes it to the tester func (h helper) noError(err error) { for errors.Unwrap(err) != nil { diff --git a/tests/automation/permissions_delete_test.go b/tests/automation/permissions_delete_test.go deleted file mode 100644 index 14b110dac..000000000 --- a/tests/automation/permissions_delete_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package automation - -import ( - "fmt" - "github.com/cortezaproject/corteza-server/automation/types" - "github.com/cortezaproject/corteza-server/pkg/rbac" - "github.com/cortezaproject/corteza-server/tests/helpers" - "net/http" - "testing" -) - -func TestPermissionsDelete(t *testing.T) { - h := newHelper(t) - p := rbac.Global() - - // Make sure our user can grant - h.allow(types.ComponentRbacResource(), "grant") - - // New role. - permDelRole := h.roleID + 1 - - h.a.Len(rbac.Global().FindRulesByRoleID(permDelRole), 0) - - // Setup a few fake rules for new roke - h.mockPermissions( - rbac.AllowRule(permDelRole, types.ComponentRbacResource(), "access"), - rbac.DenyRule(permDelRole, types.ComponentRbacResource(), "workflow.create"), - ) - - h.a.Len(p.FindRulesByRoleID(permDelRole), 2) - - h.apiInit(). - Delete(fmt.Sprintf("/permissions/%d/rules", permDelRole)). - Expect(t). - Status(http.StatusOK). - Assert(helpers.AssertNoErrors). - End() - - // Make sure everything is deleted - for _, r := range p.FindRulesByRoleID(permDelRole) { - h.a.True(r.Access == rbac.Inherit) - } -} diff --git a/tests/automation/permissions_effective_test.go b/tests/automation/permissions_effective_test.go deleted file mode 100644 index 8f18ea36f..000000000 --- a/tests/automation/permissions_effective_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package automation - -import ( - "github.com/cortezaproject/corteza-server/automation/types" - "github.com/cortezaproject/corteza-server/tests/helpers" - "net/http" - "testing" -) - -func TestPermissionsEffective(t *testing.T) { - h := newHelper(t) - h.allow(types.ComponentRbacResource(), "access") - h.deny(types.ComponentRbacResource(), "workflow.create") - - h.apiInit(). - Get("/permissions/effective"). - Expect(t). - Status(http.StatusOK). - Assert(helpers.AssertNoErrors). - End() -} diff --git a/tests/automation/permissions_list_test.go b/tests/automation/permissions_list_test.go deleted file mode 100644 index a68333d4b..000000000 --- a/tests/automation/permissions_list_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package automation - -import ( - "github.com/cortezaproject/corteza-server/tests/helpers" - "github.com/steinfletcher/apitest-jsonpath" - "net/http" - "testing" -) - -func TestPermissionsList(t *testing.T) { - h := newHelper(t) - - h.apiInit(). - Get("/permissions/"). - Expect(t). - Status(http.StatusOK). - Assert(helpers.AssertNoErrors). - Assert(jsonpath.Present(`$.response[? @.resource=="automation"]`)). - End() -} diff --git a/tests/automation/permissions_read_test.go b/tests/automation/permissions_read_test.go deleted file mode 100644 index f9972e37c..000000000 --- a/tests/automation/permissions_read_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package automation - -import ( - "fmt" - "github.com/cortezaproject/corteza-server/automation/types" - "github.com/cortezaproject/corteza-server/tests/helpers" - "net/http" - "testing" -) - -func TestPermissionsRead(t *testing.T) { - h := newHelper(t) - h.allow(types.ComponentRbacResource(), "access") - h.allow(types.ComponentRbacResource(), "grant") - h.deny(types.ComponentRbacResource(), "workflow.create") - - h.apiInit(). - Get(fmt.Sprintf("/permissions/%d/rules", h.roleID)). - Expect(t). - Status(http.StatusOK). - Assert(helpers.AssertNoErrors). - End() -} diff --git a/tests/automation/permissions_test.go b/tests/automation/permissions_test.go new file mode 100644 index 000000000..e4641449f --- /dev/null +++ b/tests/automation/permissions_test.go @@ -0,0 +1,99 @@ +package automation + +import ( + "fmt" + "net/http" + "testing" + + "github.com/cortezaproject/corteza-server/automation/types" + "github.com/cortezaproject/corteza-server/pkg/rbac" + "github.com/cortezaproject/corteza-server/tests/helpers" + "github.com/steinfletcher/apitest-jsonpath" +) + +func TestPermissionsEffective(t *testing.T) { + h := newHelper(t) + helpers.DenyMe(h, types.ComponentRbacResource(), "workflow.create") + + h.apiInit(). + Get("/permissions/effective"). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() +} + +func TestPermissionsList(t *testing.T) { + h := newHelper(t) + + helpers.AllowMe(h, types.ComponentRbacResource(), "grant") + + h.apiInit(). + Get("/permissions/"). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + Assert(jsonpath.Present(fmt.Sprintf(`$.response[? @.type=="%s"]`, types.ComponentResourceType))). + End() +} + +func TestPermissionsRead(t *testing.T) { + h := newHelper(t) + helpers.AllowMe(h, types.ComponentRbacResource(), "grant") + helpers.DenyMe(h, types.ComponentRbacResource(), "workflow.create") + + h.apiInit(). + Get(fmt.Sprintf("/permissions/%d/rules", h.roleID)). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() +} + +func TestPermissionsUpdate(t *testing.T) { + h := newHelper(t) + helpers.AllowMe(h, types.ComponentRbacResource(), "grant") + + h.apiInit(). + Patch(fmt.Sprintf("/permissions/%d/rules", h.roleID)). + Header("Accept", "application/json"). + JSON(fmt.Sprintf(`{"rules":[{"resource":"%s","operation":"workflow.create","access":"allow"}]}`, types.ComponentRbacResource())). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() +} + +func TestPermissionsDelete(t *testing.T) { + h := newHelper(t) + p := rbac.Global() + + // Make sure our user can grant + helpers.AllowMe(h, types.ComponentRbacResource(), "grant") + + // New role. + permDelRole := h.roleID + 1 + + h.a.Len(rbac.Global().FindRulesByRoleID(permDelRole), 0) + + // Setup a few fake rules for new role + helpers.Grant(rbac.AllowRule(permDelRole, types.ComponentRbacResource(), "workflow.create")) + + h.a.Len(p.FindRulesByRoleID(permDelRole), 1) + + h.apiInit(). + Delete(fmt.Sprintf("/permissions/%d/rules", permDelRole)). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() + + // Make sure all rules for this role are deleted + for _, r := range p.FindRulesByRoleID(permDelRole) { + h.a.True(r.Access == rbac.Inherit) + } +} diff --git a/tests/automation/permissions_update_test.go b/tests/automation/permissions_update_test.go deleted file mode 100644 index b427d84f1..000000000 --- a/tests/automation/permissions_update_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package automation - -import ( - "fmt" - "github.com/cortezaproject/corteza-server/tests/helpers" - "net/http" - "testing" -) - -func TestPermissionsUpdate(t *testing.T) { - h := newHelper(t) - h.allow("automation", "grant") - - h.apiInit(). - Patch(fmt.Sprintf("/permissions/%d/rules", h.roleID)). - JSON(`{"rules":[{"resource":"automation","operation":"workflow.create","access":"allow"}]}`). - Expect(t). - Status(http.StatusOK). - Assert(helpers.AssertNoErrors). - End() -} diff --git a/tests/automation/provision_test.go b/tests/automation/provision_test.go deleted file mode 100644 index 5d88b9fcc..000000000 --- a/tests/automation/provision_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package automation - -import ( - "testing" -) - -func TestProvisioning(t *testing.T) { - t.SkipNow() - //h := newHelper(t) - // - //readers, err := impAux.ReadStatic(provision.Asset) - //h.noError(err) - //h.noError(importer.Import(ctx, nil, readers...)) -} diff --git a/tests/automation/session_test.go b/tests/automation/session_test.go index 180afa75f..ce5d4d8c0 100644 --- a/tests/automation/session_test.go +++ b/tests/automation/session_test.go @@ -3,15 +3,16 @@ package automation import ( "context" "fmt" + "net/http" + "testing" + "time" + "github.com/cortezaproject/corteza-server/automation/service" "github.com/cortezaproject/corteza-server/automation/types" "github.com/cortezaproject/corteza-server/pkg/id" "github.com/cortezaproject/corteza-server/store" "github.com/cortezaproject/corteza-server/tests/helpers" jsonpath "github.com/steinfletcher/apitest-jsonpath" - "net/http" - "testing" - "time" ) func (h helper) clearSessions() { @@ -41,7 +42,7 @@ func TestSessionList(t *testing.T) { h := newHelper(t) h.clearSessions() - h.allow(types.AutomationRBACResource, "sessions.search") + helpers.AllowMe(h, types.ComponentRbacResource(), "sessions.search") wf := h.repoMakeWorkflow() h.repoMakeSession(wf) @@ -64,8 +65,8 @@ func TestSessionRead(t *testing.T) { wf := h.repoMakeWorkflow() s := h.repoMakeSession(wf) - h.allow(types.AutomationRBACResource, "sessions.search") - h.allow(types.WorkflowRBACResource.AppendWildcard(), "sessions.manage") + helpers.AllowMe(h, types.ComponentRbacResource(), "sessions.search") + helpers.AllowMe(h, types.WorkflowRbacResource(0), "sessions.manage") h.apiInit(). Get(fmt.Sprintf("/sessions/%d", s.ID)). diff --git a/tests/automation/trigger_test.go b/tests/automation/trigger_test.go index d4bf42852..8af480d49 100644 --- a/tests/automation/trigger_test.go +++ b/tests/automation/trigger_test.go @@ -70,7 +70,7 @@ func TestTriggerRead(t *testing.T) { wf := h.repoMakeWorkflow() tg := h.repoMakeTrigger(wf) - h.allow(types.ComponentRbacResource(), "triggers.search") + helpers.AllowMe(h, types.ComponentRbacResource(), "triggers.search") h.apiInit(). Get(fmt.Sprintf("/triggers/%d", tg.ID)). @@ -87,7 +87,7 @@ func TestTriggerList(t *testing.T) { h := newHelper(t) h.clearTriggers() - h.allow(types.ComponentRbacResource(), "triggers.search") + helpers.AllowMe(h, types.ComponentRbacResource(), "triggers.search") wf := h.repoMakeWorkflow() h.repoMakeTrigger(wf) @@ -121,7 +121,7 @@ func TestTriggerCreate(t *testing.T) { ) t.Run("allowed", func(t *testing.T) { - h.allow(wf.RbacResource(), "triggers.manage") + helpers.AllowMe(h, wf.RbacResource(), "triggers.manage") req().Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). @@ -129,7 +129,7 @@ func TestTriggerCreate(t *testing.T) { }) t.Run("denied", func(t *testing.T) { - h.deny(wf.RbacResource(), "triggers.manage") + helpers.DenyMe(h, wf.RbacResource(), "triggers.manage") req().Expect(t). Status(http.StatusOK). Assert(helpers.AssertError("not allowed to create triggers")). @@ -140,7 +140,7 @@ func TestTriggerCreate(t *testing.T) { func TestTriggerCreateFull(t *testing.T) { h := newHelper(t) - h.allow(types.WorkflowRbacResource(0), "triggers.manage") + helpers.AllowMe(h, types.WorkflowRbacResource(0), "triggers.manage") h.clearTriggers() var ( @@ -181,7 +181,7 @@ func TestTriggerCreateFull(t *testing.T) { h.a.Equal(input, output) - h.allow(types.ComponentRbacResource(), "triggers.search") + helpers.AllowMe(h, types.ComponentRbacResource(), "triggers.search") h.apiInit(). Get(fmt.Sprintf("/triggers/%d", output.ID)). @@ -218,7 +218,7 @@ func TestTriggerUpdate(t *testing.T) { ) t.Run("allowed", func(t *testing.T) { - h.allow(wf.RbacResource(), "triggers.manage") + helpers.AllowMe(h, wf.RbacResource(), "triggers.manage") req(tg1.ID, "foo").Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). @@ -230,7 +230,7 @@ func TestTriggerUpdate(t *testing.T) { }) t.Run("denied", func(t *testing.T) { - h.deny(wf.RbacResource(), "triggers.manage") + helpers.DenyMe(h, wf.RbacResource(), "triggers.manage") req(tg2.ID, "bar").Expect(t). Status(http.StatusOK). Assert(helpers.AssertError("not allowed to update this trigger")). @@ -263,7 +263,7 @@ func TestTriggerDeleteForbidden(t *testing.T) { func TestTriggerDelete(t *testing.T) { h := newHelper(t) - h.allow(types.WorkflowRbacResource(0), "triggers.manage") + helpers.AllowMe(h, types.WorkflowRbacResource(0), "triggers.manage") wf := h.repoMakeWorkflow() res := h.repoMakeTrigger(wf) @@ -283,7 +283,7 @@ func TestTriggerDelete(t *testing.T) { func TestTriggerUndelete(t *testing.T) { h := newHelper(t) - h.allow(types.WorkflowRBACResource.AppendWildcard(), "triggers.manage") + helpers.AllowMe(h, types.WorkflowRbacResource(0), "triggers.manage") wf := h.repoMakeWorkflow() res := h.repoMakeTrigger(wf) @@ -305,8 +305,8 @@ func TestTriggerLabels(t *testing.T) { h := newHelper(t) h.clearTriggers() - h.allow(types.WorkflowRbacResource(0), "triggers.manage") - h.allow(types.ComponentRbacResource(), "triggers.search") + helpers.AllowMe(h, types.WorkflowRbacResource(0), "triggers.manage") + helpers.AllowMe(h, types.ComponentRbacResource(), "triggers.search") var ( ID uint64 diff --git a/tests/automation/workflow_test.go b/tests/automation/workflow_test.go index 16d339a14..9af0f4bd6 100644 --- a/tests/automation/workflow_test.go +++ b/tests/automation/workflow_test.go @@ -3,6 +3,11 @@ package automation import ( "context" "fmt" + "net/http" + "net/url" + "testing" + "time" + "github.com/cortezaproject/corteza-server/automation/service" "github.com/cortezaproject/corteza-server/automation/types" "github.com/cortezaproject/corteza-server/pkg/expr" @@ -12,10 +17,6 @@ import ( "github.com/cortezaproject/corteza-server/tests/helpers" "github.com/steinfletcher/apitest-jsonpath" "github.com/stretchr/testify/require" - "net/http" - "net/url" - "testing" - "time" ) func (h helper) clearWorkflows() { @@ -65,7 +66,7 @@ func TestWorkflowRead(t *testing.T) { h.clearWorkflows() wf := h.repoMakeWorkflow() - h.allow(wf.RbacResource(), "read") + helpers.AllowMe(h, wf.RbacResource(), "read") h.apiInit(). Get(fmt.Sprintf("/workflows/%d", wf.ID)). @@ -82,7 +83,7 @@ func TestWorkflowList(t *testing.T) { h := newHelper(t) h.clearWorkflows() - h.allow(types.WorkflowRbacResource(0), "read") + helpers.AllowMe(h, types.WorkflowRbacResource(0), "read") h.repoMakeWorkflow() h.repoMakeWorkflow() @@ -107,7 +108,7 @@ func TestWorkflowList_filterForbidden(t *testing.T) { h.repoMakeWorkflow("workflow") f := h.repoMakeWorkflow() - h.deny(f.RbacResource(), "read") + helpers.DenyMe(h, f.RbacResource(), "read") h.apiInit(). Get("/workflows/"). @@ -135,7 +136,7 @@ func TestWorkflowCreateForbidden(t *testing.T) { func TestWorkflowCreateNotUnique(t *testing.T) { h := newHelper(t) - h.allow(types.ComponentRbacResource(), "workflow.create") + helpers.AllowMe(h, types.ComponentRbacResource(), "workflow.create") workflow := h.repoMakeWorkflow() h.apiInit(). @@ -151,7 +152,7 @@ func TestWorkflowCreateNotUnique(t *testing.T) { func TestWorkflowCreate(t *testing.T) { h := newHelper(t) - h.allow(types.ComponentRbacResource(), "workflow.create") + helpers.AllowMe(h, types.ComponentRbacResource(), "workflow.create") h.apiInit(). Post("/workflows/"). @@ -167,7 +168,7 @@ func TestWorkflowCreate(t *testing.T) { func TestWorkflowCreateFull(t *testing.T) { h := newHelper(t) - h.allow(types.ComponentRbacResource(), "workflow.create") + helpers.AllowMe(h, types.ComponentRbacResource(), "workflow.create") h.clearWorkflows() var ( @@ -219,7 +220,7 @@ func TestWorkflowCreateFull(t *testing.T) { h.a.Equal(input, output) - h.allow(output.RbacResource(), "read") + helpers.AllowMe(h, output.RbacResource(), "read") h.apiInit(). Get(fmt.Sprintf("/workflows/%d", output.ID)). @@ -252,7 +253,7 @@ func TestWorkflowUpdateForbidden(t *testing.T) { func TestWorkflowUpdate(t *testing.T) { h := newHelper(t) res := h.repoMakeWorkflow() - h.allow(types.WorkflowRbacResource(0), "update") + helpers.AllowMe(h, types.WorkflowRbacResource(0), "update") newName := "updated-" + rs() newHandle := "updated-" + rs() @@ -287,7 +288,7 @@ func TestWorkflowDeleteForbidden(t *testing.T) { func TestWorkflowDelete(t *testing.T) { h := newHelper(t) - h.allow(types.WorkflowRbacResource(0), "delete") + helpers.AllowMe(h, types.WorkflowRbacResource(0), "delete") res := h.repoMakeWorkflow() @@ -306,7 +307,7 @@ func TestWorkflowDelete(t *testing.T) { func TestWorkflowUndelete(t *testing.T) { h := newHelper(t) - h.allow(types.WorkflowRBACResource.AppendWildcard(), "undelete") + helpers.AllowMe(h, types.WorkflowRbacResource(0), "undelete") res := h.repoMakeWorkflow() @@ -327,10 +328,8 @@ func TestWorkflowLabels(t *testing.T) { h := newHelper(t) h.clearWorkflows() - h.allow(types.ComponentRbacResource(), "workflow.create") - h.allow(types.WorkflowRbacResource(0), "read") - h.allow(types.WorkflowRbacResource(0), "update") - h.allow(types.WorkflowRbacResource(0), "delete") + helpers.AllowMe(h, types.ComponentRbacResource(), "workflow.create", "workflows.search") + helpers.AllowMe(h, types.WorkflowRbacResource(0), "read", "update", "delete") var ( ID uint64 @@ -411,7 +410,7 @@ func TestWorkflowLabels(t *testing.T) { func TestWorkflowStepsPayload(t *testing.T) { wf := &types.Workflow{} h := newHelper(t) - h.allow(types.ComponentRbacResource(), "workflow.create") + helpers.AllowMe(h, types.ComponentRbacResource(), "workflow.create") h.apiInit(). Post("/workflows/"). diff --git a/tests/compose/chart_test.go b/tests/compose/chart_test.go index a80f327d4..46a86e9cd 100644 --- a/tests/compose/chart_test.go +++ b/tests/compose/chart_test.go @@ -45,8 +45,8 @@ func TestChartRead(t *testing.T) { h := newHelper(t) h.clearCharts() - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.ChartRbacResource(0, 0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.ChartRbacResource(0, 0), "read") ns := h.makeNamespace("some-namespace") m := h.makeChart(ns, "some-chart") @@ -64,8 +64,8 @@ func TestChartReadByHandle(t *testing.T) { h := newHelper(t) h.clearCharts() - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.ChartRbacResource(0, 0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.ChartRbacResource(0, 0), "read") ns := h.makeNamespace("some-namespace") c := h.makeChart(ns, "some-chart") @@ -81,7 +81,7 @@ func TestChartList(t *testing.T) { h := newHelper(t) h.clearCharts() - h.allow(types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") ns := h.makeNamespace("some-namespace") h.makeChart(ns, "chart1") @@ -95,24 +95,24 @@ func TestChartList(t *testing.T) { End() } -func TestChartList_filterForbiden(t *testing.T) { +func TestChartList_filterForbidden(t *testing.T) { h := newHelper(t) h.clearCharts() - h.allow(types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") ns := h.makeNamespace("some-namespace") h.makeChart(ns, "chart") f := h.makeChart(ns, "chart_forbidden") - h.deny(f.RbacResource(), "read") + helpers.DenyMe(h, f.RbacResource(), "read") h.apiInit(). Get(fmt.Sprintf("/namespace/%d/chart/", ns.ID)). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). - Assert(jsonpath.NotPresent(`$.response.set[? @.name=="chart_forbiden"]`)). + Assert(jsonpath.NotPresent(`$.response.set[? @.name=="chart_forbidden"]`)). End() } @@ -136,8 +136,8 @@ func TestChartCreate(t *testing.T) { h := newHelper(t) h.clearCharts() - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.NamespaceRbacResource(0), "chart.create") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "chart.create") ns := h.makeNamespace("some-namespace") @@ -154,7 +154,7 @@ func TestChartUpdateForbidden(t *testing.T) { h := newHelper(t) h.clearCharts() - h.allow(types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") ns := h.makeNamespace("some-namespace") m := h.makeChart(ns, "some-chart") @@ -172,10 +172,10 @@ func TestChartUpdate(t *testing.T) { h := newHelper(t) h.clearCharts() - h.allow(types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") ns := h.makeNamespace("some-namespace") res := h.makeChart(ns, "some-chart") - h.allow(types.ChartRbacResource(0, 0), "update") + helpers.AllowMe(h, types.ChartRbacResource(0, 0), "update") h.apiInit(). Post(fmt.Sprintf("/namespace/%d/chart/%d", ns.ID, res.ID)). @@ -194,8 +194,8 @@ func TestChartDeleteForbidden(t *testing.T) { h := newHelper(t) h.clearCharts() - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.ChartRbacResource(0, 0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.ChartRbacResource(0, 0), "read") ns := h.makeNamespace("some-namespace") m := h.makeChart(ns, "some-chart") @@ -212,9 +212,9 @@ func TestChartDelete(t *testing.T) { h := newHelper(t) h.clearCharts() - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.ChartRbacResource(0, 0), "read") - h.allow(types.ChartRbacResource(0, 0), "delete") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.ChartRbacResource(0, 0), "read") + helpers.AllowMe(h, types.ChartRbacResource(0, 0), "delete") ns := h.makeNamespace("some-namespace") res := h.makeChart(ns, "some-chart") @@ -234,11 +234,11 @@ func TestChartLabels(t *testing.T) { h := newHelper(t) h.clearCharts() - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.NamespaceRbacResource(0), "chart.create") - h.allow(types.ChartRbacResource(0, 0), "read") - h.allow(types.ChartRbacResource(0, 0), "update") - h.allow(types.ChartRbacResource(0, 0), "delete") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "chart.create") + helpers.AllowMe(h, types.ChartRbacResource(0, 0), "read") + helpers.AllowMe(h, types.ChartRbacResource(0, 0), "update") + helpers.AllowMe(h, types.ChartRbacResource(0, 0), "delete") var ( ns = h.makeNamespace("some-namespace") diff --git a/tests/compose/main_test.go b/tests/compose/main_test.go index 8b9035fdb..0dc432359 100644 --- a/tests/compose/main_test.go +++ b/tests/compose/main_test.go @@ -3,6 +3,9 @@ package compose import ( "context" "errors" + "os" + "testing" + "github.com/cortezaproject/corteza-server/app" "github.com/cortezaproject/corteza-server/compose/rest" "github.com/cortezaproject/corteza-server/compose/service" @@ -22,9 +25,6 @@ import ( "github.com/spf13/afero" "github.com/steinfletcher/apitest" "github.com/stretchr/testify/require" - "go.uber.org/zap" - "os" - "testing" ) type ( @@ -64,7 +64,6 @@ func InitTestApp() { testApp = helpers.NewIntegrationTestApp(ctx, func(app *app.CortezaApp) (err error) { service.DefaultStore = app.Store - rbac.SetGlobal(rbac.NewTestService(zap.NewNop(), app.Store)) service.DefaultObjectStore, err = plain.NewWithAfero(afero.NewMemMapFs(), "test") if err != nil { return err @@ -99,14 +98,16 @@ func newHelper(t *testing.T) helper { }, } - h.cUser.SetRoles([]uint64{h.roleID}) - - rbac.Global().(*rbac.TestService).ClearGrants() - h.mockPermissionsWithAccess() + h.cUser.SetRoles(h.roleID) + helpers.UpdateRBAC(h.roleID) return h } +func (h helper) MyRole() uint64 { + return h.roleID +} + // Returns context w/ security details func (h helper) secCtx() context.Context { return auth.SetIdentityToContext(context.Background(), h.cUser) @@ -131,21 +132,6 @@ func (h helper) mockPermissions(rules ...*rbac.Rule) { )) } -// Prepends allow access rule for compose service for everyone -func (h helper) mockPermissionsWithAccess(rules ...*rbac.Rule) { - h.mockPermissions(rules...) -} - -// Set allow permision for test role -func (h helper) allow(r, o string) { - h.mockPermissions(rbac.AllowRule(h.roleID, r, o)) -} - -// set deny permission for test role -func (h helper) deny(r, o string) { - h.mockPermissions(rbac.DenyRule(h.roleID, r, o)) -} - // Unwraps error before it passes it to the tester func (h helper) noError(err error) { for errors.Unwrap(err) != nil { diff --git a/tests/compose/module_test.go b/tests/compose/module_test.go index 574800f52..1a1083399 100644 --- a/tests/compose/module_test.go +++ b/tests/compose/module_test.go @@ -40,6 +40,7 @@ func (h helper) createModule(res *types.Module) *types.Module { _ = res.Fields.Walk(func(f *types.ModuleField) error { f.ID = id.Next() f.ModuleID = res.ID + f.NamespaceID = res.NamespaceID f.CreatedAt = time.Now() return nil }) @@ -63,8 +64,8 @@ func TestModuleRead(t *testing.T) { h := newHelper(t) h.clearModules() - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.ModuleRbacResource(0, 0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read") ns := h.makeNamespace("some-namespace") m := h.makeModule(ns, "some-module") @@ -82,8 +83,8 @@ func TestModuleReadByHandle(t *testing.T) { h := newHelper(t) h.clearModules() - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.ModuleRbacResource(0, 0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read") ns := h.makeNamespace("some-namespace") c := h.makeModule(ns, "some-module") @@ -99,7 +100,7 @@ func TestModuleList(t *testing.T) { h := newHelper(t) h.clearModules() - h.allow(types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") ns := h.makeNamespace("some-namespace") h.makeModule(ns, "app") @@ -117,7 +118,7 @@ func TestModuleListQuery(t *testing.T) { h := newHelper(t) h.clearModules() - h.allow(types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") ns := h.makeNamespace("some-namespace") h.createModule(&types.Module{ @@ -140,20 +141,20 @@ func TestModuleList_filterForbidden(t *testing.T) { h := newHelper(t) h.clearModules() - h.allow(types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") ns := h.makeNamespace("some-namespace") h.makeModule(ns, "module") - f := h.makeModule(ns, "module_forbiden") + f := h.makeModule(ns, "module_forbidden") - h.deny(types.ModuleRbacResource(0, f.ID), "read") + helpers.DenyMe(h, types.ModuleRbacResource(0, f.ID), "read") h.apiInit(). Get(fmt.Sprintf("/namespace/%d/module/", ns.ID)). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). - Assert(jsonpath.NotPresent(`$.response.set[? @.name=="module_forbiden"]`)). + Assert(jsonpath.NotPresent(`$.response.set[? @.name=="module_forbidden"]`)). End() } @@ -177,8 +178,8 @@ func TestModuleCreate(t *testing.T) { h := newHelper(t) h.clearModules() - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.NamespaceRbacResource(0), "module.create") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "module.create") ns := h.makeNamespace("some-namespace") @@ -195,7 +196,7 @@ func TestModuleUpdateForbidden(t *testing.T) { h := newHelper(t) h.clearModules() - h.allow(types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") ns := h.makeNamespace("some-namespace") m := h.makeModule(ns, "some-module") @@ -213,10 +214,10 @@ func TestModuleUpdate(t *testing.T) { h := newHelper(t) h.clearModules() - h.allow(types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") ns := h.makeNamespace("some-namespace") m := h.makeModule(ns, "some-module") - h.allow(types.ModuleRbacResource(0, 0), "update") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update") h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, m.ID)). @@ -237,10 +238,10 @@ func TestModuleFieldsUpdate(t *testing.T) { h := newHelper(t) h.clearModules() - h.allow(types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") ns := h.makeNamespace("some-namespace") m := h.makeModule(ns, "some-module", &types.ModuleField{ID: id.Next(), Kind: "String", Name: "existing"}) - h.allow(types.ModuleRbacResource(0, 0), "update") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update") f := m.Fields[0] fjs := fmt.Sprintf(`{ "name": "%s", "fields": [{ "fieldID": "%d", "name": "existing_edited", "kind": "Number" }, { "name": "new", "kind": "DateTime" }] }`, m.Name, f.ID) @@ -269,10 +270,11 @@ func TestModuleFieldsUpdate_defaults(t *testing.T) { h := newHelper(t) h.clearModules() - h.allow(types.NamespaceRBACResource.AppendWildcard(), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") ns := h.makeNamespace("some-namespace") m := h.makeModule(ns, "some-module", &types.ModuleField{ID: id.Next(), Kind: "String", Name: "existing", Required: true, DefaultValue: types.RecordValueSet{&types.RecordValue{Value: "test"}}}) - h.allow(types.ModuleRBACResource.AppendWildcard(), "update") + + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update") f := m.Fields[0] fjs := fmt.Sprintf(`{ "name": "%s", "fields": [{ "fieldID": "%d", "name": "existing_edited", "kind": "String", "isRequired": true, "defaultValue": [{ "value": "test" }] }, { "name": "new", "kind": "Bool", "defaultValue": [{ "value": "1" }] }] }`, m.Name, f.ID) @@ -302,7 +304,7 @@ func TestModuleFieldsDefaultValue(t *testing.T) { var ns *types.Namespace h := newHelper(t) - h.allow(types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") prep := func() { h.clearModules() @@ -313,7 +315,7 @@ func TestModuleFieldsDefaultValue(t *testing.T) { prep() m := h.makeModule(ns, "some-module", &types.ModuleField{ID: id.Next(), Kind: "Boolean", Name: "boolean"}) - h.allow(types.ModuleRbacResource(0, 0), "update") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update") f := m.Fields[0] fjs := fmt.Sprintf(`{ "name": "%s", "fields": [{ "fieldID": "%d", "name": "boolean", "kind": "Boolean", "defaultValue": [{"name": "boolean", "value": "1"}] }] }`, m.Name, f.ID) @@ -339,7 +341,7 @@ func TestModuleFieldsDefaultValue(t *testing.T) { prep() m := h.makeModule(ns, "some-module", &types.ModuleField{ID: id.Next(), Kind: "Boolean", Name: "boolean"}) - h.allow(types.ModuleRbacResource(0, 0), "update") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update") f := m.Fields[0] fjs := fmt.Sprintf(`{ "name": "%s", "fields": [{ "fieldID": "%d", "name": "boolean", "kind": "Boolean", "defaultValue": [{"name": "boolean", "value": ""}] }] }`, m.Name, f.ID) @@ -365,7 +367,7 @@ func TestModuleFieldsDefaultValue(t *testing.T) { prep() m := h.makeModule(ns, "some-module", &types.ModuleField{ID: id.Next(), Kind: "Boolean", Name: "boolean"}) - h.allow(types.ModuleRbacResource(0, 0), "update") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update") f := m.Fields[0] fjs := fmt.Sprintf(`{ "name": "%s", "fields": [{ "fieldID": "%d", "name": "boolean", "kind": "Boolean" }] }`, m.Name, f.ID) @@ -390,7 +392,7 @@ func TestModuleFieldsDefaultValue(t *testing.T) { prep() m := h.makeModule(ns, "some-module", &types.ModuleField{ID: id.Next(), Kind: "Boolean", Name: "boolean"}) - h.allow(types.ModuleRbacResource(0, 0), "update") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update") f := m.Fields[0] fjs := fmt.Sprintf(`{ "name": "%s", "fields": [{ "fieldID": "%d", "name": "boolean", "kind": "Boolean", "defaultValue": [{"value": "1"}] }] }`, m.Name, f.ID) @@ -417,10 +419,10 @@ func TestModuleFieldsUpdate_removed(t *testing.T) { h := newHelper(t) h.clearModules() - h.allow(types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") ns := h.makeNamespace("some-namespace") m := h.makeModule(ns, "some-module", &types.ModuleField{ID: id.Next(), Kind: "String", Name: "a"}, &types.ModuleField{ID: id.Next(), Kind: "String", Name: "b"}) - h.allow(types.ModuleRbacResource(0, 0), "update") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update") f := m.Fields[0] fjs := fmt.Sprintf(`{ "name": "%s", "fields": [{ "fieldID": "%d", "name": "a", "kind": "String" }] }`, m.Name, f.ID) @@ -445,11 +447,11 @@ func TestModuleFieldsUpdate_removedHasRecords(t *testing.T) { h := newHelper(t) h.clearModules() - h.allow(types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") ns := h.makeNamespace("some-namespace") m := h.makeModule(ns, "some-module", &types.ModuleField{ID: id.Next(), Kind: "String", Name: "a"}, &types.ModuleField{ID: id.Next(), Kind: "String", Name: "b"}) h.makeRecord(m, &types.RecordValue{Name: "a", Value: "va"}, &types.RecordValue{Name: "b", Value: "vb"}) - h.allow(types.ModuleRbacResource(0, 0), "update") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update") f := m.Fields[0] fjs := fmt.Sprintf(`{ "name": "%s", "fields": [{ "fieldID": "%d", "name": "a", "kind": "String" }] }`, m.Name, f.ID) @@ -474,11 +476,11 @@ func TestModuleFieldsUpdateExpressions(t *testing.T) { h := newHelper(t) h.clearModules() - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.NamespaceRbacResource(0), "module.create") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "module.create") ns := h.makeNamespace("some-namespace") - h.allow(types.ModuleRbacResource(0, 0), "read") - h.allow(types.ModuleRbacResource(0, 0), "update") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update") var ( m = &types.Module{ @@ -576,11 +578,11 @@ func TestModuleFieldsPreventUpdate_ifRecordExists(t *testing.T) { h := newHelper(t) h.clearModules() - h.allow(types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") ns := h.makeNamespace("some-namespace") m := h.makeModule(ns, "some-module", &types.ModuleField{ID: id.Next(), Kind: "String", Name: "existing"}) h.makeRecord(m, &types.RecordValue{Name: "existing", Value: "value"}) - h.allow(types.ModuleRbacResource(0, 0), "update") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update") f := m.Fields[0] fjs := fmt.Sprintf(`{ "name": "%s", "fields": [{ "fieldID": "%d", "name": "existing_edited", "kind": "Number" }, { "name": "new", "kind": "DateTime" }] }`, m.Name, f.ID) @@ -609,8 +611,8 @@ func TestModuleDeleteForbidden(t *testing.T) { h := newHelper(t) h.clearModules() - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.ModuleRbacResource(0, 0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read") ns := h.makeNamespace("some-namespace") m := h.makeModule(ns, "some-module") @@ -627,9 +629,9 @@ func TestModuleDelete(t *testing.T) { h := newHelper(t) h.clearModules() - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.ModuleRbacResource(0, 0), "read") - h.allow(types.ModuleRbacResource(0, 0), "delete") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "delete") ns := h.makeNamespace("some-namespace") res := h.makeModule(ns, "some-module") @@ -649,11 +651,11 @@ func TestModuleLabels(t *testing.T) { h := newHelper(t) h.clearModules() - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.NamespaceRbacResource(0), "module.create") - h.allow(types.ModuleRbacResource(0, 0), "read") - h.allow(types.ModuleRbacResource(0, 0), "update") - h.allow(types.ModuleRbacResource(0, 0), "delete") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "module.create") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "delete") var ( ns = h.makeNamespace("some-namespace") diff --git a/tests/compose/namespace_test.go b/tests/compose/namespace_test.go index bab3f3869..e50302c28 100644 --- a/tests/compose/namespace_test.go +++ b/tests/compose/namespace_test.go @@ -42,6 +42,7 @@ func TestNamespaceRead(t *testing.T) { h.clearNamespaces() ns := h.makeNamespace("some-namespace") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") h.apiInit(). Get(fmt.Sprintf("/namespace/%d", ns.ID)). @@ -57,8 +58,7 @@ func TestNamespaceReadByHandle(t *testing.T) { h := newHelper(t) h.clearNamespaces() - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") ns := h.makeNamespace("some-namespace-" + string(rand.Bytes(20))) nsbh, err := service.DefaultNamespace.FindByHandle(h.secCtx(), ns.Slug) @@ -84,21 +84,21 @@ func TestNamespaceList(t *testing.T) { End() } -func TestNamespaceList_filterForbiden(t *testing.T) { +func TestNamespaceList_filterForbidden(t *testing.T) { h := newHelper(t) h.clearNamespaces() h.makeNamespace("namespace") - f := h.makeNamespace("namespace_forbiden") + f := h.makeNamespace("namespace_forbidden") - h.deny(types.NamespaceRbacResource(f.ID), "read") + helpers.DenyMe(h, types.NamespaceRbacResource(f.ID), "read") h.apiInit(). Get("/namespace/"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). - Assert(jsonpath.NotPresent(`$.response.set[? @.name=="namespace_forbiden"]`)). + Assert(jsonpath.NotPresent(`$.response.set[? @.name=="namespace_forbidden"]`)). End() } @@ -120,7 +120,7 @@ func TestNamespaceCreate(t *testing.T) { h := newHelper(t) h.clearNamespaces() - h.allow(types.ComponentRbacResource(), "namespace.create") + helpers.AllowMe(h, types.ComponentRbacResource(), "namespace.create") h.apiInit(). Post("/namespace/"). @@ -152,7 +152,7 @@ func TestNamespaceUpdate(t *testing.T) { h.clearNamespaces() ns := h.makeNamespace("some-namespace") - h.allow(types.NamespaceRbacResource(0), "update") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "update") h.apiInit(). Post(fmt.Sprintf("/namespace/%d", ns.ID)). @@ -172,6 +172,8 @@ func TestNamespaceDeleteForbidden(t *testing.T) { h.clearNamespaces() ns := h.makeNamespace("some-namespace") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + helpers.DenyMe(h, types.NamespaceRbacResource(0), "delete") h.apiInit(). Delete(fmt.Sprintf("/namespace/%d", ns.ID)). @@ -186,7 +188,7 @@ func TestNamespaceDelete(t *testing.T) { h := newHelper(t) h.clearNamespaces() - h.allow(types.NamespaceRbacResource(0), "delete") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "delete") ns := h.makeNamespace("some-namespace") @@ -205,10 +207,8 @@ func TestNamespaceLabels(t *testing.T) { h := newHelper(t) h.clearNamespaces() - h.allow(types.ComponentRbacResource(), "namespace.create") - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.NamespaceRbacResource(0), "update") - h.allow(types.NamespaceRbacResource(0), "delete") + helpers.AllowMe(h, types.ComponentRbacResource(), "namespace.create") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "delete", "update") var ( ID uint64 diff --git a/tests/compose/page_test.go b/tests/compose/page_test.go index e3ef85fb3..7ab84410b 100644 --- a/tests/compose/page_test.go +++ b/tests/compose/page_test.go @@ -3,6 +3,11 @@ package compose import ( "context" "fmt" + "net/http" + "net/url" + "testing" + "time" + "github.com/cortezaproject/corteza-server/compose/service" "github.com/cortezaproject/corteza-server/compose/types" "github.com/cortezaproject/corteza-server/pkg/id" @@ -10,10 +15,6 @@ import ( "github.com/cortezaproject/corteza-server/tests/helpers" "github.com/steinfletcher/apitest-jsonpath" "github.com/stretchr/testify/require" - "net/http" - "net/url" - "testing" - "time" ) func (h helper) clearPages() { @@ -56,8 +57,8 @@ func TestPageRead(t *testing.T) { h := newHelper(t) h.clearPages() - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.PageRbacResource(0, 0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.PageRbacResource(0, 0), "read") ns := h.makeNamespace("some-namespace") m := h.repoMakePage(ns, "some-page") @@ -75,8 +76,8 @@ func TestPageReadByHandle(t *testing.T) { h := newHelper(t) h.clearPages() - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.PageRbacResource(0, 0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.PageRbacResource(0, 0), "read") ns := h.makeNamespace("some-namespace") c := h.repoMakePage(ns, "some-page") @@ -92,7 +93,7 @@ func TestPageList(t *testing.T) { h := newHelper(t) h.clearPages() - h.allow(types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") ns := h.makeNamespace("some-namespace") h.repoMakePage(ns, "app") @@ -106,24 +107,24 @@ func TestPageList(t *testing.T) { End() } -func TestPageList_filterForbiden(t *testing.T) { +func TestPageList_filterForbidden(t *testing.T) { h := newHelper(t) h.clearPages() - h.allow(types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") ns := h.makeNamespace("some-namespace") h.repoMakePage(ns, "page") - f := h.repoMakePage(ns, "page_forbiden") + f := h.repoMakePage(ns, "page_forbidden") - h.deny(types.PageRbacResource(f.NamespaceID, f.ID), "read") + helpers.DenyMe(h, types.PageRbacResource(f.NamespaceID, f.ID), "read") h.apiInit(). Get(fmt.Sprintf("/namespace/%d/page/", ns.ID)). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). - Assert(jsonpath.NotPresent(`$.response.set[? @.title=="page_forbiden"]`)). + Assert(jsonpath.NotPresent(`$.response.set[? @.title=="page_forbidden"]`)). End() } @@ -147,8 +148,8 @@ func TestPageCreate(t *testing.T) { h := newHelper(t) h.clearPages() - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.NamespaceRbacResource(0), "page.create") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "page.create") ns := h.makeNamespace("some-namespace") @@ -165,7 +166,7 @@ func TestPageUpdateForbidden(t *testing.T) { h := newHelper(t) h.clearPages() - h.allow(types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") ns := h.makeNamespace("some-namespace") m := h.repoMakePage(ns, "some-page") @@ -183,10 +184,10 @@ func TestPageUpdate(t *testing.T) { h := newHelper(t) h.clearPages() - h.allow(types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") ns := h.makeNamespace("some-namespace") res := h.repoMakePage(ns, "some-page") - h.allow(types.PageRbacResource(0, 0), "update") + helpers.AllowMe(h, types.PageRbacResource(0, 0), "update") h.apiInit(). Post(fmt.Sprintf("/namespace/%d/page/%d", ns.ID, res.ID)). @@ -205,8 +206,8 @@ func TestPageReorder(t *testing.T) { h := newHelper(t) h.clearPages() - h.allow(types.PageRBACResource.AppendWildcard(), "update") - h.allow(types.NamespaceRBACResource.AppendWildcard(), "read") + helpers.AllowMe(h, types.PageRbacResource(0, 0), "update") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") ns := h.makeNamespace("some-namespace") res := h.repoMakePage(ns, "some-page") @@ -222,8 +223,8 @@ func TestPageDeleteForbidden(t *testing.T) { h := newHelper(t) h.clearPages() - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.PageRbacResource(0, 0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.PageRbacResource(0, 0), "read") ns := h.makeNamespace("some-namespace") m := h.repoMakePage(ns, "some-page") @@ -240,9 +241,9 @@ func TestPageDelete(t *testing.T) { h := newHelper(t) h.clearPages() - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.PageRbacResource(0, 0), "read") - h.allow(types.PageRbacResource(0, 0), "delete") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.PageRbacResource(0, 0), "read") + helpers.AllowMe(h, types.PageRbacResource(0, 0), "delete") ns := h.makeNamespace("some-namespace") res := h.repoMakePage(ns, "some-page") @@ -262,8 +263,8 @@ func TestPageTreeRead(t *testing.T) { h := newHelper(t) h.clearPages() - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.PageRbacResource(0, 0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.PageRbacResource(0, 0), "read") ns := h.makeNamespace("some-namespace") h.repoMakeWeightedPage(ns, "p1", 1) h.repoMakeWeightedPage(ns, "p4", 4) @@ -286,11 +287,11 @@ func TestPageLabels(t *testing.T) { h := newHelper(t) h.clearPages() - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.NamespaceRbacResource(0), "page.create") - h.allow(types.PageRbacResource(0, 0), "read") - h.allow(types.PageRbacResource(0, 0), "update") - h.allow(types.PageRbacResource(0, 0), "delete") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "page.create") + helpers.AllowMe(h, types.PageRbacResource(0, 0), "read") + helpers.AllowMe(h, types.PageRbacResource(0, 0), "update") + helpers.AllowMe(h, types.PageRbacResource(0, 0), "delete") var ( ns = h.makeNamespace("some-namespace") diff --git a/tests/compose/permissions_delete_test.go b/tests/compose/permissions_delete_test.go deleted file mode 100644 index a609e34df..000000000 --- a/tests/compose/permissions_delete_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package compose - -import ( - "fmt" - "github.com/cortezaproject/corteza-server/compose/types" - "github.com/cortezaproject/corteza-server/pkg/rbac" - "github.com/cortezaproject/corteza-server/tests/helpers" - "net/http" - "testing" -) - -func TestPermissionsDelete(t *testing.T) { - h := newHelper(t) - p := rbac.Global() - - // Make sure our user can grant - h.allow(types.ComponentRbacResource(), "grant") - - // New role. - permDelRole := h.roleID + 1 - - h.a.Len(rbac.Global().FindRulesByRoleID(permDelRole), 0) - - // Setup a few fake rules for new roke - h.mockPermissions( - rbac.DenyRule(permDelRole, types.ComponentRbacResource(), "namespace.create"), - ) - - h.a.Len(p.FindRulesByRoleID(permDelRole), 1) - - h.apiInit(). - Delete(fmt.Sprintf("/permissions/%d/rules", permDelRole)). - Expect(t). - Status(http.StatusOK). - Assert(helpers.AssertNoErrors). - End() - - // Make sure everything is deleted - for _, r := range p.FindRulesByRoleID(permDelRole) { - h.a.True(r.Access == rbac.Inherit) - } -} diff --git a/tests/compose/permissions_effective_test.go b/tests/compose/permissions_effective_test.go deleted file mode 100644 index 690b514b4..000000000 --- a/tests/compose/permissions_effective_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package compose - -import ( - "github.com/cortezaproject/corteza-server/compose/types" - "github.com/cortezaproject/corteza-server/tests/helpers" - "net/http" - "testing" -) - -func TestPermissionsEffective(t *testing.T) { - h := newHelper(t) - h.deny(types.ComponentRbacResource(), "namespace.create") - - h.apiInit(). - Get("/permissions/effective"). - Expect(t). - Status(http.StatusOK). - Assert(helpers.AssertNoErrors). - End() -} diff --git a/tests/compose/permissions_list_test.go b/tests/compose/permissions_list_test.go deleted file mode 100644 index e9d610791..000000000 --- a/tests/compose/permissions_list_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package compose - -import ( - "github.com/cortezaproject/corteza-server/tests/helpers" - "github.com/steinfletcher/apitest-jsonpath" - "net/http" - "testing" -) - -func TestPermissionsList(t *testing.T) { - h := newHelper(t) - - h.apiInit(). - Get("/permissions/"). - Expect(t). - Status(http.StatusOK). - Assert(helpers.AssertNoErrors). - Assert(jsonpath.Present(`$.response[? @.resource=="compose"]`)). - End() -} diff --git a/tests/compose/permissions_read_test.go b/tests/compose/permissions_read_test.go deleted file mode 100644 index 2ddfec881..000000000 --- a/tests/compose/permissions_read_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package compose - -import ( - "fmt" - "github.com/cortezaproject/corteza-server/compose/types" - "github.com/cortezaproject/corteza-server/tests/helpers" - "net/http" - "testing" -) - -func TestPermissionsRead(t *testing.T) { - h := newHelper(t) - h.allow(types.ComponentRbacResource(), "grant") - h.deny(types.ComponentRbacResource(), "namespace.create") - - h.apiInit(). - Get(fmt.Sprintf("/permissions/%d/rules", h.roleID)). - Expect(t). - Status(http.StatusOK). - Assert(helpers.AssertNoErrors). - End() -} diff --git a/tests/compose/permissions_test.go b/tests/compose/permissions_test.go new file mode 100644 index 000000000..500bcd40a --- /dev/null +++ b/tests/compose/permissions_test.go @@ -0,0 +1,105 @@ +package compose + +import ( + "fmt" + "net/http" + "testing" + + "github.com/cortezaproject/corteza-server/compose/types" + "github.com/cortezaproject/corteza-server/pkg/rbac" + "github.com/cortezaproject/corteza-server/tests/helpers" + "github.com/steinfletcher/apitest-jsonpath" +) + +func TestPermissionsEffective(t *testing.T) { + h := newHelper(t) + helpers.DenyMe(h, types.ComponentRbacResource(), "namespace.create") + + h.apiInit(). + Get("/permissions/effective"). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() +} + +func TestPermissionsList(t *testing.T) { + h := newHelper(t) + + helpers.AllowMe(h, types.ComponentRbacResource(), "grant") + + h.apiInit(). + Debug(). + Get("/permissions/"). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + Assert(jsonpath.Present(fmt.Sprintf(`$.response[? @.type=="%s"]`, types.ComponentResourceType))). + End() +} + +func TestPermissionsRead(t *testing.T) { + h := newHelper(t) + helpers.AllowMe(h, types.ComponentRbacResource(), "grant") + helpers.DenyMe(h, types.ComponentRbacResource(), "namespace.create") + + h.apiInit(). + Get(fmt.Sprintf("/permissions/%d/rules", h.roleID)). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() +} + +func TestPermissionsUpdate(t *testing.T) { + h := newHelper(t) + helpers.AllowMe(h, types.ComponentRbacResource(), "grant") + + h.apiInit(). + Patch(fmt.Sprintf("/permissions/%d/rules", h.roleID)). + Header("Accept", "application/json"). + JSON(fmt.Sprintf( + `{"rules":[{"resource":"%s","operation":"namespace.create","access":"allow"},{"resource":"%s","operation":"read","access":"allow"}]}`, + types.ComponentRbacResource(), + "corteza::compose:chart/1/2", + //types.ChartRbacResource(1, 0), + )). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() +} + +func TestPermissionsDelete(t *testing.T) { + h := newHelper(t) + p := rbac.Global() + + // Make sure our user can grant + helpers.AllowMe(h, types.ComponentRbacResource(), "grant") + + // New role. + permDelRole := h.roleID + 1 + + h.a.Len(rbac.Global().FindRulesByRoleID(permDelRole), 0) + + // Setup a few fake rules for new role + helpers.Grant(rbac.AllowRule(permDelRole, types.ComponentRbacResource(), "namespace.create")) + + h.a.Len(p.FindRulesByRoleID(permDelRole), 1) + + h.apiInit(). + Delete(fmt.Sprintf("/permissions/%d/rules", permDelRole)). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() + + // Make sure all rules for this role are deleted + for _, r := range p.FindRulesByRoleID(permDelRole) { + h.a.True(r.Access == rbac.Inherit) + } +} diff --git a/tests/compose/permissions_update_test.go b/tests/compose/permissions_update_test.go deleted file mode 100644 index 9af534a2e..000000000 --- a/tests/compose/permissions_update_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package compose - -import ( - "fmt" - "github.com/cortezaproject/corteza-server/tests/helpers" - "net/http" - "testing" -) - -func TestPermissionsUpdate(t *testing.T) { - h := newHelper(t) - h.allow("compose", "grant") - - h.apiInit(). - Patch(fmt.Sprintf("/permissions/%d/rules", h.roleID)). - JSON(`{"rules":[{"resource":"compose","operation":"namespace.create","access":"allow"}]}`). - Expect(t). - Status(http.StatusOK). - Assert(helpers.AssertNoErrors). - End() -} diff --git a/tests/compose/provision_test.go b/tests/compose/provision_test.go deleted file mode 100644 index 642413b74..000000000 --- a/tests/compose/provision_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package compose - -import ( - "testing" -) - -func TestProvisioning(t *testing.T) { - t.SkipNow() - //h := newHelper(t) - // - //readers, err := impAux.ReadStatic(provision.Asset) - //h.noError(err) - //h.noError(importer.Import(ctx, nil, readers...)) -} diff --git a/tests/compose/record_batch_test.go b/tests/compose/record_batch_test.go index ce94bc97c..b9ea008f6 100644 --- a/tests/compose/record_batch_test.go +++ b/tests/compose/record_batch_test.go @@ -2,22 +2,24 @@ package compose import ( "fmt" - "github.com/cortezaproject/corteza-server/compose/types" - "github.com/cortezaproject/corteza-server/tests/helpers" - "github.com/steinfletcher/apitest-jsonpath" "net/http" "strconv" "testing" + + "github.com/cortezaproject/corteza-server/compose/types" + "github.com/cortezaproject/corteza-server/tests/helpers" + "github.com/steinfletcher/apitest-jsonpath" ) -func TestRecordCreate_batch(t *testing.T) { +func TestBatchRecordCreate(t *testing.T) { h := newHelper(t) h.clearRecords() ns := h.makeNamespace("batch testing namespace") module := h.makeRecordModuleWithFieldsOnNs("record testing module", ns) childModule := h.makeRecordModuleWithFieldsOnNs("record testing module child", ns) - h.allow(types.ModuleRbacResource(0, 0), "record.create") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "record.create") + helpers.AllowMe(h, types.ModuleFieldRbacResource(0, 0, 0), "record.value.update") h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/", module.NamespaceID, module.ID)). @@ -31,14 +33,15 @@ func TestRecordCreate_batch(t *testing.T) { End() } -func TestRecordUpdate_batch(t *testing.T) { +func TestBatchRecordUpdate(t *testing.T) { h := newHelper(t) h.clearRecords() ns := h.makeNamespace("batch testing namespace") module := h.makeRecordModuleWithFieldsOnNs("record testing module", ns) childModule := h.makeRecordModuleWithFieldsOnNs("record testing module child", ns) - h.allow(types.ModuleRbacResource(0, 0), "record.update") + helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "update") + helpers.AllowMe(h, types.ModuleFieldRbacResource(0, 0, 0), "record.value.update") record := h.makeRecord(module) childRecord := h.makeRecord(childModule, &types.RecordValue{Name: "another_record", Value: strconv.FormatUint(record.ID, 10), Ref: record.ID}) @@ -56,15 +59,15 @@ func TestRecordUpdate_batch(t *testing.T) { End() } -func TestRecordDelete_batch(t *testing.T) { +func TestBatchRecordDelete(t *testing.T) { h := newHelper(t) h.clearRecords() ns := h.makeNamespace("batch testing namespace") module := h.makeRecordModuleWithFieldsOnNs("record testing module", ns) childModule := h.makeRecordModuleWithFieldsOnNs("record testing module child", ns) - h.allow(types.ModuleRbacResource(0, 0), "record.update") - h.allow(types.ModuleRbacResource(0, 0), "record.delete") + + helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "update", "delete") record := h.makeRecord(module) childRecord := h.makeRecord(childModule, &types.RecordValue{Name: "another_record", Value: strconv.FormatUint(record.ID, 10), Ref: record.ID}) @@ -85,15 +88,15 @@ func TestRecordDelete_batch(t *testing.T) { h.a.NotNil(record.DeletedAt) } -func TestRecordMixed_batch(t *testing.T) { +func TestBatchRecordMixed(t *testing.T) { h := newHelper(t) h.clearRecords() ns := h.makeNamespace("batch testing namespace") module := h.makeRecordModuleWithFieldsOnNs("record testing module", ns) childModule := h.makeRecordModuleWithFieldsOnNs("record testing module child", ns) - h.allow(types.ModuleRbacResource(0, 0), "record.update") - h.allow(types.ModuleRbacResource(0, 0), "record.create") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "record.create") + helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "update", "delete") record := h.makeRecord(module) childRecord := h.makeRecord(childModule, &types.RecordValue{Name: "another_record", Value: strconv.FormatUint(record.ID, 10), Ref: record.ID}) diff --git a/tests/compose/record_exec_test.go b/tests/compose/record_exec_test.go index de076de11..6663ecd60 100644 --- a/tests/compose/record_exec_test.go +++ b/tests/compose/record_exec_test.go @@ -1,8 +1,13 @@ package compose import ( + "context" "encoding/json" "fmt" + "net/http" + "strconv" + "testing" + "github.com/cortezaproject/corteza-server/compose/rest/request" "github.com/cortezaproject/corteza-server/compose/service" "github.com/cortezaproject/corteza-server/compose/types" @@ -10,9 +15,6 @@ import ( "github.com/cortezaproject/corteza-server/store" "github.com/cortezaproject/corteza-server/tests/helpers" "github.com/steinfletcher/apitest" - "net/http" - "strconv" - "testing" ) func (h helper) apiSendRecordExec(nsID, modID uint64, proc string, args []request.ProcedureArg) *apitest.Response { @@ -41,7 +43,10 @@ func TestRecordExecOrganize(t *testing.T) { h := newHelper(t) h.clearRecords() - h.allow(types.ModuleRbacResource(0, 0), "record.update") + //helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + //helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read") + helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "read", "update") + //helpers.AllowMe(h, types.ModuleFieldRbacResource(0, 0, 0), "record.value.read", "record.value.update") module := h.repoMakeRecordModuleWithFields( "record testing module", @@ -61,7 +66,7 @@ func TestRecordExecOrganize(t *testing.T) { assertSort := func(expectedHandles, expectedCats string) { // Using record service for fetching to avoid value pre-fetching etc.. sorting, _ := filter.NewSorting("position ASC") - set, _, err := store.SearchComposeRecords(h.secCtx(), service.DefaultStore, module, types.RecordFilter{ + set, _, err := store.SearchComposeRecords(context.Background(), service.DefaultStore, module, types.RecordFilter{ ModuleID: module.ID, NamespaceID: module.NamespaceID, Sorting: sorting, diff --git a/tests/compose/record_test.go b/tests/compose/record_test.go index 31bd436eb..0856ff20b 100644 --- a/tests/compose/record_test.go +++ b/tests/compose/record_test.go @@ -37,9 +37,11 @@ type ( ) func (h helper) makeRecordModuleWithFieldsOnNs(name string, namespace *types.Namespace, ff ...*types.ModuleField) *types.Module { - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.ModuleRbacResource(0, 0), "read") - h.allow(types.ModuleRbacResource(0, 0), "record.read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read") + helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "read") + helpers.AllowMe(h, types.ModuleFieldRbacResource(0, 0, 0), "record.value.read") + helpers.AllowMe(h, types.ModuleFieldRbacResource(0, 0, 0), "record.value.update") if len(ff) == 0 { // Default fields @@ -70,9 +72,10 @@ func (h helper) makeRecordModuleWithFieldsOnNs(name string, namespace *types.Nam func (h helper) repoMakeRecordModuleWithFields(name string, ff ...*types.ModuleField) *types.Module { namespace := h.makeNamespace("record testing namespace") - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.ModuleRbacResource(0, 0), "read") - h.allow(types.ModuleRbacResource(0, 0), "record.read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read") + helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "read") + helpers.AllowMe(h, types.ModuleFieldRbacResource(0, 0, 0), "record.value.read", "record.value.update") if len(ff) == 0 { // Default fields @@ -103,9 +106,10 @@ func (h helper) repoMakeRecordModuleWithFields(name string, ff ...*types.ModuleF func (h helper) repoMakeRecordModuleWithFieldsRequired(name string, ff ...*types.ModuleField) *types.Module { namespace := h.makeNamespace("record testing namespace") - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.ModuleRbacResource(0, 0), "read") - h.allow(types.ModuleRbacResource(0, 0), "record.read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read") + helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "read") + helpers.AllowMe(h, types.ModuleFieldRbacResource(0, 0, 0), "record.value.read", "record.value.update") if len(ff) == 0 { // Default fields @@ -168,6 +172,7 @@ func TestRecordRead(t *testing.T) { h.apiInit(). Get(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)). + Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). @@ -187,6 +192,7 @@ func TestRecordList(t *testing.T) { h.apiInit(). Get(fmt.Sprintf("/namespace/%d/module/%d/record/", module.NamespaceID, module.ID)). Query("incTotal", "true"). + Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). @@ -194,12 +200,12 @@ func TestRecordList(t *testing.T) { End() } -func TestRecordListForbidenRecords(t *testing.T) { +func TestRecordListForbiddenRecords(t *testing.T) { h := newHelper(t) h.clearRecords() module := h.repoMakeRecordModuleWithFields("record testing module") - h.deny(types.ModuleRbacResource(0, 0), "record.read") + helpers.DenyMe(h, types.RecordRbacResource(0, 0, 0), "read") h.makeRecord(module) h.makeRecord(module) @@ -207,6 +213,7 @@ func TestRecordListForbidenRecords(t *testing.T) { h.apiInit(). Get(fmt.Sprintf("/namespace/%d/module/%d/record/", module.NamespaceID, module.ID)). Query("incTotal", "true"). + Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). @@ -215,12 +222,12 @@ func TestRecordListForbidenRecords(t *testing.T) { End() } -func TestRecordListForbidenFields(t *testing.T) { +func TestRecordListForbiddenFields(t *testing.T) { h := newHelper(t) h.clearRecords() module := h.repoMakeRecordModuleWithFields("record testing module") - h.deny(types.ModuleFieldRbacResource(0, 0, module.Fields[0].ID), "record.value.read") + helpers.DenyMe(h, types.ModuleFieldRbacResource(0, 0, module.Fields[0].ID), "record.value.read") h.makeRecord(module, &types.RecordValue{Name: "name", Value: "v_name_0"}, &types.RecordValue{Name: "email", Value: "v_email_0"}) h.makeRecord(module, &types.RecordValue{Name: "name", Value: "v_name_1"}, &types.RecordValue{Name: "email", Value: "v_email_1"}) @@ -228,6 +235,7 @@ func TestRecordListForbidenFields(t *testing.T) { h.apiInit(). Get(fmt.Sprintf("/namespace/%d/module/%d/record/", module.NamespaceID, module.ID)). Query("incTotal", "true"). + Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). @@ -252,6 +260,7 @@ func TestRecordCreateForbidden(t *testing.T) { Post(fmt.Sprintf("/namespace/%d/module/%d/record/", module.NamespaceID, module.ID)). Header("Accept", "application/json"). JSON(fmt.Sprintf(`{"values": [{"name": "name", "value": "val"}]}`)). + Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertError("not allowed to create records")). @@ -263,18 +272,20 @@ func TestRecordCreate(t *testing.T) { h.clearRecords() module := h.repoMakeRecordModuleWithFields("record testing module") - h.allow(types.ModuleRbacResource(0, 0), "record.create") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "record.create") + helpers.AllowMe(h, types.ModuleFieldRbacResource(0, 0, 0), "record.value.update") h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/", module.NamespaceID, module.ID)). JSON(fmt.Sprintf(`{"values": [{"name": "name", "value": "val"}]}`)). + Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). End() } -func TestRecordCreateForbiden_forbidenFields(t *testing.T) { +func TestRecordCreateForbidden_forbiddenFields(t *testing.T) { h := newHelper(t) h.clearRecords() @@ -282,19 +293,19 @@ func TestRecordCreateForbiden_forbidenFields(t *testing.T) { &types.ModuleField{Name: "f1", Kind: "String"}, &types.ModuleField{Name: "f2", Kind: "String"}, ) - h.allow(types.ModuleRBACResource.AppendWildcard(), "record.create") - h.deny(types.ModuleFieldRBACResource.AppendID(module.Fields[1].ID), "record.value.update") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "record.create") + helpers.DenyMe(h, module.Fields[1].RbacResource(), "record.value.update") h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/", module.NamespaceID, module.ID)). JSON(fmt.Sprintf(`{"values": [{"name": "f1", "value": "f1.v1"}, {"name": "f2", "value": "f2.v1"}]}`)). Expect(t). Status(http.StatusOK). - Assert(helpers.AssertError("1 issue(s) found")). + Assert(helpers.AssertErrorP("1 issue(s) found")). End() } -func TestRecordCreate_forbidenFields(t *testing.T) { +func TestRecordCreate_forbiddenFields(t *testing.T) { h := newHelper(t) h.clearRecords() @@ -302,8 +313,8 @@ func TestRecordCreate_forbidenFields(t *testing.T) { &types.ModuleField{Name: "f1", Kind: "String"}, &types.ModuleField{Name: "f2", Kind: "String"}, ) - h.allow(types.ModuleRBACResource.AppendWildcard(), "record.create") - h.deny(types.ModuleFieldRBACResource.AppendID(module.Fields[1].ID), "record.value.update") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "record.create") + helpers.DenyMe(h, module.Fields[1].RbacResource(), "record.value.update") h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/", module.NamespaceID, module.ID)). @@ -318,12 +329,6 @@ func TestRecordCreate_xss(t *testing.T) { h := newHelper(t) h.clearRecords() - h.allow(types.NamespaceRBACResource.AppendWildcard(), "read") - h.allow(types.ModuleRBACResource.AppendWildcard(), "read") - h.allow(types.ModuleRBACResource.AppendWildcard(), "record.create") - h.allow(types.ModuleRBACResource.AppendWildcard(), "record.update") - h.allow(types.ModuleRBACResource.AppendWildcard(), "record.read") - var ( ns = h.makeNamespace("some-namespace") mod = h.makeModule(ns, "some-module", @@ -341,6 +346,14 @@ func TestRecordCreate_xss(t *testing.T) { ) ) + helpers.AllowMe(h, ns.RbacResource(), "read") + helpers.AllowMe(h, mod.RbacResource(), "read") + helpers.AllowMe(h, mod.RbacResource(), "record.create") + helpers.AllowMe(h, mod.Fields[0].RbacResource(), "record.value.update") + helpers.AllowMe(h, mod.Fields[1].RbacResource(), "record.value.update") + helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "update") + helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "read") + t.Run("create with rich text fields", func(t *testing.T) { var ( req = require.New(t) @@ -390,11 +403,13 @@ func TestRecordCreateWithErrors(t *testing.T) { }, } module := h.repoMakeRecordModuleWithFields("record testing module", fields...) - h.allow(types.ModuleRbacResource(0, 0), "record.create") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "record.create") + helpers.AllowMe(h, types.ModuleFieldRbacResource(0, 0, 0), "record.value.update") h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/", module.NamespaceID, module.ID)). JSON(fmt.Sprintf(`{"values": [{"name": "name", "value": "val"}]}`)). + Header("Accept", "application/json"). Expect(t). Assert(helpers.AssertRecordValueError( &types.RecordValueError{ @@ -417,6 +432,7 @@ func TestRecordUpdateForbidden(t *testing.T) { Post(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)). Header("Accept", "application/json"). JSON(fmt.Sprintf(`{"values": [{"name": "name", "value": "changed-val"}]}`)). + Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertError("not allowed to update this record")). @@ -429,11 +445,12 @@ func TestRecordUpdate(t *testing.T) { module := h.repoMakeRecordModuleWithFields("record testing module") record := h.makeRecord(module) - h.allow(types.ModuleRbacResource(0, 0), "record.update") + helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "update") h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)). JSON(fmt.Sprintf(`{"values": [{"name": "name", "value": "changed-val"}]}`)). + Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). @@ -456,8 +473,8 @@ func TestRecordUpdate_missingField(t *testing.T) { &types.RecordValue{Name: "f2", Value: "f2.v1"}, ) _ = record - h.allow(types.ModuleRBACResource.AppendWildcard(), "update") - h.allow(types.ModuleRBACResource.AppendWildcard(), "record.update") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update") + helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "update") api := h.apiInit() @@ -486,7 +503,7 @@ func TestRecordUpdate_missingField(t *testing.T) { h.a.Equal("f1.v1 (edited)", r.Values[0].Value) } -func TestRecordUpdateForbiden_forbidenFields(t *testing.T) { +func TestRecordUpdateForbidden_forbiddenFields(t *testing.T) { h := newHelper(t) h.clearRecords() @@ -498,8 +515,9 @@ func TestRecordUpdateForbiden_forbidenFields(t *testing.T) { &types.RecordValue{Name: "f1", Value: "f1.v1"}, &types.RecordValue{Name: "f2", Value: "f2.v1"}, ) - h.allow(types.ModuleRBACResource.AppendWildcard(), "record.update") - h.deny(types.ModuleFieldRBACResource.AppendID(module.Fields[1].ID), "record.value.update") + + helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "update") + helpers.DenyMe(h, module.Fields[1].RbacResource(), "record.value.update") h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)). @@ -514,7 +532,7 @@ func TestRecordUpdateForbiden_forbidenFields(t *testing.T) { h.a.Equal("f2.v1", r.Values.FilterByName("f2")[0].Value) } -func TestRecordUpdate_forbidenFields(t *testing.T) { +func TestRecordUpdate_forbiddenFields(t *testing.T) { h := newHelper(t) h.clearRecords() @@ -526,8 +544,8 @@ func TestRecordUpdate_forbidenFields(t *testing.T) { &types.RecordValue{Name: "f1", Value: "f1.v1"}, &types.RecordValue{Name: "f2", Value: "f2.v1"}, ) - h.allow(types.ModuleRBACResource.AppendWildcard(), "record.update") - h.deny(types.ModuleFieldRBACResource.AppendID(module.Fields[1].ID), "record.value.update") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "record.update") + helpers.DenyMe(h, module.Fields[1].RbacResource(), "record.value.update") h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)). @@ -577,11 +595,12 @@ func TestRecordUpdate_refUnchanged(t *testing.T) { }, ) - h.allow(types.ModuleRbacResource(0, 0), "record.update") + helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "update") h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)). JSON(fmt.Sprintf(`{"values": [{"name": "name", "value": "changed-val"}, {"name": "ref", "value": "%d"}]}`, rRef.ID)). + Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). @@ -630,11 +649,12 @@ func TestRecordUpdate_refChanged(t *testing.T) { }, ) - h.allow(types.ModuleRbacResource(0, 0), "record.update") + helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "update") h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)). JSON(fmt.Sprintf(`{"values": [{"name": "name", "value": "changed-val"}, {"name": "ref", "value": "%d"}]}`, rRef2.ID)). + Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). @@ -652,11 +672,12 @@ func TestRecordUpdate_deleteOld(t *testing.T) { module := h.repoMakeRecordModuleWithFields("record testing module") record := h.makeRecord(module, &types.RecordValue{Name: "name", Value: "test name"}, &types.RecordValue{Name: "email", Value: "test@email.tld"}) - h.allow(types.ModuleRbacResource(0, 0), "record.update") + helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "update") h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)). JSON(fmt.Sprintf(`{"values": [{"name": "email", "value": "test@email.tld"}]}`)). + Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). @@ -691,10 +712,11 @@ func TestRecordDelete(t *testing.T) { module := h.repoMakeRecordModuleWithFields("record testing module") record := h.makeRecord(module) - h.allow(types.ModuleRbacResource(0, 0), "record.delete") + helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "delete") h.apiInit(). Delete(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)). + Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). @@ -720,6 +742,7 @@ func TestRecordExport(t *testing.T) { r := h.apiInit(). Get(fmt.Sprintf("/namespace/%d/module/%d/record/export.csv", module.NamespaceID, module.ID)). Query("fields", "name"). + Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). End() @@ -777,7 +800,7 @@ func TestRecordImportInit(t *testing.T) { } for _, test := range tests { - t.Run(t.Name(), func(t *testing.T) { + t.Run(test.Name, func(t *testing.T) { url := fmt.Sprintf("/namespace/%d/module/%d/record/import", module.NamespaceID, module.ID) h.apiInitRecordImport(h.apiInit(), url, test.Name, []byte(test.Content)). Assert(jsonpath.Present("$.response.sessionID")). @@ -804,7 +827,7 @@ func TestRecordImportInit_invalidFileFormat(t *testing.T) { func TestRecordImportRun(t *testing.T) { h := newHelper(t) h.clearRecords() - h.allow(types.ModuleRbacResource(0, 0), "record.create") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "record.create") module := h.repoMakeRecordModuleWithFields("record import run module") tests := []struct { @@ -818,7 +841,7 @@ func TestRecordImportRun(t *testing.T) { } for _, test := range tests { - t.Run(t.Name(), func(t *testing.T) { + t.Run(test.Name, func(t *testing.T) { url := fmt.Sprintf("/namespace/%d/module/%d/record/import", module.NamespaceID, module.ID) rsp := &rImportSession{} api := h.apiInit() @@ -849,7 +872,7 @@ func TestRecordImportRun_sessionNotFound(t *testing.T) { func TestRecordImportRunForbidden(t *testing.T) { h := newHelper(t) h.clearRecords() - h.deny(types.ModuleRbacResource(0, 0), "record.create") + helpers.DenyMe(h, types.ModuleRbacResource(0, 0), "record.create") module := h.repoMakeRecordModuleWithFields("record import run module") tests := []struct { @@ -863,7 +886,7 @@ func TestRecordImportRunForbidden(t *testing.T) { } for _, test := range tests { - t.Run(t.Name(), func(t *testing.T) { + t.Run(test.Name, func(t *testing.T) { url := fmt.Sprintf("/namespace/%d/module/%d/record/import", module.NamespaceID, module.ID) rsp := &rImportSession{} api := h.apiInit() @@ -881,12 +904,13 @@ func TestRecordImportRunForbidden(t *testing.T) { func TestRecordImportRunForbidden_field(t *testing.T) { h := newHelper(t) h.clearRecords() - h.allow(types.ModuleRbacResource(0, 0), "record.create") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "record.create") + helpers.AllowMe(h, types.ModuleFieldRbacResource(0, 0, 0), "record.value.update") module := h.repoMakeRecordModuleWithFields("record import run module") f := module.Fields.FindByName("name") - h.deny(types.ModuleFieldRbacResource(0, 0, f.ID), "record.value.update") + helpers.DenyMe(h, f.RbacResource(), "record.value.update") tests := []struct { Name string @@ -899,7 +923,8 @@ func TestRecordImportRunForbidden_field(t *testing.T) { } for _, test := range tests { - t.Run(t.Name(), func(t *testing.T) { + t.Run(test.Name, func(t *testing.T) { + h.t = t url := fmt.Sprintf("/namespace/%d/module/%d/record/import", module.NamespaceID, module.ID) rsp := &rImportSession{} api := h.apiInit() @@ -917,7 +942,7 @@ func TestRecordImportRunForbidden_field(t *testing.T) { func TestRecordImportRunFieldError_missing(t *testing.T) { h := newHelper(t) h.clearRecords() - h.allow(types.ModuleRbacResource(0, 0), "record.create") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "record.create") module := h.repoMakeRecordModuleWithFieldsRequired("record import run module") @@ -932,7 +957,7 @@ func TestRecordImportRunFieldError_missing(t *testing.T) { } for _, test := range tests { - t.Run(t.Name(), func(t *testing.T) { + t.Run(test.Name, func(t *testing.T) { url := fmt.Sprintf("/namespace/%d/module/%d/record/import", module.NamespaceID, module.ID) rsp := &rImportSession{} api := h.apiInit() @@ -969,7 +994,7 @@ func TestRecordImportImportProgress(t *testing.T) { } for _, test := range tests { - t.Run(t.Name(), func(t *testing.T) { + t.Run(test.Name, func(t *testing.T) { url := fmt.Sprintf("/namespace/%d/module/%d/record/import", module.NamespaceID, module.ID) rsp := &rImportSession{} api := h.apiInit() @@ -1008,10 +1033,10 @@ func TestRecordFieldModulePermissionCheck(t *testing.T) { // make a standard module, and prevent (DENY) current user to // read from "name" and update "email" fields module := h.repoMakeRecordModuleWithFields("record testing module") - h.deny(module.Fields.FindByName("name").RbacResource(), "record.value.read") - h.deny(module.Fields.FindByName("email").RbacResource(), "record.value.update") - h.allow(types.ModuleRbacResource(0, 0), "record.create") - h.allow(types.ModuleRbacResource(0, 0), "record.update") + helpers.DenyMe(h, module.Fields.FindByName("name").RbacResource(), "record.value.read") + helpers.DenyMe(h, module.Fields.FindByName("email").RbacResource(), "record.value.update") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "record.create") + helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "update") record := h.makeRecord( module, @@ -1022,6 +1047,7 @@ func TestRecordFieldModulePermissionCheck(t *testing.T) { // Fetching record should work as before but without read-protected fields h.apiInit(). Get(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)). + Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). @@ -1034,6 +1060,7 @@ func TestRecordFieldModulePermissionCheck(t *testing.T) { // Searching records should work as before but without read-protected fields h.apiInit(). Get(fmt.Sprintf("/namespace/%d/module/%d/record/", module.NamespaceID, module.ID)). + Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). @@ -1063,7 +1090,7 @@ func TestRecordFieldModulePermissionCheck(t *testing.T) { Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). - Assert(helpers.AssertError("1 issue(s) found")). + Assert(helpers.AssertErrorP("1 issue(s) found")). End() }) @@ -1094,11 +1121,9 @@ func TestRecordLabels(t *testing.T) { h := newHelper(t) h.clearRecords() - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.ModuleRbacResource(0, 0), "read") - h.allow(types.ModuleRbacResource(0, 0), "record.create") - h.allow(types.ModuleRbacResource(0, 0), "record.update") - h.allow(types.ModuleRbacResource(0, 0), "record.read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read", "record.create") + helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "update", "read") var ( ns = h.makeNamespace("some-namespace") @@ -1106,6 +1131,10 @@ func TestRecordLabels(t *testing.T) { ID uint64 ) + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "record.create", "read") + helpers.AllowMe(h, types.ModuleFieldRbacResource(0, 0, 0), "record.value.read") + helpers.AllowMe(h, types.ModuleFieldRbacResource(0, 0, 0), "record.value.update") + t.Run("create", func(t *testing.T) { var ( req = require.New(t) @@ -1126,6 +1155,7 @@ func TestRecordLabels(t *testing.T) { h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/", ns.ID, mod.ID)). JSON(helpers.JSON(rec)). + Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). @@ -1170,6 +1200,7 @@ func TestRecordLabels(t *testing.T) { h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/%d", ns.ID, mod.ID, ID)). JSON(helpers.JSON(rec)). + Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). @@ -1214,10 +1245,9 @@ func TestRecordReports(t *testing.T) { h := newHelper(t) h.clearRecords() - h.allow(types.NamespaceRbacResource(0), "read") - h.allow(types.ModuleRbacResource(0, 0), "read") - h.allow(types.ModuleRbacResource(0, 0), "record.create") - h.allow(types.ModuleRbacResource(0, 0), "record.read") + helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") + helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read", "record.create") + helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "read") var ( ns = h.makeNamespace("some-namespace") diff --git a/tests/federation/main_test.go b/tests/federation/main_test.go index 06a81649b..ae5866b56 100644 --- a/tests/federation/main_test.go +++ b/tests/federation/main_test.go @@ -15,14 +15,12 @@ import ( "github.com/cortezaproject/corteza-server/pkg/eventbus" "github.com/cortezaproject/corteza-server/pkg/id" "github.com/cortezaproject/corteza-server/pkg/logger" - "github.com/cortezaproject/corteza-server/pkg/rbac" sysTypes "github.com/cortezaproject/corteza-server/system/types" "github.com/cortezaproject/corteza-server/tests/helpers" "github.com/go-chi/chi" _ "github.com/joho/godotenv/autoload" "github.com/steinfletcher/apitest" "github.com/stretchr/testify/require" - "go.uber.org/zap" ) type ( @@ -52,10 +50,8 @@ func InitTestApp() { testApp = helpers.NewIntegrationTestApp(ctx, func(app *app.CortezaApp) (err error) { app.Opt.Federation.Enabled = true - service.DefaultLogger = app.Log service.DefaultStore = app.Store - rbac.SetGlobal(rbac.NewTestService(zap.NewNop(), app.Store)) eventbus.Set(eventBus) return nil @@ -86,14 +82,16 @@ func newHelper(t *testing.T) helper { }, } - h.cUser.SetRoles([]uint64{h.roleID}) - - rbac.Global().(*rbac.TestService).ClearGrants() - h.mockPermissionsWithAccess() + h.cUser.SetRoles(h.roleID) + helpers.UpdateRBAC(h.roleID) return h } +func (h helper) MyRole() uint64 { + return h.roleID +} + // Returns context w/ security details func (h helper) secCtx() context.Context { return auth.SetIdentityToContext(context.Background(), h.cUser) @@ -110,29 +108,6 @@ func (h helper) apiInit() *apitest.APITest { } -func (h helper) mockPermissions(rules ...*rbac.Rule) { - h.noError(rbac.Global().Grant( - // TestService we use does not have any backend storage, - context.Background(), - rules..., - )) -} - -// Prepends allow access rule for federation service for everyone -func (h helper) mockPermissionsWithAccess(rules ...*rbac.Rule) { - h.mockPermissions(rules...) -} - -// Set allow permision for test role -func (h helper) allow(r, o string) { - h.mockPermissions(rbac.AllowRule(h.roleID, o, r)) -} - -// set deny permission for test role -func (h helper) deny(r, o string) { - h.mockPermissions(rbac.DenyRule(h.roleID, o, r)) -} - // Unwraps error before it passes it to the tester func (h helper) noError(err error) { for errors.Unwrap(err) != nil { diff --git a/tests/federation/node_pairing_test.go b/tests/federation/node_pairing_test.go index 25b1623ff..e00bfba7d 100644 --- a/tests/federation/node_pairing_test.go +++ b/tests/federation/node_pairing_test.go @@ -36,9 +36,8 @@ func (h helper) clearNodes() { } func (h helper) prepareRBAC() { - h.allow(types.ComponentRbacResource(), "node.create") - h.allow(types.ComponentRbacResource(), "pair") - h.allow(types.NodeRbacResource(0), "manage") + helpers.AllowMe(h, types.ComponentRbacResource(), "node.create", "pair") + helpers.AllowMe(h, types.NodeRbacResource(0), "manage") h.noError(service.DefaultStore.CreateRole(context.Background(), &st.Role{ ID: h.roleID, @@ -169,7 +168,7 @@ func TestSuccessfulNodePairing(t *testing.T) { service.DefaultNode.SetHandshaker(&mockNodeHandshake{ init: func(ctx context.Context, n *types.Node, authToken string) error { h.apiInit(). - // Debug(). + //Debug(). // make sure we do not use test auth-token for authentication but // we do it with pairing token Intercept(helpers.ReqHeaderRawAuthBearer(n.AuthToken)). @@ -186,7 +185,7 @@ func TestSuccessfulNodePairing(t *testing.T) { }) h.apiInit(). - // Debug(). + //Debug(). Post(fmt.Sprintf("/nodes/%d/pair", bNodeID)). Expect(t). Status(http.StatusOK). @@ -205,7 +204,7 @@ func TestSuccessfulNodePairing(t *testing.T) { service.DefaultNode.SetHandshaker(&mockNodeHandshake{ complete: func(ctx context.Context, n *types.Node, authToken string) error { h.apiInit(). - // Debug(). + //Debug(). // make sure we do not use test auth-token but // one provided to us in the initial handshake step Intercept(helpers.ReqHeaderRawAuthBearer(n.AuthToken)). @@ -221,7 +220,7 @@ func TestSuccessfulNodePairing(t *testing.T) { }) h.apiInit(). - // Debug(). + //Debug(). Intercept(helpers.ReqHeaderRawAuthBearer(getNodeAuthToken(aNodeID))). Post(fmt.Sprintf("/nodes/%d/handshake-confirm", aNodeID)). Expect(t). diff --git a/tests/helpers/app.go b/tests/helpers/app.go index acc952dcc..3d6558276 100644 --- a/tests/helpers/app.go +++ b/tests/helpers/app.go @@ -2,11 +2,13 @@ package helpers import ( "context" + "time" + "github.com/cortezaproject/corteza-server/app" "github.com/cortezaproject/corteza-server/pkg/cli" "github.com/cortezaproject/corteza-server/pkg/logger" + "github.com/cortezaproject/corteza-server/pkg/options" "github.com/cortezaproject/corteza-server/pkg/rand" - "time" // Explicitly register SQLite (not done in the app as for testing only) _ "github.com/cortezaproject/corteza-server/store/sqlite3" @@ -19,6 +21,8 @@ func NewIntegrationTestApp(ctx context.Context, initTestServices func(*app.Corte a = app.New() ) + a.Opt = options.Init() + // When running integration tests, we want to upgrade the db. Always. a.Opt.Upgrade.Always = true diff --git a/tests/helpers/assert.go b/tests/helpers/assert.go index ae6fced76..11c7642b8 100644 --- a/tests/helpers/assert.go +++ b/tests/helpers/assert.go @@ -152,7 +152,7 @@ func AssertBody(expected string) assertFn { } } -// AssertErrorP checks if the expected error is part of the error messsage +// AssertErrorP checks if the expected error is part of the error message func AssertErrorP(expectedError string) assertFn { return func(rsp *http.Response, _ *http.Request) (err error) { tmp := StdErrorResponse{} diff --git a/tests/helpers/labels.go b/tests/helpers/labels.go index 41f3173b9..9e1fc65fc 100644 --- a/tests/helpers/labels.go +++ b/tests/helpers/labels.go @@ -2,14 +2,15 @@ package helpers import ( "context" - "github.com/cortezaproject/corteza-server/pkg/label/types" - "github.com/cortezaproject/corteza-server/store" - "github.com/steinfletcher/apitest" - "github.com/stretchr/testify/require" "net/http" "net/url" "strings" "testing" + + "github.com/cortezaproject/corteza-server/pkg/label/types" + "github.com/cortezaproject/corteza-server/store" + "github.com/steinfletcher/apitest" + "github.com/stretchr/testify/require" ) func SetLabelsViaAPI(api *apitest.APITest, t *testing.T, endpoint string, in, out interface{}) { @@ -43,7 +44,8 @@ func SearchWithLabelsViaAPI(api *apitest.APITest, t *testing.T, endpoint string, payload.Response.Set = res - api.Get(endpoint). + api. + Get(endpoint). QueryCollection(labels). Expect(t). Status(http.StatusOK). diff --git a/tests/helpers/rbac.go b/tests/helpers/rbac.go new file mode 100644 index 000000000..4221c6e7a --- /dev/null +++ b/tests/helpers/rbac.go @@ -0,0 +1,52 @@ +package helpers + +import ( + "context" + + "github.com/cortezaproject/corteza-server/pkg/auth" + "github.com/cortezaproject/corteza-server/pkg/cli" + "github.com/cortezaproject/corteza-server/pkg/eventbus" + "github.com/cortezaproject/corteza-server/pkg/rbac" +) + +type ( + myRoleGetter interface { + MyRole() uint64 + } +) + +func UpdateRBAC(rr ...uint64) { + // make sure event listener for role changes is removed + eventbus.Service().UnregisterByResource("system:role") + + // convert given list of role IDs to RBAC roles + // and update service + ccr := []*rbac.Role{} + + // Make sure all bypass roles are set on RBAC service + for _, r := range auth.ServiceUser().Roles() { + ccr = append(ccr, rbac.BypassRole.Make(r, auth.BypassRoleHandle)) + } + + for _, r := range rr { + ccr = append(ccr, rbac.CommonRole.Make(r, "integration-tests")) + } + rbac.Global().Clear() + rbac.Global().UpdateRoles(ccr...) +} + +func AllowMe(mrg myRoleGetter, r string, oo ...string) { + for _, o := range oo { + Grant(rbac.AllowRule(mrg.MyRole(), r, o)) + } +} + +func DenyMe(mrg myRoleGetter, r string, oo ...string) { + for _, o := range oo { + Grant(rbac.DenyRule(mrg.MyRole(), r, o)) + } +} + +func Grant(rr ...*rbac.Rule) { + cli.HandleError(rbac.Global().Grant(context.Background(), rr...)) +} diff --git a/tests/messagebus/main_test.go b/tests/messagebus/main_test.go index 1fc6f5880..9e30e9ef3 100644 --- a/tests/messagebus/main_test.go +++ b/tests/messagebus/main_test.go @@ -9,7 +9,6 @@ import ( "github.com/cortezaproject/corteza-server/app" "github.com/cortezaproject/corteza-server/automation/service" - "github.com/cortezaproject/corteza-server/automation/types" "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/cli" "github.com/cortezaproject/corteza-server/pkg/eventbus" @@ -18,7 +17,6 @@ import ( "github.com/cortezaproject/corteza-server/pkg/messagebus" "github.com/cortezaproject/corteza-server/pkg/options" "github.com/cortezaproject/corteza-server/pkg/rand" - "github.com/cortezaproject/corteza-server/pkg/rbac" "github.com/cortezaproject/corteza-server/store/sqlite3" sysTypes "github.com/cortezaproject/corteza-server/system/types" "github.com/cortezaproject/corteza-server/tests/helpers" @@ -63,8 +61,6 @@ func InitTestApp() { ctx := cli.Context() testApp = helpers.NewIntegrationTestApp(ctx, func(app *app.CortezaApp) (err error) { - rbac.SetGlobal(rbac.NewTestService(logger.Default(), app.Store)) - service.DefaultStore, err = sqlite3.ConnectInMemory(ctx) if err != nil { @@ -96,10 +92,8 @@ func newHelper(t *testing.T) helper { }, } - h.cUser.SetRoles([]uint64{h.roleID}) - - rbac.Global().(*rbac.TestService).ClearGrants() - h.mockPermissionsWithAccess() + h.cUser.SetRoles(h.roleID) + helpers.UpdateRBAC(h.roleID) return h } @@ -109,34 +103,6 @@ func (h helper) secCtx() context.Context { return auth.SetIdentityToContext(context.Background(), h.cUser) } -func (h helper) mockPermissions(rules ...*rbac.Rule) { - h.a.NoError(rbac.Global().Grant( - // TestService we use does not have any backend storage, - context.Background(), - rules..., - )) -} - -// Prepends allow access rule for messaging service for everyone -func (h helper) mockPermissionsWithAccess(rules ...*rbac.Rule) { - rules = append( - rules, - rbac.AllowRule(1, types.ComponentRbacResource(), "access"), - ) - - h.mockPermissions(rules...) -} - -// Set allow permision for test role -func (h helper) allow(r, o string) { - h.mockPermissions(rbac.AllowRule(h.roleID, r, o)) -} - -// set deny permission for test role -func (h helper) deny(r, o string) { - h.mockPermissions(rbac.DenyRule(h.roleID, r, o)) -} - // Unwraps error before it passes it to the tester func (h helper) noError(err error) { for errors.Unwrap(err) != nil { @@ -146,8 +112,6 @@ func (h helper) noError(err error) { h.a.NoError(err) } -func (h helper) prepareRBAC() {} - func (h helper) prepareQueues(ctx context.Context, qs ...*messagebus.QueueSettings) { h.noError(testApp.Store.TruncateMessagebusQueueSettings(ctx)) h.noError(testApp.Store.CreateMessagebusQueueSetting(ctx, qs...)) diff --git a/tests/messagebus/message_consume_test.go b/tests/messagebus/message_consume_test.go index 27e39c1a9..e96261e16 100644 --- a/tests/messagebus/message_consume_test.go +++ b/tests/messagebus/message_consume_test.go @@ -54,7 +54,6 @@ func TestMessageWrite(t *testing.T) { ctx = context.Background() ) - h.prepareRBAC() h.prepareQueues(ctx, testQueueDispatched) // reinit the messagebus @@ -108,7 +107,6 @@ func TestMessageWriteEventbus(t *testing.T) { } ) - h.prepareRBAC() h.prepareQueues(ctx, testQueueEb) // reinit the messagebus diff --git a/tests/system/actionlog_test.go b/tests/system/actionlog_test.go index cf178c696..865f3eccf 100644 --- a/tests/system/actionlog_test.go +++ b/tests/system/actionlog_test.go @@ -2,6 +2,10 @@ package system import ( "context" + "net/http" + "testing" + "time" + "github.com/cortezaproject/corteza-server/pkg/actionlog" "github.com/cortezaproject/corteza-server/pkg/id" "github.com/cortezaproject/corteza-server/store" @@ -9,9 +13,6 @@ import ( "github.com/cortezaproject/corteza-server/system/types" "github.com/cortezaproject/corteza-server/tests/helpers" jsonpath "github.com/steinfletcher/apitest-jsonpath" - "net/http" - "testing" - "time" ) func (h helper) clearActionLog() { @@ -23,7 +24,7 @@ func (h helper) repoMakeActionLog() *actionlog.Action { ID: id.Next(), Timestamp: time.Now(), ActorID: id.Next(), - Resource: types.SystemRBACResource.String(), + Resource: types.ComponentRbacResource(), Action: "lookup", } diff --git a/tests/system/application_test.go b/tests/system/application_test.go index 09bedcb0a..3f0d419e6 100644 --- a/tests/system/application_test.go +++ b/tests/system/application_test.go @@ -85,15 +85,16 @@ func (h helper) lookupApplicationByName(name string) *types.Application { func TestApplicationRead(t *testing.T) { h := newHelper(t) - u := h.repoMakeApplication() + app := h.repoMakeApplication() + helpers.AllowMe(h, types.ApplicationRbacResource(0), "read") h.apiInit(). - Get(fmt.Sprintf("/application/%d", u.ID)). + Get(fmt.Sprintf("/application/%d", app.ID)). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). - Assert(jsonpath.Equal(`$.response.name`, u.Name)). - Assert(jsonpath.Equal(`$.response.applicationID`, fmt.Sprintf("%d", u.ID))). + Assert(jsonpath.Equal(`$.response.name`, app.Name)). + Assert(jsonpath.Equal(`$.response.applicationID`, fmt.Sprintf("%d", app.ID))). End() } @@ -121,7 +122,7 @@ func TestApplicationList_filterForbidden(t *testing.T) { h.repoMakeApplication("application") f := h.repoMakeApplication() - h.deny(f.RbacResource(), "read") + helpers.DenyMe(h, f.RbacResource(), "read") h.apiInit(). Get("/application/"). @@ -148,7 +149,7 @@ func TestApplicationCreateForbidden(t *testing.T) { func TestApplicationCreate(t *testing.T) { h := newHelper(t) - h.allow(types.ComponentRbacResource(), "application.create") + helpers.AllowMe(h, types.ComponentRbacResource(), "application.create") h.apiInit(). Post("/application/"). @@ -162,7 +163,7 @@ func TestApplicationCreate(t *testing.T) { func TestApplicationCreate_weight(t *testing.T) { h := newHelper(t) - h.allow(types.ComponentRbacResource(), "application.create") + helpers.AllowMe(h, types.ComponentRbacResource(), "application.create") name := "name_weight_create_" + rs() h.apiInit(). @@ -196,7 +197,7 @@ func TestApplicationUpdateForbidden(t *testing.T) { func TestApplicationUpdate(t *testing.T) { h := newHelper(t) res := h.repoMakeApplication() - h.allow(types.ApplicationRbacResource(0), "update") + helpers.AllowMe(h, types.ApplicationRbacResource(0), "update") newName := "updated-" + rs() newHandle := "updated-" + rs() @@ -219,7 +220,7 @@ func TestApplicationUpdate(t *testing.T) { func TestApplicationUpdate_weight(t *testing.T) { h := newHelper(t) res := h.repoMakeApplication() - h.allow(types.ApplicationRbacResource(0), "update") + helpers.AllowMe(h, types.ApplicationRbacResource(0), "update") newName := "updated-" + rs() newHandle := "updated-" + rs() @@ -240,13 +241,13 @@ func TestApplicationUpdate_weight(t *testing.T) { h.a.Equal(20, res.Weight) } -func TestApplicationReorder_forbiden(t *testing.T) { +func TestApplicationReorder_forbidden(t *testing.T) { h := newHelper(t) - h.allow(types.ApplicationRbacResource(0), "update") + helpers.AllowMe(h, types.ApplicationRbacResource(0), "update") a := h.repoMakeApplication() b := h.repoMakeApplication() c := h.repoMakeApplication() - h.deny(b.RbacResource(), "update") + helpers.DenyMe(h, b.RbacResource(), "update") h.apiInit(). Post("/application/reorder"). @@ -260,7 +261,7 @@ func TestApplicationReorder_forbiden(t *testing.T) { func TestApplicationReorder(t *testing.T) { h := newHelper(t) - h.allow(types.ApplicationRbacResource(0), "update") + helpers.AllowMe(h, types.ApplicationRbacResource(0), "update") a := h.repoMakeApplication() b := h.repoMakeApplication() c := h.repoMakeApplication() @@ -300,7 +301,7 @@ func TestApplicationDeleteForbidden(t *testing.T) { func TestApplicationDelete(t *testing.T) { h := newHelper(t) - h.allow(types.ApplicationRbacResource(0), "delete") + helpers.AllowMe(h, types.ApplicationRbacResource(0), "delete") res := h.repoMakeApplication() @@ -319,7 +320,7 @@ func TestApplicationDelete(t *testing.T) { func TestApplicationUndelete(t *testing.T) { h := newHelper(t) - h.allow(types.ApplicationRBACResource.AppendWildcard(), "delete") + helpers.AllowMe(h, types.ApplicationRbacResource(0), "delete") res := h.repoMakeApplication() @@ -340,10 +341,8 @@ func TestApplicationLabels(t *testing.T) { h := newHelper(t) h.clearApplications() - h.allow(types.ComponentRbacResource(), "application.create") - h.allow(types.ApplicationRbacResource(0), "read") - h.allow(types.ApplicationRbacResource(0), "update") - h.allow(types.ApplicationRbacResource(0), "delete") + helpers.AllowMe(h, types.ComponentRbacResource(), "application.create") + helpers.AllowMe(h, types.ApplicationRbacResource(0), "read", "update", "delete") var ( ID uint64 @@ -421,10 +420,11 @@ func TestApplicationFlags(t *testing.T) { h := newHelper(t) h.clearApplications() - h.allow(types.ComponentRbacResource(), "application.create") + helpers.AllowMe(h, types.ComponentRbacResource(), "application.create") + helpers.AllowMe(h, types.ApplicationRbacResource(0), "read") t.Run("create", func(t *testing.T) { - h.allow(types.ComponentRbacResource(), "application.flag.global") + helpers.AllowMe(h, types.ComponentRbacResource(), "application.flag.global") res := h.repoMakeApplication() h.apiInit(). @@ -441,7 +441,7 @@ func TestApplicationFlags(t *testing.T) { }) t.Run("create; not allowed", func(t *testing.T) { - h.deny(types.ComponentRbacResource(), "application.flag.global") + helpers.DenyMe(h, types.ComponentRbacResource(), "application.flag.global") res := h.repoMakeApplication() h.apiInit(). @@ -451,37 +451,34 @@ func TestApplicationFlags(t *testing.T) { Status(http.StatusOK). Assert(helpers.AssertError("not allowed to manage global flags for applications")). End() + }) t.Run("create own", func(t *testing.T) { - h.allow(types.ComponentRbacResource(), "application.flag.self") + helpers.AllowMe(h, types.ComponentRbacResource(), "application.flag.self") res := h.repoMakeApplication() h.repoFlagApplication(res.ID, 0, "testFlag", true) h.apiInit(). - Post(fmt.Sprintf("/application/%d/flag/%d/testFlag", res.ID, 10)). + Post(fmt.Sprintf("/application/%d/flag/%d/testFlag", res.ID, h.cUser.ID)). Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). End() - ff := h.searchApplicationFlags(res.ID, 0, "testFlag") - h.a.NotNil(ff) - h.a.Len(ff, 1) - - ff = h.searchApplicationFlags(res.ID, 10, "testFlag") + ff := h.searchApplicationFlags(res.ID, h.cUser.ID, "testFlag") h.a.NotNil(ff) h.a.Len(ff, 1) }) t.Run("create own; not allowed", func(t *testing.T) { - h.deny(types.ComponentRbacResource(), "application.flag.self") + helpers.DenyMe(h, types.ComponentRbacResource(), "application.flag.self") res := h.repoMakeApplication() h.repoFlagApplication(res.ID, 0, "testFlag", true) h.apiInit(). - Post(fmt.Sprintf("/application/%d/flag/%d/testFlag", res.ID, 10)). + Post(fmt.Sprintf("/application/%d/flag/%d/testFlag", res.ID, h.cUser.ID)). Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). @@ -490,7 +487,6 @@ func TestApplicationFlags(t *testing.T) { }) t.Run("read application", func(t *testing.T) { - h.allow(types.ApplicationRbacResource(0), "read") res := h.repoMakeApplication() h.repoFlagApplication(res.ID, 0, "testFlag", true) @@ -506,7 +502,6 @@ func TestApplicationFlags(t *testing.T) { }) t.Run("list applications", func(t *testing.T) { - h.allow(types.ApplicationRbacResource(0), "read") h.clearApplications() res := h.repoMakeApplication() h.repoFlagApplication(res.ID, 0, "testFlag", true) @@ -523,7 +518,6 @@ func TestApplicationFlags(t *testing.T) { }) t.Run("read application; with own flag", func(t *testing.T) { - h.allow(types.ApplicationRbacResource(0), "read") res := h.repoMakeApplication() h.repoFlagApplication(res.ID, 0, "testFlag", true) h.repoFlagApplication(res.ID, h.cUser.ID, "testFlagOwn", true) @@ -539,7 +533,6 @@ func TestApplicationFlags(t *testing.T) { }) t.Run("read application; overwrite global", func(t *testing.T) { - h.allow(types.ApplicationRbacResource(0), "read") res := h.repoMakeApplication() h.repoFlagApplication(res.ID, 0, "testFlag", true) h.repoFlagApplication(res.ID, h.cUser.ID, "testFlag", false) @@ -555,7 +548,6 @@ func TestApplicationFlags(t *testing.T) { t.Run("filter by flags", func(t *testing.T) { flag := rs() - h.allow(types.ApplicationRbacResource(0), "read") h.repoMakeApplication() h.repoMakeApplication() res := h.repoMakeApplication() @@ -573,7 +565,6 @@ func TestApplicationFlags(t *testing.T) { t.Run("filter by flags; self inactive", func(t *testing.T) { flag := rs() - h.allow(types.ApplicationRbacResource(0), "read") h.repoMakeApplication() h.repoMakeApplication() res := h.repoMakeApplication() @@ -595,11 +586,11 @@ func TestApplicationFlags_Flow1(t *testing.T) { h := newHelper(t) h.clearApplications() - h.allow(types.ComponentRbacResource(), "application.create") + helpers.AllowMe(h, types.ComponentRbacResource(), "application.create") + helpers.AllowMe(h, types.ApplicationRbacResource(0), "read") t.Run("create", func(t *testing.T) { - h.allow(types.ComponentRbacResource(), "application.flag.global") - h.allow(types.ComponentRbacResource(), "application.flag.self") + helpers.AllowMe(h, types.ComponentRbacResource(), "application.flag.global", "application.flag.self") res := h.repoMakeApplication() a := h.apiInit() diff --git a/tests/system/attachment_test.go b/tests/system/attachment_test.go index d8a23208e..08f8047fa 100644 --- a/tests/system/attachment_test.go +++ b/tests/system/attachment_test.go @@ -64,7 +64,7 @@ func TestAttachmentDelete(t *testing.T) { h.clearAttachments() a := h.repoMakeAttachment() - h.allow(types.ApplicationRBACResource.AppendWildcard(), "delete") + helpers.AllowMe(h, types.ApplicationRbacResource(0), "delete") h.apiInit(). Delete(fmt.Sprintf("/attachment/%s/%d", a.Kind, a.ID)). diff --git a/tests/system/auth_client_test.go b/tests/system/auth_client_test.go index ed332ccb6..55288af38 100644 --- a/tests/system/auth_client_test.go +++ b/tests/system/auth_client_test.go @@ -54,7 +54,7 @@ func TestAuthClientList(t *testing.T) { h.repoMakeAuthClient() h.repoMakeAuthClient() - h.allow(types.AuthClientRBACResource.AppendWildcard(), "read") + helpers.AllowMe(h, types.AuthClientRbacResource(0), "read") h.apiInit(). Get("/auth/clients/"). @@ -71,7 +71,7 @@ func TestAuthClientRead(t *testing.T) { client := h.repoMakeAuthClient() - h.allow(types.AuthClientRBACResource.AppendWildcard(), "read") + helpers.AllowMe(h, types.AuthClientRbacResource(0), "read") h.apiInit(). Get(fmt.Sprintf("/auth/clients/%d", client.ID)). @@ -87,7 +87,7 @@ func TestAuthClientCreate(t *testing.T) { handle := rs() - h.allow(types.SystemRBACResource, "auth-client.create") + helpers.AllowMe(h, types.ComponentRbacResource(), "auth-client.create") h.apiInit(). Post("/auth/clients/"). @@ -109,7 +109,7 @@ func TestAuthClientUpdate(t *testing.T) { client := h.repoMakeAuthClient() client.Handle = rs() - h.allow(types.AuthClientRBACResource.AppendWildcard(), "update") + helpers.AllowMe(h, types.AuthClientRbacResource(0), "update") h.apiInit(). Put(fmt.Sprintf("/auth/clients/%d", client.ID)). @@ -130,7 +130,7 @@ func TestAuthClientDelete(t *testing.T) { client := h.repoMakeAuthClient() - h.allow(types.AuthClientRBACResource.AppendWildcard(), "delete") + helpers.AllowMe(h, types.AuthClientRbacResource(0), "delete") h.apiInit(). Delete(fmt.Sprintf("/auth/clients/%d", client.ID)). @@ -151,7 +151,7 @@ func TestAuthClientUnDelete(t *testing.T) { client := h.repoMakeAuthClient() - h.allow(types.AuthClientRBACResource.AppendWildcard(), "delete") + helpers.AllowMe(h, types.AuthClientRbacResource(0), "delete") h.apiInit(). Post(fmt.Sprintf("/auth/clients/%d/undelete", client.ID)). diff --git a/tests/system/auth_external_test.go b/tests/system/auth_external_test.go deleted file mode 100644 index 5f029365f..000000000 --- a/tests/system/auth_external_test.go +++ /dev/null @@ -1,9 +0,0 @@ -package system - -import ( - "testing" -) - -func TestAuthExternal(t *testing.T) { - t.Skip("pending implementation") -} diff --git a/tests/system/auth_internal_test.go b/tests/system/auth_internal_test.go deleted file mode 100644 index d4f97d948..000000000 --- a/tests/system/auth_internal_test.go +++ /dev/null @@ -1,9 +0,0 @@ -package system - -import ( - "testing" -) - -func TestAuthInternal(t *testing.T) { - t.Skip("pending implementation") -} diff --git a/tests/system/auth_test.go b/tests/system/auth_test.go deleted file mode 100644 index 230bcd465..000000000 --- a/tests/system/auth_test.go +++ /dev/null @@ -1,9 +0,0 @@ -package system - -import ( - "testing" -) - -func TestAuth(t *testing.T) { - t.Skip("pending implementation") -} diff --git a/tests/system/main_test.go b/tests/system/main_test.go index 24d1dc351..0413731eb 100644 --- a/tests/system/main_test.go +++ b/tests/system/main_test.go @@ -9,14 +9,14 @@ import ( "testing" "github.com/cortezaproject/corteza-server/app" - handlers "github.com/cortezaproject/corteza-server/auth/handlers" + "github.com/cortezaproject/corteza-server/auth/handlers" "github.com/cortezaproject/corteza-server/auth/request" "github.com/cortezaproject/corteza-server/auth/saml" "github.com/cortezaproject/corteza-server/pkg/api/server" "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/cli" "github.com/cortezaproject/corteza-server/pkg/id" - label "github.com/cortezaproject/corteza-server/pkg/label" + "github.com/cortezaproject/corteza-server/pkg/label" ltype "github.com/cortezaproject/corteza-server/pkg/label/types" "github.com/cortezaproject/corteza-server/pkg/logger" "github.com/cortezaproject/corteza-server/pkg/objstore/plain" @@ -79,8 +79,6 @@ func InitTestApp() { testApp = helpers.NewIntegrationTestApp(ctx, func(app *app.CortezaApp) (err error) { service.CurrentSettings.Auth.External.Enabled = true - - rbac.SetGlobal(rbac.NewTestService(zap.NewNop(), app.Store)) service.DefaultObjectStore, err = plain.NewWithAfero(afero.NewMemMapFs(), "test") if err != nil { return err @@ -132,14 +130,17 @@ func newHelper(t *testing.T) helper { data: mockData, } - h.cUser.SetRoles([]uint64{h.roleID}) - - rbac.Global().(*rbac.TestService).ClearGrants() + h.cUser.SetRoles(h.roleID) + helpers.UpdateRBAC(h.roleID) h.mockPermissionsWithAccess() return h } +func (h helper) MyRole() uint64 { + return h.roleID +} + // Returns context w/ security details func (h helper) secCtx() context.Context { return auth.SetIdentityToContext(context.Background(), h.cUser) @@ -169,16 +170,6 @@ func (h helper) mockPermissionsWithAccess(rules ...*rbac.Rule) { h.mockPermissions(rules...) } -// Set allow permision for test role -func (h helper) allow(r, o string) { - h.mockPermissions(rbac.AllowRule(h.roleID, r, o)) -} - -// set deny permission for test role -func (h helper) deny(r, o string) { - h.mockPermissions(rbac.DenyRule(h.roleID, r, o)) -} - // Unwraps error before it passes it to the tester func (h helper) noError(err error) { for errors.Unwrap(err) != nil { diff --git a/tests/system/permissions_delete_test.go b/tests/system/permissions_delete_test.go deleted file mode 100644 index 75fb0e955..000000000 --- a/tests/system/permissions_delete_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package system - -import ( - "fmt" - "github.com/cortezaproject/corteza-server/pkg/rbac" - "github.com/cortezaproject/corteza-server/system/types" - "github.com/cortezaproject/corteza-server/tests/helpers" - "net/http" - "testing" -) - -func TestPermissionsDelete(t *testing.T) { - h := newHelper(t) - p := rbac.Global() - - // Make sure our user can grant - h.allow(types.ComponentRbacResource(), "grant") - - // New role. - permDelRole := h.roleID + 1 - - h.a.Len(p.FindRulesByRoleID(permDelRole), 0) - - // Setup a few fake rules for new roke - h.mockPermissions( - rbac.DenyRule(permDelRole, types.ComponentRbacResource(), "application.create"), - rbac.DenyRule(permDelRole, types.ComponentRbacResource(), "user.create"), - ) - - h.a.Len(p.FindRulesByRoleID(permDelRole), 2) - - h.apiInit(). - Delete(fmt.Sprintf("/permissions/%d/rules", permDelRole)). - Expect(t). - Status(http.StatusOK). - Assert(helpers.AssertNoErrors). - End() - - // Make sure everything is deleted - for _, r := range p.FindRulesByRoleID(permDelRole) { - h.a.True(r.Access == rbac.Inherit) - } -} diff --git a/tests/system/permissions_effective_test.go b/tests/system/permissions_effective_test.go deleted file mode 100644 index c0a703eb3..000000000 --- a/tests/system/permissions_effective_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package system - -import ( - "net/http" - "testing" - - "github.com/cortezaproject/corteza-server/system/types" - "github.com/cortezaproject/corteza-server/tests/helpers" -) - -func TestPermissionsEffective(t *testing.T) { - h := newHelper(t) - h.deny(types.ComponentRbacResource(), "application.create") - - h.apiInit(). - Get("/permissions/effective"). - Expect(t). - Status(http.StatusOK). - Assert(helpers.AssertNoErrors). - End() -} diff --git a/tests/system/permissions_list_test.go b/tests/system/permissions_list_test.go deleted file mode 100644 index 8998b4d54..000000000 --- a/tests/system/permissions_list_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package system - -import ( - "net/http" - "testing" - - jsonpath "github.com/steinfletcher/apitest-jsonpath" - - "github.com/cortezaproject/corteza-server/tests/helpers" -) - -func TestPermissionsList(t *testing.T) { - h := newHelper(t) - - h.apiInit(). - Get("/permissions/"). - Expect(t). - Status(http.StatusOK). - Assert(helpers.AssertNoErrors). - Assert(jsonpath.Present(`$.response[? @.resource=="system"]`)). - End() -} diff --git a/tests/system/permissions_read_test.go b/tests/system/permissions_read_test.go deleted file mode 100644 index 3efdbd428..000000000 --- a/tests/system/permissions_read_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package system - -import ( - "fmt" - "net/http" - "testing" - - "github.com/cortezaproject/corteza-server/system/types" - "github.com/cortezaproject/corteza-server/tests/helpers" -) - -func TestPermissionsRead(t *testing.T) { - h := newHelper(t) - h.allow(types.ComponentRbacResource(), "grant") - h.deny(types.ComponentRbacResource(), "application.create") - - h.apiInit(). - Get(fmt.Sprintf("/permissions/%d/rules", h.roleID)). - Expect(t). - Status(http.StatusOK). - Assert(helpers.AssertNoErrors). - End() -} diff --git a/tests/system/permissions_test.go b/tests/system/permissions_test.go new file mode 100644 index 000000000..9043beb02 --- /dev/null +++ b/tests/system/permissions_test.go @@ -0,0 +1,99 @@ +package system + +import ( + "fmt" + "net/http" + "testing" + + "github.com/cortezaproject/corteza-server/pkg/rbac" + "github.com/cortezaproject/corteza-server/system/types" + "github.com/cortezaproject/corteza-server/tests/helpers" + "github.com/steinfletcher/apitest-jsonpath" +) + +func TestPermissionsEffective(t *testing.T) { + h := newHelper(t) + helpers.DenyMe(h, types.ComponentRbacResource(), "user.create") + + h.apiInit(). + Get("/permissions/effective"). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() +} + +func TestPermissionsList(t *testing.T) { + h := newHelper(t) + + helpers.AllowMe(h, types.ComponentRbacResource(), "grant") + + h.apiInit(). + Get("/permissions/"). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + Assert(jsonpath.Present(fmt.Sprintf(`$.response[? @.type=="%s"]`, types.ComponentResourceType))). + End() +} + +func TestPermissionsRead(t *testing.T) { + h := newHelper(t) + helpers.AllowMe(h, types.ComponentRbacResource(), "grant") + helpers.DenyMe(h, types.ComponentRbacResource(), "user.create") + + h.apiInit(). + Get(fmt.Sprintf("/permissions/%d/rules", h.roleID)). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() +} + +func TestPermissionsUpdate(t *testing.T) { + h := newHelper(t) + helpers.AllowMe(h, types.ComponentRbacResource(), "grant") + + h.apiInit(). + Patch(fmt.Sprintf("/permissions/%d/rules", h.roleID)). + Header("Accept", "application/json"). + JSON(fmt.Sprintf(`{"rules":[{"resource":"%s","operation":"user.create","access":"allow"}]}`, types.ComponentRbacResource())). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() +} + +func TestPermissionsDelete(t *testing.T) { + h := newHelper(t) + p := rbac.Global() + + // Make sure our user can grant + helpers.AllowMe(h, types.ComponentRbacResource(), "grant") + + // New role. + permDelRole := h.roleID + 1 + + h.a.Len(rbac.Global().FindRulesByRoleID(permDelRole), 0) + + // Setup a few fake rules for new role + helpers.Grant(rbac.AllowRule(permDelRole, types.ComponentRbacResource(), "user.create")) + + h.a.Len(p.FindRulesByRoleID(permDelRole), 1) + + h.apiInit(). + Delete(fmt.Sprintf("/permissions/%d/rules", permDelRole)). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() + + // Make sure all rules for this role are deleted + for _, r := range p.FindRulesByRoleID(permDelRole) { + h.a.True(r.Access == rbac.Inherit) + } +} diff --git a/tests/system/permissions_update_test.go b/tests/system/permissions_update_test.go deleted file mode 100644 index 5f2a90adf..000000000 --- a/tests/system/permissions_update_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package system - -import ( - "fmt" - "net/http" - "testing" - - "github.com/cortezaproject/corteza-server/system/types" - "github.com/cortezaproject/corteza-server/tests/helpers" -) - -func TestPermissionsUpdate(t *testing.T) { - h := newHelper(t) - h.allow(types.ComponentRbacResource(), "grant") - - h.apiInit(). - Patch(fmt.Sprintf("/permissions/%d/rules", h.roleID)). - JSON(`{"rules":[{"resource":"system","operation":"application.create","access":"allow"}]}`). - Expect(t). - Status(http.StatusOK). - Assert(helpers.AssertNoErrors). - End() -} diff --git a/tests/system/provision_test.go b/tests/system/provision_test.go deleted file mode 100644 index 9e18d2761..000000000 --- a/tests/system/provision_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package system - -import ( - "testing" -) - -func TestProvisioning(t *testing.T) { - t.SkipNow() - //h := newHelper(t) - // - //readers, err := impAux.ReadStatic(provision.Asset) - //h.a.NoError(err) - //h.a.NoError(importer.Import(ctx, readers...)) -} diff --git a/tests/system/queues_test.go b/tests/system/queues_test.go index f231e5319..37563f738 100644 --- a/tests/system/queues_test.go +++ b/tests/system/queues_test.go @@ -3,6 +3,10 @@ package system import ( "context" "fmt" + "net/http" + "testing" + "time" + "github.com/cortezaproject/corteza-server/pkg/id" "github.com/cortezaproject/corteza-server/pkg/messagebus" "github.com/cortezaproject/corteza-server/store" @@ -10,9 +14,6 @@ import ( "github.com/cortezaproject/corteza-server/system/types" "github.com/cortezaproject/corteza-server/tests/helpers" jsonpath "github.com/steinfletcher/apitest-jsonpath" - "net/http" - "testing" - "time" ) func (h helper) clearMessagebusQueueSettings() { @@ -56,7 +57,7 @@ func TestQueueList(t *testing.T) { h.repoMakeMessagebusQueueSetting() h.repoMakeMessagebusQueueSetting() - h.allow(types.MessagebusQueueRBACResource.AppendWildcard(), "read") + helpers.AllowMe(h, messagebus.QueueRbacResource(0), "read") h.apiInit(). Get("/queues/"). @@ -73,7 +74,7 @@ func TestQueueRead(t *testing.T) { res := h.repoMakeMessagebusQueueSetting() - h.allow(types.MessagebusQueueRBACResource.AppendWildcard(), "read") + helpers.AllowMe(h, messagebus.QueueRbacResource(0), "read") h.apiInit(). Get(fmt.Sprintf("/queues/%d", res.ID)). @@ -87,7 +88,7 @@ func TestQueueCreate(t *testing.T) { h := newHelper(t) h.clearMessagebusQueueSettings() - h.allow(types.SystemRBACResource, "messagebus-queue.create") + helpers.AllowMe(h, types.ComponentRbacResource(), "queue.create") consumer := string(messagebus.ConsumerStore) queue := rs() @@ -114,7 +115,7 @@ func TestQueueUpdate(t *testing.T) { res := h.repoMakeMessagebusQueueSetting() res.Consumer = consumer - h.allow(types.MessagebusQueueRBACResource.AppendWildcard(), "update") + helpers.AllowMe(h, messagebus.QueueRbacResource(0), "update") h.apiInit(). Post(fmt.Sprintf("/queues/%d", res.ID)). @@ -136,7 +137,7 @@ func TestQueueDelete(t *testing.T) { res := h.repoMakeMessagebusQueueSetting() - h.allow(types.MessagebusQueueRBACResource.AppendWildcard(), "delete") + helpers.AllowMe(h, messagebus.QueueRbacResource(0), "delete") h.apiInit(). Delete(fmt.Sprintf("/queues/%d", res.ID)). @@ -157,7 +158,7 @@ func TestQueueUnDelete(t *testing.T) { res := h.repoMakeMessagebusQueueSetting() - h.allow(types.MessagebusQueueRBACResource.AppendWildcard(), "delete") + helpers.AllowMe(h, messagebus.QueueRbacResource(0), "delete") h.apiInit(). Post(fmt.Sprintf("/queues/%d/undelete", res.ID)). diff --git a/tests/system/reminder_test.go b/tests/system/reminder_test.go index 0f0b11ddc..25633cd34 100644 --- a/tests/system/reminder_test.go +++ b/tests/system/reminder_test.go @@ -50,7 +50,7 @@ func TestReminderCreate(t *testing.T) { End() } -func TestReminderAssign_forbiden(t *testing.T) { +func TestReminderAssign_forbidden(t *testing.T) { h := newHelper(t) h.clearReminders() @@ -69,7 +69,7 @@ func TestReminderAssign(t *testing.T) { h := newHelper(t) h.clearReminders() - h.allow(types.ComponentRbacResource(), "reminder.assign") + helpers.AllowMe(h, types.ComponentRbacResource(), "reminder.assign") h.apiInit(). Post("/reminder/"). @@ -134,7 +134,7 @@ func TestReminderUpdate(t *testing.T) { h := newHelper(t) h.clearReminders() - h.allow(types.ComponentRbacResource(), "reminder.assign") + helpers.AllowMe(h, types.ComponentRbacResource(), "reminder.assign") rm := h.makeReminder() diff --git a/tests/system/role_test.go b/tests/system/role_test.go index a41497528..372ee8d6d 100644 --- a/tests/system/role_test.go +++ b/tests/system/role_test.go @@ -3,6 +3,11 @@ package system import ( "context" "fmt" + "net/http" + "net/url" + "testing" + "time" + "github.com/cortezaproject/corteza-server/pkg/id" "github.com/cortezaproject/corteza-server/store" "github.com/cortezaproject/corteza-server/system/service" @@ -10,10 +15,6 @@ import ( "github.com/cortezaproject/corteza-server/tests/helpers" "github.com/steinfletcher/apitest-jsonpath" "github.com/stretchr/testify/require" - "net/http" - "net/url" - "testing" - "time" ) func (h helper) clearRoles() { @@ -116,7 +117,7 @@ func TestRoleList_filterForbidden(t *testing.T) { h.repoMakeRole("role") f := h.repoMakeRole() - h.deny(f.RbacResource(), "read") + helpers.DenyMe(h, f.RbacResource(), "read") h.apiInit(). Get("/roles/"). @@ -143,7 +144,7 @@ func TestRoleCreateForbidden(t *testing.T) { func TestRoleCreateNotUnique(t *testing.T) { h := newHelper(t) - h.allow(types.ComponentRbacResource(), "role.create") + helpers.AllowMe(h, types.ComponentRbacResource(), "role.create") role := h.repoMakeRole() h.apiInit(). @@ -170,7 +171,7 @@ func TestRoleCreateNotUnique(t *testing.T) { func TestRoleCreate(t *testing.T) { h := newHelper(t) - h.allow(types.ComponentRbacResource(), "role.create") + helpers.AllowMe(h, types.ComponentRbacResource(), "role.create") h.apiInit(). Post("/roles/"). @@ -182,6 +183,87 @@ func TestRoleCreate(t *testing.T) { End() } +func TestRoleCreateWithJSON(t *testing.T) { + h := newHelper(t) + helpers.AllowMe(h, types.ComponentRbacResource(), "role.create") + + h.clearRoles() + + h.apiInit(). + Post("/roles/"). + Header("Accept", "application/json"). + JSON(fmt.Sprintf(`{ + "name": "fake role", + "handle": "fakeRoleFromJSON", + "meta": { + "description": "this is my description", + "context": { + "resourceTypes": ["corteza::compose:record"], + "expr": "userID == resource.ownedBy" + } + } + }`)). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() + + r, err := store.LookupRoleByHandle(context.Background(), service.DefaultStore, "fakeRoleFromJSON") + h.a.NoError(err) + h.a.NotNil(r.Meta) + h.a.NotNil(r.Meta.Context) + h.a.NotEmpty(r.Meta.Context.Resource) + h.a.Equal("corteza::compose:record", r.Meta.Context.Resource[0]) + h.a.Equal("userID == resource.ownedBy", r.Meta.Context.Expr) +} + +func TestRoleUpdateWithJSON(t *testing.T) { + h := newHelper(t) + helpers.AllowMe(h, types.ComponentRbacResource(), "role.create") + + h.clearRoles() + + h.noError(store.CreateRole(context.Background(), service.DefaultStore, &types.Role{ + ID: 42, + Name: "fix", + Handle: "fix", + Meta: &types.RoleMeta{ + Description: "fix", + Context: &types.RoleContext{ + Resource: []string{"corteza::compose:module"}, + Expr: "1!=2", + }, + }, + })) + + h.apiInit(). + Post("/roles/"). + Header("Accept", "application/json"). + JSON(fmt.Sprintf(`{ + "name": "fake role", + "handle": "fakeRoleFromJSON", + "meta": { + "description": "this is my description", + "context": { + "resourceTypes": ["corteza::compose:record"], + "expr": "userID == resource.ownedBy" + } + } + }`)). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End() + + r, err := store.LookupRoleByHandle(context.Background(), service.DefaultStore, "fakeRoleFromJSON") + h.a.NoError(err) + h.a.NotNil(r.Meta) + h.a.NotNil(r.Meta.Context) + h.a.NotEmpty(r.Meta.Context.Resource) + h.a.Equal("corteza::compose:record", r.Meta.Context.Resource[0]) + h.a.Equal("userID == resource.ownedBy", r.Meta.Context.Expr) +} + func TestRoleUpdateForbidden(t *testing.T) { h := newHelper(t) u := h.repoMakeRole() @@ -199,7 +281,7 @@ func TestRoleUpdateForbidden(t *testing.T) { func TestRoleUpdate(t *testing.T) { h := newHelper(t) res := h.repoMakeRole() - h.allow(types.RoleRbacResource(0), "update") + helpers.AllowMe(h, types.RoleRbacResource(0), "update") newName := "updated-" + rs() newHandle := "updated-" + rs() @@ -234,7 +316,7 @@ func TestRoleDeleteForbidden(t *testing.T) { func TestRoleDelete(t *testing.T) { h := newHelper(t) - h.allow(types.RoleRbacResource(0), "delete") + helpers.AllowMe(h, types.RoleRbacResource(0), "delete") res := h.repoMakeRole() @@ -252,7 +334,7 @@ func TestRoleDelete(t *testing.T) { func TestRoleUndelete(t *testing.T) { h := newHelper(t) - h.allow(types.RoleRBACResource.AppendWildcard(), "delete") + helpers.AllowMe(h, types.RoleRbacResource(0), "delete") res := h.repoMakeRole() @@ -270,7 +352,7 @@ func TestRoleUndelete(t *testing.T) { func TestRoleArchive(t *testing.T) { h := newHelper(t) - h.allow(types.RoleRBACResource.AppendWildcard(), "update") + helpers.AllowMe(h, types.RoleRbacResource(0), "update") res := h.repoMakeRole() @@ -288,7 +370,7 @@ func TestRoleArchive(t *testing.T) { func TestRoleUnarchive(t *testing.T) { h := newHelper(t) - h.allow(types.RoleRBACResource.AppendWildcard(), "delete") + helpers.AllowMe(h, types.RoleRbacResource(0), "delete") res := h.repoMakeRole() @@ -308,10 +390,8 @@ func TestRoleLabels(t *testing.T) { h := newHelper(t) h.clearRoles() - h.allow(types.ComponentRbacResource(), "role.create") - h.allow(types.RoleRbacResource(0), "read") - h.allow(types.RoleRbacResource(0), "update") - h.allow(types.RoleRbacResource(0), "delete") + helpers.AllowMe(h, types.ComponentRbacResource(), "role.create") + helpers.AllowMe(h, types.RoleRbacResource(0), "read", "update", "delete") var ( ID uint64 @@ -387,7 +467,7 @@ func TestRoleLabels(t *testing.T) { func TestMemberList(t *testing.T) { h := newHelper(t) - h.allow(types.RoleRBACResource, "read") + helpers.AllowMe(h, types.RoleRbacResource(0), "read") r := h.repoMakeRole(h.randEmail()) h.createRoleMember(id.Next(), r.ID) @@ -404,7 +484,7 @@ func TestMemberList(t *testing.T) { func TestMemberAdd(t *testing.T) { h := newHelper(t) - h.allow(types.RoleRBACResource.AppendWildcard(), "members.manage") + helpers.AllowMe(h, types.RoleRbacResource(0), "members.manage") r := h.repoMakeRole(h.randEmail()) u := h.createUserWithEmail(h.randEmail()) @@ -419,7 +499,7 @@ func TestMemberAdd(t *testing.T) { func TestMemberRemove(t *testing.T) { h := newHelper(t) - h.allow(types.RoleRBACResource.AppendWildcard(), "members.manage") + helpers.AllowMe(h, types.RoleRbacResource(0), "members.manage") r := h.repoMakeRole(h.randEmail()) u := h.createUserWithEmail(h.randEmail()) diff --git a/tests/system/scim_test.go b/tests/system/scim_test.go index 0287e5874..13c532a79 100644 --- a/tests/system/scim_test.go +++ b/tests/system/scim_test.go @@ -3,6 +3,10 @@ package system import ( "context" "fmt" + "net/http" + "regexp" + "testing" + "github.com/cortezaproject/corteza-server/pkg/api/server" "github.com/cortezaproject/corteza-server/pkg/logger" "github.com/cortezaproject/corteza-server/store" @@ -12,9 +16,6 @@ import ( "github.com/go-chi/chi" "github.com/steinfletcher/apitest" jsonpath "github.com/steinfletcher/apitest-jsonpath" - "net/http" - "regexp" - "testing" ) // apitest basics, initialize, set handler, add auth diff --git a/tests/system/settings_test.go b/tests/system/settings_test.go index 41ff6617c..8b16ef56a 100644 --- a/tests/system/settings_test.go +++ b/tests/system/settings_test.go @@ -1,19 +1,20 @@ package system import ( + "net/http" + "testing" + "github.com/cortezaproject/corteza-server/system/service" "github.com/cortezaproject/corteza-server/system/types" "github.com/cortezaproject/corteza-server/tests/helpers" sqlTypes "github.com/jmoiron/sqlx/types" jsonpath "github.com/steinfletcher/apitest-jsonpath" - "net/http" - "testing" ) func TestSettingsList(t *testing.T) { h := newHelper(t) - h.allow(types.ComponentRbacResource(), "settings.read") - h.allow(types.ComponentRbacResource(), "settings.manage") + helpers.AllowMe(h, types.ComponentRbacResource(), "settings.read") + helpers.AllowMe(h, types.ComponentRbacResource(), "settings.manage") err := service.DefaultSettings.BulkSet(h.secCtx(), types.SettingValueSet{ &types.SettingValue{Name: "t_sys_k1.s1", Value: sqlTypes.JSONText(`"t_sys_v1"`)}, @@ -35,7 +36,7 @@ func TestSettingsList(t *testing.T) { func TestSettingsList_noPermissions(t *testing.T) { h := newHelper(t) - h.deny(types.ComponentRbacResource(), "settings.read") + helpers.DenyMe(h, types.ComponentRbacResource(), "settings.read") h.apiInit(). Get("/settings/"). @@ -48,8 +49,8 @@ func TestSettingsList_noPermissions(t *testing.T) { func TestSettingsUpdate(t *testing.T) { h := newHelper(t) - h.allow(types.ComponentRbacResource(), "settings.manage") - h.allow(types.ComponentRbacResource(), "settings.read") + helpers.AllowMe(h, types.ComponentRbacResource(), "settings.manage") + helpers.AllowMe(h, types.ComponentRbacResource(), "settings.read") err := service.DefaultSettings.BulkSet(h.secCtx(), types.SettingValueSet{ &types.SettingValue{Name: "t_sys_k1.s1", Value: sqlTypes.JSONText(`"t_sys_v1"`)}, @@ -75,7 +76,7 @@ func TestSettingsUpdate(t *testing.T) { func TestSettingsUpdate_noPermissions(t *testing.T) { h := newHelper(t) - h.deny(types.ComponentRbacResource(), "settings.manage") + helpers.DenyMe(h, types.ComponentRbacResource(), "settings.manage") h.apiInit(). Patch("/settings/"). @@ -89,8 +90,8 @@ func TestSettingsUpdate_noPermissions(t *testing.T) { func TestSettingsGet(t *testing.T) { h := newHelper(t) - h.allow(types.ComponentRbacResource(), "settings.read") - h.allow(types.ComponentRbacResource(), "settings.manage") + helpers.AllowMe(h, types.ComponentRbacResource(), "settings.read") + helpers.AllowMe(h, types.ComponentRbacResource(), "settings.manage") err := service.DefaultSettings.BulkSet(h.secCtx(), types.SettingValueSet{ &types.SettingValue{Name: "t_sys_k1.s1", Value: sqlTypes.JSONText(`"t_sys_v1"`)}, @@ -117,7 +118,7 @@ func TestSettingsGet(t *testing.T) { func TestSettingsGet_noPermissions(t *testing.T) { h := newHelper(t) - h.deny(types.ComponentRbacResource(), "settings.read") + helpers.DenyMe(h, types.ComponentRbacResource(), "settings.read") h.apiInit(). Get("/settings/t_sys_k1.s1"). @@ -130,7 +131,7 @@ func TestSettingsGet_noPermissions(t *testing.T) { func TestSettingsSet_noPermissions(t *testing.T) { h := newHelper(t) - h.deny(types.SystemRBACResource, "settings.read") + helpers.DenyMe(h, types.ComponentRbacResource(), "settings.read") h.apiInit(). Get("/settings/t_sys_k1.s1"). @@ -143,7 +144,7 @@ func TestSettingsSet_noPermissions(t *testing.T) { func TestSettingsCurrent(t *testing.T) { h := newHelper(t) - h.allow(types.SystemRBACResource, "settings.read") + helpers.AllowMe(h, types.ComponentRbacResource(), "settings.read") h.apiInit(). Get("/settings/current"). diff --git a/tests/system/template_test.go b/tests/system/template_test.go index fb444ef37..21dd32257 100644 --- a/tests/system/template_test.go +++ b/tests/system/template_test.go @@ -48,10 +48,13 @@ func TestTemplateRead(t *testing.T) { h := newHelper(t) h.clearTemplates() + helpers.AllowMe(h, types.TemplateRbacResource(0), "read") + u := h.repoMakeTemplate() h.apiInit(). Get(fmt.Sprintf("/template/%d", u.ID)). + Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). @@ -64,11 +67,14 @@ func TestTemplateList(t *testing.T) { h := newHelper(t) h.clearTemplates() + helpers.AllowMe(h, types.TemplateRbacResource(0), "read") + h.repoMakeTemplate(rs()) h.repoMakeTemplate(rs()) h.apiInit(). Get("/template/"). + Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). @@ -87,10 +93,11 @@ func TestTemplateList_filterForbidden(t *testing.T) { h.repoMakeTemplate("template") f := h.repoMakeTemplate() - h.deny(f.RbacResource(), "read") + helpers.DenyMe(h, f.RbacResource(), "read") h.apiInit(). Get("/template/"). + Header("Accept", "application/json"). Query("handle", f.Handle). Expect(t). Status(http.StatusOK). @@ -116,10 +123,11 @@ func TestTemplateCreateForbidden(t *testing.T) { func TestTemplateCreate(t *testing.T) { h := newHelper(t) h.clearTemplates() - h.allow(types.ComponentRbacResource(), "template.create") + helpers.AllowMe(h, types.ComponentRbacResource(), "template.create") h.apiInit(). Post("/template/"). + Header("Accept", "application/json"). FormData("handle", rs()). FormData("handle", "handle_"+rs()). Expect(t). @@ -147,7 +155,7 @@ func TestTemplateUpdate(t *testing.T) { h := newHelper(t) h.clearTemplates() res := h.repoMakeTemplate() - h.allow(types.TemplateRbacResource(0), "update") + helpers.AllowMe(h, types.TemplateRbacResource(0), "update") newHandle := "updated-" + rs() @@ -182,7 +190,7 @@ func TestTemplateDeleteForbidden(t *testing.T) { func TestTemplateDelete(t *testing.T) { h := newHelper(t) h.clearTemplates() - h.allow(types.TemplateRbacResource(0), "delete") + helpers.AllowMe(h, types.TemplateRbacResource(0), "delete") res := h.repoMakeTemplate() @@ -202,7 +210,7 @@ func TestTemplateDelete(t *testing.T) { func TestTemplateUndelete(t *testing.T) { h := newHelper(t) h.clearTemplates() - h.allow(types.TemplateRBACResource.AppendWildcard(), "delete") + helpers.AllowMe(h, types.TemplateRbacResource(0), "delete") res := h.repoMakeTemplate() @@ -219,10 +227,11 @@ func TestTemplateUndelete(t *testing.T) { h.a.Nil(res.DeletedAt) } -func TestTemplateRenderForbiden(t *testing.T) { +func TestTemplateRenderForbidden(t *testing.T) { h := newHelper(t) h.clearTemplates() - h.deny(types.TemplateRbacResource(0), "render") + helpers.AllowMe(h, types.TemplateRbacResource(0), "read") + helpers.DenyMe(h, types.TemplateRbacResource(0), "render") res := h.repoMakeTemplate("rendering", "Hello, {{.interpolate}}", "text/plain") @@ -239,7 +248,8 @@ func TestTemplateRenderForbiden(t *testing.T) { func TestTemplateRenderDriverUndefined(t *testing.T) { h := newHelper(t) h.clearTemplates() - h.allow(types.TemplateRbacResource(0), "render") + helpers.AllowMe(h, types.TemplateRbacResource(0), "read") + helpers.AllowMe(h, types.TemplateRbacResource(0), "render") res := h.repoMakeTemplate("rendering", "Hello, {{.interpolate}}", "text/notexisting") @@ -256,12 +266,14 @@ func TestTemplateRenderDriverUndefined(t *testing.T) { func TestTemplateRenderPlain(t *testing.T) { h := newHelper(t) h.clearTemplates() - h.allow(types.TemplateRbacResource(0), "render") + helpers.AllowMe(h, types.TemplateRbacResource(0), "read") + helpers.AllowMe(h, types.TemplateRbacResource(0), "render") res := h.repoMakeTemplate("rendering", "Hello, {{.interpolate}}", "text/plain") h.apiInit(). Post(fmt.Sprintf("/template/%d/render/testing.txt", res.ID)). + Header("Accept", "application/json"). JSON(`{"variables": {"interpolate": "world!"}}`). Expect(t). Status(http.StatusOK). @@ -272,12 +284,14 @@ func TestTemplateRenderPlain(t *testing.T) { func TestTemplateRenderHTML(t *testing.T) { h := newHelper(t) h.clearTemplates() - h.allow(types.TemplateRbacResource(0), "render") + helpers.AllowMe(h, types.TemplateRbacResource(0), "read") + helpers.AllowMe(h, types.TemplateRbacResource(0), "render") res := h.repoMakeTemplate("rendering", "

Hello, {{.interpolate}}

", "text/html") h.apiInit(). Post(fmt.Sprintf("/template/%d/render/testing.html", res.ID)). + Header("Accept", "application/json"). JSON(`{"variables": {"interpolate": "world!"}}`). Expect(t). Status(http.StatusOK). diff --git a/tests/system/user_test.go b/tests/system/user_test.go index 689126d8b..b7c0e7880 100644 --- a/tests/system/user_test.go +++ b/tests/system/user_test.go @@ -3,6 +3,11 @@ package system import ( "context" "fmt" + "net/http" + "net/url" + "testing" + "time" + "github.com/cortezaproject/corteza-server/pkg/id" "github.com/cortezaproject/corteza-server/store" "github.com/cortezaproject/corteza-server/system/service" @@ -10,10 +15,6 @@ import ( "github.com/cortezaproject/corteza-server/tests/helpers" "github.com/steinfletcher/apitest-jsonpath" "github.com/stretchr/testify/require" - "net/http" - "net/url" - "testing" - "time" ) func (h helper) randEmail() string { @@ -62,6 +63,7 @@ func TestUserRead(t *testing.T) { h.apiInit(). Get(fmt.Sprintf("/users/%d", u.ID)). + Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). @@ -70,10 +72,11 @@ func TestUserRead(t *testing.T) { End() u = h.createUserWithEmail(h.randEmail()) - h.allow(types.UserRbacResource(0), "unmask.email") + helpers.AllowMe(h, types.UserRbacResource(0), "email.unmask") h.apiInit(). Get(fmt.Sprintf("/users/%d", u.ID)). + Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). @@ -92,10 +95,11 @@ func TestUserListAll(t *testing.T) { h.createUserWithEmail(h.randEmail()) } - h.allow(types.UserRbacResource(0), "read") + helpers.AllowMe(h, types.UserRbacResource(0), "read") h.apiInit(). Get("/users/"). + Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). @@ -115,7 +119,7 @@ func TestUserListWithPaging(t *testing.T) { h.createUserWithEmail(h.randEmail()) } - h.allow(types.UserRbacResource(0), "read") + helpers.AllowMe(h, types.UserRbacResource(0), "read") var aux = struct { Response struct { @@ -129,6 +133,7 @@ func TestUserListWithPaging(t *testing.T) { h.apiInit(). Get("/users/"). Query("limit", "13"). + Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). @@ -163,12 +168,12 @@ func TestUserList_filterForbidden(t *testing.T) { h := newHelper(t) h.clearUsers() - h.allow(types.UserRbacResource(0), "read") + helpers.AllowMe(h, types.UserRbacResource(0), "read") h.createUserWithEmail("usr") f := h.createUserWithEmail(h.randEmail()) - h.deny(f.RbacResource(), "read") + helpers.DenyMe(h, f.RbacResource(), "read") h.apiInit(). Get("/users/"). @@ -186,10 +191,11 @@ func TestUserListQuery(t *testing.T) { h.secCtx() - h.allow(types.UserRbacResource(0), "read") + helpers.AllowMe(h, types.UserRbacResource(0), "read") h.apiInit(). Get("/users/"). + Header("Accept", "application/json"). Query("query", h.randEmail()). Query("email", h.randEmail()). Query("name", "John Doe"). @@ -207,14 +213,15 @@ func TestUserListQueryEmail(t *testing.T) { h.clearUsers() h.secCtx() - h.allow(types.UserRbacResource(0), "read") - h.allow(types.UserRbacResource(0), "unmask.email") + helpers.AllowMe(h, types.UserRbacResource(0), "read") + helpers.AllowMe(h, types.UserRbacResource(0), "email.unmask") ee := h.randEmail() h.createUserWithEmail(ee) h.apiInit(). Get("/users/"). + Header("Accept", "application/json"). Query("email", ee). Expect(t). Status(http.StatusOK). @@ -228,7 +235,7 @@ func TestUserListQueryUsername(t *testing.T) { h.clearUsers() h.secCtx() - h.allow(types.UserRbacResource(0), "read") + helpers.AllowMe(h, types.UserRbacResource(0), "read") ee := h.randEmail() h.createUser(&types.User{ @@ -238,6 +245,7 @@ func TestUserListQueryUsername(t *testing.T) { h.apiInit(). Get("/users/"). + Header("Accept", "application/json"). Query("username", ee). Expect(t). Status(http.StatusOK). @@ -251,7 +259,7 @@ func TestUserListQueryHandle(t *testing.T) { h.clearUsers() h.secCtx() - h.allow(types.UserRbacResource(0), "read") + helpers.AllowMe(h, types.UserRbacResource(0), "read") h.createUser(&types.User{ Email: "test@test.tld", @@ -261,6 +269,7 @@ func TestUserListQueryHandle(t *testing.T) { h.apiInit(). Get("/users/"). Query("handle", "johnDoe"). + Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). @@ -275,7 +284,7 @@ func TestUserListWithOneAllowed(t *testing.T) { h.secCtx() newUserWeCanAccess := h.createUserWithEmail(h.randEmail()) - h.allow(newUserWeCanAccess.RbacResource(), "read") + helpers.AllowMe(h, newUserWeCanAccess.RbacResource(), "read") // And one we cannot access h.createUserWithEmail(h.randEmail()) @@ -288,6 +297,7 @@ func TestUserListWithOneAllowed(t *testing.T) { h.apiInit(). Get("/users/"). + Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). @@ -316,12 +326,13 @@ func TestUserCreate(t *testing.T) { h := newHelper(t) h.clearUsers() - h.allow(types.ComponentRbacResource(), "user.create") + helpers.AllowMe(h, types.ComponentRbacResource(), "user.create") email := h.randEmail() h.apiInit(). Post("/users/"). + Header("Accept", "application/json"). FormData("email", email). Expect(t). Status(http.StatusOK). @@ -350,12 +361,13 @@ func TestUserUpdate(t *testing.T) { h.clearUsers() u := h.createUserWithEmail(h.randEmail()) - h.allow(types.UserRbacResource(0), "update") + helpers.AllowMe(h, types.UserRbacResource(0), "update") newEmail := h.randEmail() h.apiInit(). Put(fmt.Sprintf("/users/%d", u.ID)). + Header("Accept", "application/json"). FormData("email", newEmail). Expect(t). Status(http.StatusOK). @@ -368,7 +380,7 @@ func TestUserSetPassword(t *testing.T) { h.clearUsers() u := h.createUserWithEmail(h.randEmail()) - h.allow(types.UserRBACResource.AppendWildcard(), "update") + helpers.AllowMe(h, types.UserRbacResource(0), "update") h.apiInit(). Post(fmt.Sprintf("/users/%d/password", u.ID)). @@ -384,7 +396,7 @@ func TestUserSuspend(t *testing.T) { h.clearUsers() u := h.createUserWithEmail(h.randEmail()) - h.allow(types.UserRBACResource.AppendWildcard(), "suspend") + helpers.AllowMe(h, types.UserRbacResource(0), "suspend") h.apiInit(). Post(fmt.Sprintf("/users/%d/suspend", u.ID)). @@ -403,7 +415,7 @@ func TestUserUnsuspend(t *testing.T) { h.clearUsers() u := h.createUserWithEmail(h.randEmail()) - h.allow(types.UserRBACResource.AppendWildcard(), "unsuspend") + helpers.AllowMe(h, types.UserRbacResource(0), "unsuspend") h.apiInit(). Post(fmt.Sprintf("/users/%d/unsuspend", u.ID)). @@ -436,12 +448,13 @@ func TestUserDelete(t *testing.T) { h := newHelper(t) h.clearUsers() - h.allow(types.UserRbacResource(0), "delete") + helpers.AllowMe(h, types.UserRbacResource(0), "delete") u := h.createUserWithEmail(h.randEmail()) h.apiInit(). Delete(fmt.Sprintf("/users/%d", u.ID)). + Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). @@ -452,7 +465,7 @@ func TestUserUndelete(t *testing.T) { h := newHelper(t) h.clearUsers() - h.allow(types.UserRBACResource.AppendWildcard(), "delete") + helpers.AllowMe(h, types.UserRbacResource(0), "delete") u := h.createUserWithEmail(h.randEmail()) @@ -472,10 +485,10 @@ func TestUserLabels(t *testing.T) { h := newHelper(t) h.clearUsers() - h.allow(types.ComponentRbacResource(), "user.create") - h.allow(types.UserRbacResource(0), "read") - h.allow(types.UserRbacResource(0), "update") - h.allow(types.UserRbacResource(0), "delete") + helpers.AllowMe(h, types.ComponentRbacResource(), "user.create") + helpers.AllowMe(h, types.UserRbacResource(0), "read") + helpers.AllowMe(h, types.UserRbacResource(0), "update") + helpers.AllowMe(h, types.UserRbacResource(0), "delete") var ( ID uint64 @@ -552,7 +565,7 @@ func TestUserLabels(t *testing.T) { func TestUserMemberList(t *testing.T) { h := newHelper(t) h.clearUsers() - h.allow(types.RoleRBACResource, "read") + helpers.AllowMe(h, types.RoleRbacResource(0), "read") u := h.createUserWithEmail(h.randEmail()) @@ -573,7 +586,7 @@ func TestUserMemberList(t *testing.T) { func TestUserMemberAdd(t *testing.T) { h := newHelper(t) h.clearUsers() - h.allow(types.RoleRBACResource.AppendWildcard(), "members.manage") + helpers.AllowMe(h, types.RoleRbacResource(0), "members.manage") u := h.createUserWithEmail(h.randEmail()) @@ -591,7 +604,7 @@ func TestUserMemberAdd(t *testing.T) { func TestUserMemberRemove(t *testing.T) { h := newHelper(t) h.clearUsers() - h.allow(types.RoleRBACResource.AppendWildcard(), "members.manage") + helpers.AllowMe(h, types.RoleRbacResource(0), "members.manage") u := h.createUserWithEmail(h.randEmail())