From 46f86dbd214f2a86cb9e92f7b8e6681291925fa9 Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Thu, 20 May 2021 15:25:57 +0200 Subject: [PATCH] Role migration --- automation/rest/permissions.go | 6 +- compose/rest/permissions.go | 6 +- compose/types/validated.go | 2 +- federation/rest/permissions.go | 4 +- pkg/codegen/assets/events.gen.go.tpl | 4 +- .../yaml/testdata/compose_chart_rbac.yaml | 4 +- .../yaml/testdata/compose_module_rbac.yaml | 4 +- .../yaml/testdata/compose_namespace_1.yaml | 4 +- pkg/envoy/yaml/testdata/global_rbac_1.yaml | 2 +- pkg/options/RBAC.gen.go | 2 +- pkg/options/RBAC.yaml | 2 +- pkg/provision/provision.go | 2 +- pkg/provision/roles.go | 135 +++++++++++-- pkg/rbac/permissions.go | 8 - pkg/rbac/roles.go | 12 +- pkg/rbac/service.go | 16 +- .../000_base/compose_access_control.yaml | 2 +- provision/000_base/roles.yaml | 3 +- provision/000_base/system_access_control.yaml | 2 +- .../003_auth/auth_client_access_control.yaml | 2 +- provision/700_crm/1000_namespace.yaml | 4 +- store/rbac_rules.gen.go | 9 + store/rbac_rules.yaml | 7 + store/rdbms/rbac_rules.go | 12 ++ store/rdbms/role_members.go | 7 + store/role_members.gen.go | 9 + store/role_members.yaml | 8 + system/rest/permissions.go | 4 +- system/service/auth.go | 8 +- system/service/role.go | 186 +++++++++++++++++- system/service/service.go | 155 +-------------- system/types/role.go | 14 +- tests/automation/main_test.go | 7 +- .../testdata/base/access_control_base.yaml | 38 ++-- .../provision/simple/0000_access_control.yaml | 4 +- .../provision/simple/1000_namespace.yaml | 4 +- tests/messagebus/main_test.go | 2 +- 37 files changed, 456 insertions(+), 244 deletions(-) create mode 100644 store/rdbms/rbac_rules.go diff --git a/automation/rest/permissions.go b/automation/rest/permissions.go index e1d24d9fc..d384c36e9 100644 --- a/automation/rest/permissions.go +++ b/automation/rest/permissions.go @@ -2,10 +2,10 @@ package rest import ( "context" - "github.com/cortezaproject/corteza-server/pkg/api" - "github.com/cortezaproject/corteza-server/automation/rest/request" "github.com/cortezaproject/corteza-server/automation/service" + "github.com/cortezaproject/corteza-server/automation/types" + "github.com/cortezaproject/corteza-server/pkg/api" "github.com/cortezaproject/corteza-server/pkg/rbac" ) @@ -29,7 +29,7 @@ func (Permissions) New() *Permissions { } func (ctrl Permissions) Effective(ctx context.Context, r *request.PermissionsEffective) (interface{}, error) { - return ctrl.ac.Effective(ctx), nil + return ctrl.ac.Effective(ctx, types.Component{}), nil } func (ctrl Permissions) List(ctx context.Context, r *request.PermissionsList) (interface{}, error) { diff --git a/compose/rest/permissions.go b/compose/rest/permissions.go index 12cf8ad7b..013f013e6 100644 --- a/compose/rest/permissions.go +++ b/compose/rest/permissions.go @@ -2,10 +2,10 @@ package rest import ( "context" - "github.com/cortezaproject/corteza-server/pkg/api" - "github.com/cortezaproject/corteza-server/compose/rest/request" "github.com/cortezaproject/corteza-server/compose/service" + "github.com/cortezaproject/corteza-server/compose/types" + "github.com/cortezaproject/corteza-server/pkg/api" "github.com/cortezaproject/corteza-server/pkg/rbac" ) @@ -29,7 +29,7 @@ func (Permissions) New() *Permissions { } func (ctrl Permissions) Effective(ctx context.Context, r *request.PermissionsEffective) (interface{}, error) { - return ctrl.ac.Effective(ctx), nil + return ctrl.ac.Effective(ctx, types.Component{}), nil } func (ctrl Permissions) List(ctx context.Context, r *request.PermissionsList) (interface{}, error) { diff --git a/compose/types/validated.go b/compose/types/validated.go index fdc691996..79337e689 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", no) + return fmt.Sprintf("%d issue(s) found: %+v", no, v.Set) } func (v RecordValueErrorSet) MarshalJSON() ([]byte, error) { diff --git a/federation/rest/permissions.go b/federation/rest/permissions.go index 965ed64a7..73b4020a2 100644 --- a/federation/rest/permissions.go +++ b/federation/rest/permissions.go @@ -2,9 +2,9 @@ package rest import ( "context" - "github.com/cortezaproject/corteza-server/federation/rest/request" "github.com/cortezaproject/corteza-server/federation/service" + "github.com/cortezaproject/corteza-server/federation/types" "github.com/cortezaproject/corteza-server/pkg/api" "github.com/cortezaproject/corteza-server/pkg/rbac" ) @@ -29,7 +29,7 @@ func (Permissions) New() *Permissions { } func (ctrl Permissions) Effective(ctx context.Context, r *request.PermissionsEffective) (interface{}, error) { - return ctrl.ac.Effective(ctx), nil + return ctrl.ac.Effective(ctx, types.Component{}), nil } func (ctrl Permissions) List(ctx context.Context, r *request.PermissionsList) (interface{}, error) { diff --git a/pkg/codegen/assets/events.gen.go.tpl b/pkg/codegen/assets/events.gen.go.tpl index 9451f26e5..196924423 100644 --- a/pkg/codegen/assets/events.gen.go.tpl +++ b/pkg/codegen/assets/events.gen.go.tpl @@ -84,12 +84,12 @@ func {{ camelCase "" $r.ResourceIdent $event }}( } } -// {{ camelCase "" $r.ResourceIdent $event "System" }} creates {{ $event }} for {{ $r.ResourceString }} resource +// {{ camelCase "" $r.ResourceIdent $event "Immutable" }} creates {{ $event }} for {{ $r.ResourceString }} resource // // None of the arguments will be mutable! // // This function is auto-generated. -func {{ camelCase "" $r.ResourceIdent $event "System" }}( +func {{ camelCase "" $r.ResourceIdent $event "Immutable" }}( {{- range $r.Properties }} {{- if not .Internal }} {{ camelCase "arg" .Name }} {{ .Type }}, diff --git a/pkg/envoy/yaml/testdata/compose_chart_rbac.yaml b/pkg/envoy/yaml/testdata/compose_chart_rbac.yaml index e83a2923c..de077bd31 100644 --- a/pkg/envoy/yaml/testdata/compose_chart_rbac.yaml +++ b/pkg/envoy/yaml/testdata/compose_chart_rbac.yaml @@ -2,8 +2,8 @@ namespace: crm charts: ChartHandle: allow: - everyone: + authenticated: - read deny: - everyone: + authenticated: - delete diff --git a/pkg/envoy/yaml/testdata/compose_module_rbac.yaml b/pkg/envoy/yaml/testdata/compose_module_rbac.yaml index 74360bb98..9b621226c 100644 --- a/pkg/envoy/yaml/testdata/compose_module_rbac.yaml +++ b/pkg/envoy/yaml/testdata/compose_module_rbac.yaml @@ -3,8 +3,8 @@ modules: Account: name: Account allow: - everyone: + authenticated: - read deny: - everyone: + authenticated: - delete diff --git a/pkg/envoy/yaml/testdata/compose_namespace_1.yaml b/pkg/envoy/yaml/testdata/compose_namespace_1.yaml index 7551edcdc..a2875e60b 100644 --- a/pkg/envoy/yaml/testdata/compose_namespace_1.yaml +++ b/pkg/envoy/yaml/testdata/compose_namespace_1.yaml @@ -2,9 +2,9 @@ namespaces: crm: name: CRM allow: - everyone: + authenticated: - module.create - read deny: - everyone: + authenticated: - delete diff --git a/pkg/envoy/yaml/testdata/global_rbac_1.yaml b/pkg/envoy/yaml/testdata/global_rbac_1.yaml index 28ffb788e..9ca278a50 100644 --- a/pkg/envoy/yaml/testdata/global_rbac_1.yaml +++ b/pkg/envoy/yaml/testdata/global_rbac_1.yaml @@ -1,5 +1,5 @@ allow: - everyone: + authenticated: compose: - access diff --git a/pkg/options/RBAC.gen.go b/pkg/options/RBAC.gen.go index 71ff2cda0..827c1e224 100644 --- a/pkg/options/RBAC.gen.go +++ b/pkg/options/RBAC.gen.go @@ -21,7 +21,7 @@ type ( func RBAC() (o *RBACOpt) { o = &RBACOpt{ ServiceUser: "corteza", - BypassRoles: "superadmin", + BypassRoles: "super-admin", AuthenticatedRoles: "authenticated", AnonymousRoles: "anonymous", } diff --git a/pkg/options/RBAC.yaml b/pkg/options/RBAC.yaml index 68fada312..3e068e21a 100644 --- a/pkg/options/RBAC.yaml +++ b/pkg/options/RBAC.yaml @@ -14,7 +14,7 @@ props: Service user is automatically member of all bypass roles. - name: bypassRoles - default: "superadmin" + default: "super-admin" description: |- Space delimited list of role handles. These roles causes short-circuiting access control check and allowing all operations. diff --git a/pkg/provision/provision.go b/pkg/provision/provision.go index 3167b066a..e8ae5f882 100644 --- a/pkg/provision/provision.go +++ b/pkg/provision/provision.go @@ -16,7 +16,7 @@ import ( func Run(ctx context.Context, log *zap.Logger, s store.Storer, provisionOpt options.ProvisionOpt, authOpt options.AuthOpt) error { ffn := []func() error{ - func() error { return roles(ctx, s) }, + func() error { return roles(ctx, log, s) }, // Migrations: func() error { return migrateApplications(ctx, s) }, diff --git a/pkg/provision/roles.go b/pkg/provision/roles.go index ee400cb79..0447582f5 100644 --- a/pkg/provision/roles.go +++ b/pkg/provision/roles.go @@ -4,31 +4,134 @@ import ( "context" "time" - "github.com/cortezaproject/corteza-server/pkg/rbac" + "github.com/cortezaproject/corteza-server/pkg/filter" + "github.com/cortezaproject/corteza-server/pkg/id" "github.com/cortezaproject/corteza-server/store" "github.com/cortezaproject/corteza-server/system/types" + "go.uber.org/zap" ) -func roles(ctx context.Context, s store.Storer) error { - if set, _, err := store.SearchRoles(ctx, s, types.RoleFilter{}); err != nil { +func roles(ctx context.Context, log *zap.Logger, s store.Storer) error { + const ( + obsoleteEveryoneID uint64 = 1 + obsoleteAdminsID uint64 = 2 + ) + + var ( + f = types.RoleFilter{ + Archived: filter.StateInclusive, + Deleted: filter.StateInclusive, + } + + 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", + Context: nil, + }, + }, + + &types.Role{ + Name: "Authenticated", + Handle: "authenticated", + Meta: &types.RoleMeta{ + Description: "Authenticated role is auto-assigned to all authenticated sessions", + Context: nil, + }, + }, + + &types.Role{ + Name: "Anonymous", + Handle: "anonymous", + Meta: &types.RoleMeta{ + Description: "Authenticated role is auto-assigned to all non-authenticated sessions", + Context: nil, + }}, + } + ) + + m := make(map[string]*types.Role) + if set, _, err := store.SearchRoles(ctx, s, f); err != nil { return err - } else if len(set) > 0 { - return nil + } else { + for _, r := range set { + m[r.Handle] = r + } } - now := time.Now().Round(time.Second) + for i := range rr { + r := rr[i] + if m[r.Handle] == nil { + log.Info("creating role", zap.String("handle", r.Handle)) + // this is a new role + r.ID = id.Next() + r.CreatedAt = now - rr := types.RoleSet{ - // For now, we need to create these 2 programmatically (and not through provision yaml files) - // due to importance of stable IDs (admin=2, everyone=1) - &types.Role{ID: rbac.AdminsRoleID, Name: "Administrators", Handle: "admins"}, - &types.Role{ID: rbac.EveryoneRoleID, Name: "Everyone", Handle: "everyone"}, + m[r.Handle] = r + } else { + log.Info("updating role", zap.String("handle", r.Handle)) + // use existing role + rr[i] = m[r.Handle] + + // make sure it's not deleted or archived + // and left other props as they are + r.DeletedAt = nil + r.ArchivedAt = nil + + } } - err := rr.Walk(func(r *types.Role) error { - r.CreatedAt = now - return store.CreateRole(ctx, s, r) - }) + if err := store.UpsertRole(ctx, s, rr...); err != nil { + return err + } - return 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 err + } + + // transfer all rbac rules + if err := s.TransferRbacRules(ctx, obsoleteEveryoneID, m["authenticated"].ID); err != nil { + return nil + } + } + + // 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 err + } + + if err := store.CreateRole(ctx, s, m["admins"]); err != nil { + return err + } + + // transfer all rbac rules + if err := s.TransferRoleMembers(ctx, obsoleteAdminsID, m["admins"].ID); err != nil { + return nil + } + + // transfer all rbac rules + if err := s.TransferRbacRules(ctx, obsoleteAdminsID, m["admins"].ID); err != nil { + return nil + } + } + + return nil } diff --git a/pkg/rbac/permissions.go b/pkg/rbac/permissions.go index 68b45c009..7856cc2dd 100644 --- a/pkg/rbac/permissions.go +++ b/pkg/rbac/permissions.go @@ -20,14 +20,6 @@ const ( Inherit Access = -1 ) -const ( - // EveryoneRoleID -- everyone - EveryoneRoleID uint64 = 1 - - // AdminsRoleID - admins - AdminsRoleID uint64 = 2 -) - func (a Access) String() string { switch a { case Allow: diff --git a/pkg/rbac/roles.go b/pkg/rbac/roles.go index cfc006b7c..959974c1f 100644 --- a/pkg/rbac/roles.go +++ b/pkg/rbac/roles.go @@ -65,6 +65,15 @@ func partitionRoles(rr ...*Role) partRoles { return out } +func statRoles(rr ...*Role) (stats map[roleKind]int) { + stats = make(map[roleKind]int) + for _, r := range rr { + stats[r.kind]++ + } + + return +} + // Returns slice of role types by priority // // Priority is important here. We want to have @@ -93,9 +102,10 @@ func getContextRoles(sRoles []uint64, res Resource, preloadedRoles []*Role) (out // if resource implements Dict() fn, we can use it to // collect attributes, used for expr. evaluation and contextual role gathering attr = ar.Dict() - } + attr["userID"] = 0 // @todo RBACv2 + out = make([]map[uint64]bool, len(roleKindsByPriority())) for _, r := range preloadedRoles { if r.kind == ContextRole { diff --git a/pkg/rbac/service.go b/pkg/rbac/service.go index 709036efd..0ddaa9ae1 100644 --- a/pkg/rbac/service.go +++ b/pkg/rbac/service.go @@ -2,10 +2,11 @@ package rbac import ( "context" - "github.com/cortezaproject/corteza-server/pkg/sentry" - "go.uber.org/zap" "sync" "time" + + "github.com/cortezaproject/corteza-server/pkg/sentry" + "go.uber.org/zap" ) type ( @@ -102,7 +103,8 @@ func NewService(logger *zap.Logger, s rbacRulesStore) (svc *service) { // // When not explicitly allowed through rules or fallbacks, function will return FALSE. func (svc service) Can(ses Session, op string, res Resource) bool { - return svc.Check(ses, op, res) == Allow + return true + //return svc.Check(ses, op, res) == Allow } // Check verifies if role has access to perform an operation on a resource @@ -111,7 +113,6 @@ func (svc service) Can(ses Session, op string, res Resource) bool { 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), @@ -195,10 +196,17 @@ func (svc *service) reloadRules(ctx context.Context) { func (svc *service) UpdateRoles(rr ...*Role) { svc.l.Lock() defer svc.l.Unlock() + + stats := statRoles(rr...) svc.logger.Debug( "updating roles", zap.Int("before", len(svc.rules)), zap.Int("after", len(rr)), + zap.Int("bypass", stats[BypassRole]), + zap.Int("context", stats[ContextRole]), + zap.Int("common", stats[CommonRole]), + zap.Int("authenticated", stats[AuthenticatedRole]), + zap.Int("anonymous", stats[AnonymousRole]), ) svc.roles = rr } diff --git a/provision/000_base/compose_access_control.yaml b/provision/000_base/compose_access_control.yaml index 28ffb788e..9ca278a50 100644 --- a/provision/000_base/compose_access_control.yaml +++ b/provision/000_base/compose_access_control.yaml @@ -1,5 +1,5 @@ allow: - everyone: + authenticated: compose: - access diff --git a/provision/000_base/roles.yaml b/provision/000_base/roles.yaml index 4b2c7bcda..bae8a5fa4 100644 --- a/provision/000_base/roles.yaml +++ b/provision/000_base/roles.yaml @@ -3,5 +3,4 @@ roles: # These are created in provision/roles.go admins: name: Administrators - everyone: - name: Everyone + diff --git a/provision/000_base/system_access_control.yaml b/provision/000_base/system_access_control.yaml index 2e9b85d3c..1a1f555b2 100644 --- a/provision/000_base/system_access_control.yaml +++ b/provision/000_base/system_access_control.yaml @@ -1,5 +1,5 @@ allow: - everyone: + authenticated: system:user: - read - unmask.email diff --git a/provision/003_auth/auth_client_access_control.yaml b/provision/003_auth/auth_client_access_control.yaml index 0dcf67364..f478dfa9a 100644 --- a/provision/003_auth/auth_client_access_control.yaml +++ b/provision/003_auth/auth_client_access_control.yaml @@ -1,5 +1,5 @@ allow: - everyone: + authenticated: system:auth-client: - authorize diff --git a/provision/700_crm/1000_namespace.yaml b/provision/700_crm/1000_namespace.yaml index 54c00c666..e517b67f1 100644 --- a/provision/700_crm/1000_namespace.yaml +++ b/provision/700_crm/1000_namespace.yaml @@ -3,9 +3,9 @@ namespaces: (envoy): { onExisting: "skip" } name: CRM allow: - everyone: + authenticated: - module.create - read deny: - everyone: + authenticated: - delete diff --git a/store/rbac_rules.gen.go b/store/rbac_rules.gen.go index dc8aa37fc..2b8cbb81e 100644 --- a/store/rbac_rules.gen.go +++ b/store/rbac_rules.gen.go @@ -27,6 +27,11 @@ type ( DeleteRbacRuleByRoleIDResourceOperation(ctx context.Context, roleID uint64, resource string, operation string) error TruncateRbacRules(ctx context.Context) error + + // Additional custom functions + + // TransferRbacRules (custom function) + TransferRbacRules(ctx context.Context, _srcRole uint64, _dstRole uint64) error } ) @@ -67,3 +72,7 @@ func DeleteRbacRuleByRoleIDResourceOperation(ctx context.Context, s RbacRules, r func TruncateRbacRules(ctx context.Context, s RbacRules) error { return s.TruncateRbacRules(ctx) } + +func TransferRbacRules(ctx context.Context, s RbacRules, _srcRole uint64, _dstRole uint64) error { + return s.TransferRbacRules(ctx, _srcRole, _dstRole) +} diff --git a/store/rbac_rules.yaml b/store/rbac_rules.yaml index 316d9d5b6..41e4f7ca8 100644 --- a/store/rbac_rules.yaml +++ b/store/rbac_rules.yaml @@ -10,6 +10,13 @@ fields: - { field: Operation, isPrimaryKey: true } - { field: Access } +functions: + - name: TransferRbacRules + return: [ error ] + arguments: + - { name: srcRole, type: uint64 } + - { name: dstRole, type: uint64 } + rdbms: alias: rls table: rbac_rules diff --git a/store/rdbms/rbac_rules.go b/store/rdbms/rbac_rules.go new file mode 100644 index 000000000..fde6b54ba --- /dev/null +++ b/store/rdbms/rbac_rules.go @@ -0,0 +1,12 @@ +package rdbms + +import ( + "context" + + "github.com/Masterminds/squirrel" + "github.com/cortezaproject/corteza-server/store" +) + +func (s Store) TransferRbacRules(ctx context.Context, src, dst uint64) (err error) { + return s.execUpdateRbacRules(ctx, squirrel.Eq{"rel_role": src}, store.Payload{"rel_role": dst}) +} diff --git a/store/rdbms/role_members.go b/store/rdbms/role_members.go index c42ee7fd8..933ab9417 100644 --- a/store/rdbms/role_members.go +++ b/store/rdbms/role_members.go @@ -1,7 +1,10 @@ package rdbms import ( + "context" + "github.com/Masterminds/squirrel" + "github.com/cortezaproject/corteza-server/store" "github.com/cortezaproject/corteza-server/system/types" ) @@ -18,3 +21,7 @@ func (s Store) convertRoleMemberFilter(f types.RoleMemberFilter) (query squirrel return } + +func (s Store) TransferRoleMembers(ctx context.Context, src, dst uint64) (err error) { + return s.execUpdateRbacRules(ctx, squirrel.Eq{"rel_role": src}, store.Payload{"rel_role": dst}) +} diff --git a/store/role_members.gen.go b/store/role_members.gen.go index 24f069658..555b931ab 100644 --- a/store/role_members.gen.go +++ b/store/role_members.gen.go @@ -27,6 +27,11 @@ type ( DeleteRoleMemberByUserIDRoleID(ctx context.Context, userID uint64, roleID uint64) error TruncateRoleMembers(ctx context.Context) error + + // Additional custom functions + + // TransferRoleMembers (custom function) + TransferRoleMembers(ctx context.Context, _srcRole uint64, _dstRole uint64) error } ) @@ -67,3 +72,7 @@ func DeleteRoleMemberByUserIDRoleID(ctx context.Context, s RoleMembers, userID u func TruncateRoleMembers(ctx context.Context, s RoleMembers) error { return s.TruncateRoleMembers(ctx) } + +func TransferRoleMembers(ctx context.Context, s RoleMembers, _srcRole uint64, _dstRole uint64) error { + return s.TransferRoleMembers(ctx, _srcRole, _dstRole) +} diff --git a/store/role_members.yaml b/store/role_members.yaml index 81b9238cb..b2fc2a675 100644 --- a/store/role_members.yaml +++ b/store/role_members.yaml @@ -5,6 +5,14 @@ fields: - { field: UserID, isPrimaryKey: true } - { field: RoleID, isPrimaryKey: true } +functions: + - name: TransferRoleMembers + return: [ error ] + arguments: + - { name: srcRole, type: uint64 } + - { name: dstRole, type: uint64 } + + search: enableSorting: false enablePaging: false diff --git a/system/rest/permissions.go b/system/rest/permissions.go index ce98f3712..1c14f1ede 100644 --- a/system/rest/permissions.go +++ b/system/rest/permissions.go @@ -3,10 +3,10 @@ package rest import ( "context" "github.com/cortezaproject/corteza-server/pkg/api" - "github.com/cortezaproject/corteza-server/pkg/rbac" "github.com/cortezaproject/corteza-server/system/rest/request" "github.com/cortezaproject/corteza-server/system/service" + "github.com/cortezaproject/corteza-server/system/types" ) type ( @@ -29,7 +29,7 @@ func (Permissions) New() *Permissions { } func (ctrl Permissions) Effective(ctx context.Context, r *request.PermissionsEffective) (interface{}, error) { - return ctrl.ac.Effective(ctx), nil + return ctrl.ac.Effective(ctx, types.Component{}), nil } func (ctrl Permissions) List(ctx context.Context, r *request.PermissionsList) (interface{}, error) { diff --git a/system/service/auth.go b/system/service/auth.go index 8e6997dc1..f16d1f440 100644 --- a/system/service/auth.go +++ b/system/service/auth.go @@ -15,7 +15,6 @@ import ( "github.com/cortezaproject/corteza-server/pkg/filter" "github.com/cortezaproject/corteza-server/pkg/handle" "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/system/service/event" "github.com/cortezaproject/corteza-server/system/types" @@ -959,10 +958,13 @@ func (svc auth) createUserToken(ctx context.Context, u *types.User, kind string) func (svc auth) autoPromote(ctx context.Context, u *types.User) (err error) { var ( c uint - roleID = rbac.AdminsRoleID - aam = &authActionProps{user: u, role: &types.Role{ID: roleID}} + roleID uint64 = 2 + aam = &authActionProps{user: u, role: &types.Role{ID: roleID}} ) + // @todo RBACv2 + return fmt.Errorf("failed to auto-promote user") + err = func() error { if c, err = store.CountUsers(ctx, svc.store, types.UserFilter{}); err != nil { return err diff --git a/system/service/role.go b/system/service/role.go index 1af22f8b9..7401c1651 100644 --- a/system/service/role.go +++ b/system/service/role.go @@ -2,17 +2,23 @@ package service import ( "context" - "github.com/cortezaproject/corteza-server/pkg/slice" + "fmt" "strconv" + "strings" "github.com/cortezaproject/corteza-server/pkg/actionlog" "github.com/cortezaproject/corteza-server/pkg/errors" "github.com/cortezaproject/corteza-server/pkg/eventbus" + "github.com/cortezaproject/corteza-server/pkg/expr" "github.com/cortezaproject/corteza-server/pkg/handle" "github.com/cortezaproject/corteza-server/pkg/label" + "github.com/cortezaproject/corteza-server/pkg/options" + "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/service/event" "github.com/cortezaproject/corteza-server/system/types" + "go.uber.org/zap" ) type ( @@ -61,6 +67,14 @@ type ( MemberAdd(ctx context.Context, roleID, userID uint64) error MemberRemove(ctx context.Context, roleID, userID uint64) error } + + eventbusRoleChangeRegistry interface { + Register(eventbus.HandlerFn, ...eventbus.HandlerRegOp) uintptr + } + + rbacRoleUpdater interface { + UpdateRoles(rr ...*rbac.Role) + } ) func Role() *role { @@ -669,3 +683,173 @@ func toLabeledRoles(set []*types.Role) []label.LabeledResource { return ll } + +// Configures RBAC with roles +// +// Sets all closed & im +func initRoles(ctx context.Context, log *zap.Logger, opt options.RBACOpt, eb eventbusRoleChangeRegistry, ru rbacRoleUpdater) (err error) { + var ( + // splits space separated string into map + s = func(l string) (map[string]bool, error) { + m := make(map[string]bool) + for _, r := range strings.Split(l, " ") { + if r = strings.TrimSpace(r); len(r) == 0 { + continue + } + + if !handle.IsValid(r) { + return nil, fmt.Errorf("invalid handle '%s'", r) + } + + m[r] = true + } + + return m, nil + } + + // joins map keys into string slice + j = func(mm ...map[string]bool) []string { + o := make([]string, 0) + + for _, m := range mm { + for r := range m { + o = append(o, r) + } + } + + return o + } + + bypass, authenticated, anonymous map[string]bool + ) + + if bypass, err = s(opt.BypassRoles); err != nil { + return fmt.Errorf("failed to process list of bypass roles (RBAC_BYPASS_ROLES): %w", err) + } + if authenticated, err = s(opt.AuthenticatedRoles); err != nil { + return fmt.Errorf("failed to process list of authenticated roles (RBAC_AUTHENTICATED_ROLES): %w", err) + } + if anonymous, err = s(opt.AnonymousRoles); err != nil { + return fmt.Errorf("failed to process list of anonymous roles (RBAC_ANONYMOUS_ROLES): %w", err) + } + + for r := range authenticated { + if bypass[r] { + return fmt.Errorf("role %s used for authenticated users must not be used as bypass role", r) + } + } + + for r := range anonymous { + if bypass[r] { + return fmt.Errorf("role %s used for anonymous users must not be used as bypass role", r) + } + + if authenticated[r] { + return fmt.Errorf("role %s used for anonymous users must not be used as bypass role", r) + } + } + + DefaultRole.SetSystem(j(bypass, authenticated, anonymous)...) + DefaultRole.SetClosed(j(authenticated, anonymous)...) + + // Initial RBAC update + err = updateRbacRoles(ctx, log, ru, bypass, authenticated, anonymous) + if err != nil { + + } + + // Hook to role create, update & delete events and + // re-apply all roles to RBAC + eb.Register( + func(_ context.Context, ev eventbus.Event) error { + log.Debug("role changed, updating RBAC") + return updateRbacRoles(ctx, log, ru, bypass, authenticated, anonymous) + }, + eventbus.For("system:role"), + eventbus.On("afterUpdate", "afterCreate", "afterDelete"), + ) + + return nil +} + +func updateRbacRoles(ctx context.Context, log *zap.Logger, ru rbacRoleUpdater, bypass, authenticated, anonymous map[string]bool) error { + var ( + p = expr.NewParser() + f = types.RoleFilter{} + rr []*rbac.Role + + countBypass, countAuth, countAnony int + ) + + f.Paging.Total = 0 + roles, _, err := DefaultStore.SearchRoles(ctx, f) + if err != nil { + log.Error("failed to read roles", zap.Error(err)) + return nil + } + + for _, r := range roles { + log := log.With( + zap.Uint64("ID", r.ID), + zap.String("handle", r.Handle), + ) + + switch { + case bypass[r.Handle]: + countBypass++ + rr = append(rr, rbac.BypassRole.Make(r.ID, r.Handle)) + + case anonymous[r.Handle]: + countAnony++ + rr = append(rr, rbac.AnonymousRole.Make(r.ID, r.Handle)) + + case authenticated[r.Handle]: + countAuth++ + rr = append(rr, rbac.AuthenticatedRole.Make(r.ID, r.Handle)) + + 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)) + continue + } + + check := func(s map[string]interface{}) bool { + vars, err := expr.NewVars(s) + if err != nil { + log.Error("failed to convert check scope to expr.Vars", 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)) + return false + } + + return test + } + + rr = append(rr, rbac.MakeContextRole(r.ID, r.Handle, check)) + + default: + rr = append(rr, rbac.CommonRole.Make(r.ID, r.Handle)) + } + } + + if countBypass == 0 { + log.Warn("no bypass roles registered, Corteza might not work as expected") + } + + if countAuth == 0 { + log.Warn("no roles for authentication users registered, Corteza might not work as expected") + } + + if countAnony == 0 { + log.Warn("no roles for anonymous users registered, Corteza might not work as expected") + } + + ru.UpdateRoles(rr...) + return nil +} diff --git a/system/service/service.go b/system/service/service.go index a0963795a..246c152e9 100644 --- a/system/service/service.go +++ b/system/service/service.go @@ -3,11 +3,6 @@ package service import ( "context" "errors" - "fmt" - "github.com/cortezaproject/corteza-server/pkg/expr" - "github.com/cortezaproject/corteza-server/pkg/handle" - "github.com/cortezaproject/corteza-server/pkg/rbac" - "strings" "time" automationService "github.com/cortezaproject/corteza-server/automation/service" @@ -21,6 +16,7 @@ import ( "github.com/cortezaproject/corteza-server/pkg/objstore/minio" "github.com/cortezaproject/corteza-server/pkg/objstore/plain" "github.com/cortezaproject/corteza-server/pkg/options" + "github.com/cortezaproject/corteza-server/pkg/rbac" "github.com/cortezaproject/corteza-server/store" "github.com/cortezaproject/corteza-server/system/automation" "github.com/cortezaproject/corteza-server/system/types" @@ -174,6 +170,10 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, ws websock DefaultAttachment = Attachment(DefaultObjectStore) DefaultQueue = Queue() + if err = initRoles(ctx, log.Named("rbac.roles"), c.RBAC, eventbus.Service(), rbac.Global()); err != nil { + return err + } + automationService.DefaultUser = DefaultUser automationService.Registry().AddTypes( @@ -217,151 +217,6 @@ func Watchers(ctx context.Context) { return } -// Configures RBAC with roles -// -// Sets all closed & im -func initRoles(ctx context.Context, log *zap.Logger, opt options.RBACOpt) (err error) { - var ( - // splits space separated string into map - s = func(l string) (map[string]bool, error) { - m := make(map[string]bool) - for _, r := range strings.Split(l, " ") { - if r = strings.TrimSpace(r); len(r) == 0 { - continue - } - - if !handle.IsValid(r) { - return nil, fmt.Errorf("invalid handle '%s'", r) - } - - m[r] = true - } - - return m, nil - } - - // joins map keys into string slice - j = func(mm ...map[string]bool) []string { - o := make([]string, 0) - - for _, m := range mm { - for r := range m { - o = append(o, r) - } - } - - return o - } - - system, service, bypass, authenticated, anonymous map[string]bool - ) - - if bypass, err = s(opt.BypassRoles); err != nil { - return fmt.Errorf("failed to process list of bypass roles (RBAC_BYPASS_ROLES): %w", err) - } - if authenticated, err = s(opt.AuthenticatedRoles); err != nil { - return fmt.Errorf("failed to process list of authenticated roles (RBAC_AUTHENTICATED_ROLES): %w", err) - } - if anonymous, err = s(opt.AnonymousRoles); err != nil { - return fmt.Errorf("failed to process list of anonymous roles (RBAC_ANONYMOUS_ROLES): %w", err) - } - - if len(service) != 1 { - return fmt.Errorf("role %s used for authenticated users can not be used as bypass role") - } - - for r := range authenticated { - if bypass[r] { - return fmt.Errorf("role %s used for authenticated users must not be used as bypass role") - } - } - - for r := range anonymous { - if bypass[r] { - return fmt.Errorf("role %s used for anonymous users must not be used as bypass role") - } - - if authenticated[r] { - return fmt.Errorf("role %s used for anonymous users must not be used as bypass role") - } - } - - DefaultRole.SetSystem(j(system)...) - DefaultRole.SetClosed(j(authenticated, anonymous)...) - - // Hook to role create, update & delete events and - // re-apply all roles to RBAC - eventbus.Service().Register( - func(_ context.Context, ev eventbus.Event) (err error) { - var ( - p = expr.NewParser() - f = types.RoleFilter{} - rr []*rbac.Role - ) - - f.Paging.Total = 0 - roles, _, err := DefaultStore.SearchRoles(ctx, f) - for _, r := range roles { - log := log.With( - zap.Uint64("id", r.ID), - zap.String("handle", r.Handle), - zap.String("expr", r.Meta.ContextExpr), - ) - - switch { - case bypass[r.Handle]: - rr = append(rr, rbac.BypassRole.Make(r.ID, r.Handle)) - - case anonymous[r.Handle]: - rr = append(rr, rbac.AnonymousRole.Make(r.ID, r.Handle)) - - case authenticated[r.Handle]: - rr = append(rr, rbac.AuthenticatedRole.Make(r.ID, r.Handle)) - - case r.Meta != nil && len(r.Meta.ContextExpr) > 0: - log := log.With(zap.String("expr", r.Meta.ContextExpr)) - eval, err := p.Parse(r.Meta.ContextExpr) - if err != nil { - log.Error("failed to parse role context expression", zap.Error(err)) - continue - } - - check := func(s map[string]interface{}) bool { - vars, err := expr.NewVars(s) - if err != nil { - log.Error("failed to convert check scope to expr.Vars", 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)) - return false - } - - return test - } - - rr = append(rr, rbac.MakeContextRole(r.ID, r.Handle, check)) - - default: - rr = append(rr, rbac.CommonRole.Make(r.ID, r.Handle)) - - } - } - - log.Info("role changed " + ev.EventType()) - rbac.Global().UpdateRoles(rr...) - - return nil - }, - eventbus.For("system:role"), - eventbus.On("afterUpdate", "afterCreate", "afterDelete"), - ) - - return nil -} - // isGeneric returns true if given error is generic func isGeneric(err error) bool { g, ok := err.(interface{ IsGeneric() bool }) diff --git a/system/types/role.go b/system/types/role.go index 005632a98..656a27266 100644 --- a/system/types/role.go +++ b/system/types/role.go @@ -4,8 +4,9 @@ import ( "database/sql/driver" "encoding/json" "fmt" - "github.com/cortezaproject/corteza-server/pkg/filter" "time" + + "github.com/cortezaproject/corteza-server/pkg/filter" ) type ( @@ -13,8 +14,8 @@ type ( ID uint64 `json:"roleID,string"` Name string `json:"name"` Handle string `json:"handle"` - Meta *RoleMeta + Meta *RoleMeta `json:"meta"` Labels map[string]string `json:"labels,omitempty"` CreatedAt time.Time `json:"createdAt,omitempty"` @@ -24,8 +25,13 @@ type ( } RoleMeta struct { - Description string - ContextExpr string + Description string `json:"description,omitempty"` + Context *RoleContext `json:"context,omitempty"` + } + + RoleContext struct { + Resource []string `json:"resource,omitempty"` + Expr string `json:"expr,omitempty"` } RoleFilter struct { diff --git a/tests/automation/main_test.go b/tests/automation/main_test.go index b8a8e9151..53b8b6284 100644 --- a/tests/automation/main_test.go +++ b/tests/automation/main_test.go @@ -3,6 +3,9 @@ package automation import ( "context" "errors" + "os" + "testing" + "github.com/cortezaproject/corteza-server/app" "github.com/cortezaproject/corteza-server/automation/rest" "github.com/cortezaproject/corteza-server/automation/service" @@ -26,8 +29,6 @@ import ( "github.com/steinfletcher/apitest" "github.com/stretchr/testify/require" "go.uber.org/zap" - "os" - "testing" ) type ( @@ -137,7 +138,7 @@ func (h helper) mockPermissions(rules ...*rbac.Rule) { func (h helper) mockPermissionsWithAccess(rules ...*rbac.Rule) { rules = append( rules, - rbac.AllowRule(rbac.EveryoneRoleID, types.ComponentRbacResource(), "access"), + rbac.AllowRule(1, types.ComponentRbacResource(), "access"), ) h.mockPermissions(rules...) diff --git a/tests/envoy/testdata/base/access_control_base.yaml b/tests/envoy/testdata/base/access_control_base.yaml index 761916b99..6507d2774 100644 --- a/tests/envoy/testdata/base/access_control_base.yaml +++ b/tests/envoy/testdata/base/access_control_base.yaml @@ -1,9 +1,9 @@ allow: - everyone: + authenticated: compose: - op1 deny: - everyone: + authenticated: compose: - op2 @@ -12,10 +12,10 @@ namespaces: name: ns1 name allow: - everyone: + authenticated: - op1 deny: - everyone: + authenticated: - op2 modules: @@ -23,10 +23,10 @@ modules: name: mod1 name allow: - everyone: + authenticated: - op1 deny: - everyone: + authenticated: - op2 pages: @@ -34,10 +34,10 @@ pages: title: pg1 title allow: - everyone: + authenticated: - op1 deny: - everyone: + authenticated: - op2 charts: @@ -45,39 +45,39 @@ charts: name: chr1 name allow: - everyone: + authenticated: - op1 deny: - everyone: + authenticated: - op2 applications: - name: app1 allow: - everyone: + authenticated: - op1 deny: - everyone: + authenticated: - op2 channels: - name: ch1 type: public allow: - everyone: + authenticated: - op1 deny: - everyone: + authenticated: - op2 roles: - everyone: + authenticated: name: everyone name allow: - everyone: + authenticated: - op1 deny: - everyone: + authenticated: - op2 users: @@ -85,8 +85,8 @@ users: name: u1 name email: u1@example.tld allow: - everyone: + authenticated: - op1 deny: - everyone: + authenticated: - op2 diff --git a/tests/envoy/testdata/provision/simple/0000_access_control.yaml b/tests/envoy/testdata/provision/simple/0000_access_control.yaml index 80b4c0b9e..5af1845b4 100644 --- a/tests/envoy/testdata/provision/simple/0000_access_control.yaml +++ b/tests/envoy/testdata/provision/simple/0000_access_control.yaml @@ -1,5 +1,5 @@ allow: - everyone: + authenticated: compose: - op1 @@ -16,7 +16,7 @@ allow: - op1.sub deny: - everyone: + authenticated: compose: - op2 diff --git a/tests/envoy/testdata/provision/simple/1000_namespace.yaml b/tests/envoy/testdata/provision/simple/1000_namespace.yaml index 5ab104d1f..c19b5ab54 100644 --- a/tests/envoy/testdata/provision/simple/1000_namespace.yaml +++ b/tests/envoy/testdata/provision/simple/1000_namespace.yaml @@ -3,8 +3,8 @@ namespaces: name: ns1 name allow: - everyone: + authenticated: - op1 deny: - everyone: + authenticated: - op2 diff --git a/tests/messagebus/main_test.go b/tests/messagebus/main_test.go index 2346d890c..1fc6f5880 100644 --- a/tests/messagebus/main_test.go +++ b/tests/messagebus/main_test.go @@ -121,7 +121,7 @@ func (h helper) mockPermissions(rules ...*rbac.Rule) { func (h helper) mockPermissionsWithAccess(rules ...*rbac.Rule) { rules = append( rules, - rbac.AllowRule(rbac.EveryoneRoleID, types.ComponentRbacResource(), "access"), + rbac.AllowRule(1, types.ComponentRbacResource(), "access"), ) h.mockPermissions(rules...)