Role migration
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 }},
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@ namespace: crm
|
||||
charts:
|
||||
ChartHandle:
|
||||
allow:
|
||||
everyone:
|
||||
authenticated:
|
||||
- read
|
||||
deny:
|
||||
everyone:
|
||||
authenticated:
|
||||
- delete
|
||||
|
||||
+2
-2
@@ -3,8 +3,8 @@ modules:
|
||||
Account:
|
||||
name: Account
|
||||
allow:
|
||||
everyone:
|
||||
authenticated:
|
||||
- read
|
||||
deny:
|
||||
everyone:
|
||||
authenticated:
|
||||
- delete
|
||||
|
||||
+2
-2
@@ -2,9 +2,9 @@ namespaces:
|
||||
crm:
|
||||
name: CRM
|
||||
allow:
|
||||
everyone:
|
||||
authenticated:
|
||||
- module.create
|
||||
- read
|
||||
deny:
|
||||
everyone:
|
||||
authenticated:
|
||||
- delete
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
allow:
|
||||
everyone:
|
||||
authenticated:
|
||||
compose:
|
||||
- access
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -21,7 +21,7 @@ type (
|
||||
func RBAC() (o *RBACOpt) {
|
||||
o = &RBACOpt{
|
||||
ServiceUser: "corteza",
|
||||
BypassRoles: "superadmin",
|
||||
BypassRoles: "super-admin",
|
||||
AuthenticatedRoles: "authenticated",
|
||||
AnonymousRoles: "anonymous",
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) },
|
||||
|
||||
+119
-16
@@ -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
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
+11
-1
@@ -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 {
|
||||
|
||||
+12
-4
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
allow:
|
||||
everyone:
|
||||
authenticated:
|
||||
compose:
|
||||
- access
|
||||
|
||||
|
||||
@@ -3,5 +3,4 @@ roles:
|
||||
# These are created in provision/roles.go
|
||||
admins:
|
||||
name: Administrators
|
||||
everyone:
|
||||
name: Everyone
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
allow:
|
||||
everyone:
|
||||
authenticated:
|
||||
system:user:
|
||||
- read
|
||||
- unmask.email
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
allow:
|
||||
everyone:
|
||||
authenticated:
|
||||
system:auth-client:
|
||||
- authorize
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ namespaces:
|
||||
(envoy): { onExisting: "skip" }
|
||||
name: CRM
|
||||
allow:
|
||||
everyone:
|
||||
authenticated:
|
||||
- module.create
|
||||
- read
|
||||
deny:
|
||||
everyone:
|
||||
authenticated:
|
||||
- delete
|
||||
|
||||
Generated
+9
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
Generated
+9
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
+185
-1
@@ -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
|
||||
}
|
||||
|
||||
+5
-150
@@ -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 })
|
||||
|
||||
+10
-4
@@ -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 {
|
||||
|
||||
@@ -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...)
|
||||
|
||||
+19
-19
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
allow:
|
||||
everyone:
|
||||
authenticated:
|
||||
compose:
|
||||
- op1
|
||||
|
||||
@@ -16,7 +16,7 @@ allow:
|
||||
- op1.sub
|
||||
|
||||
deny:
|
||||
everyone:
|
||||
authenticated:
|
||||
compose:
|
||||
- op2
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ namespaces:
|
||||
name: ns1 name
|
||||
|
||||
allow:
|
||||
everyone:
|
||||
authenticated:
|
||||
- op1
|
||||
deny:
|
||||
everyone:
|
||||
authenticated:
|
||||
- op2
|
||||
|
||||
@@ -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...)
|
||||
|
||||
Reference in New Issue
Block a user