snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
This commit is contained in:
531
server/pkg/rbac/service.go
Normal file
531
server/pkg/rbac/service.go
Normal file
@@ -0,0 +1,531 @@
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/id"
|
||||
"github.com/cortezaproject/corteza/server/pkg/sentry"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type (
|
||||
// UserResolver is a callback that loads user properties by ID.
|
||||
// Used during contextual role expression evaluation to populate
|
||||
// the "user" variable in expression scope.
|
||||
UserResolver func(userID uint64) map[string]interface{}
|
||||
|
||||
service struct {
|
||||
l *sync.RWMutex
|
||||
logger *zap.Logger
|
||||
|
||||
// service will flush values on TRUE or just reload on FALSE
|
||||
f chan bool
|
||||
|
||||
rules RuleSet
|
||||
indexed *ruleIndex
|
||||
|
||||
roles []*Role
|
||||
|
||||
orgTree *orgTree
|
||||
|
||||
store rbacRulesStore
|
||||
|
||||
userResolver UserResolver
|
||||
userCache userCache
|
||||
}
|
||||
|
||||
// RuleFilter is a dummy struct to satisfy store codegen
|
||||
RuleFilter struct {
|
||||
Limit uint
|
||||
}
|
||||
|
||||
RoleSettings struct {
|
||||
Bypass []uint64
|
||||
Authenticated []uint64
|
||||
Anonymous []uint64
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
// Global RBAC service
|
||||
gRBAC *service
|
||||
)
|
||||
|
||||
const (
|
||||
watchInterval = time.Hour
|
||||
debugWatchInterval = time.Minute * 5
|
||||
|
||||
RuleResourceType = "corteza::generic:rbac-rule"
|
||||
)
|
||||
|
||||
// Global returns global RBAC service
|
||||
func Global() *service {
|
||||
return gRBAC
|
||||
}
|
||||
|
||||
// SetGlobal re-sets global service
|
||||
func SetGlobal(svc *service) {
|
||||
gRBAC = svc
|
||||
}
|
||||
|
||||
// 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.RWMutex{},
|
||||
f: make(chan bool),
|
||||
|
||||
store: s,
|
||||
userCache: newUserCache(),
|
||||
}
|
||||
|
||||
if logger != nil {
|
||||
svc.logger = logger.Named("rbac")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// @todo consider a more graceful update
|
||||
func (svc *service) UpdateUserGroups(gm ...GroupMembers) (err error) {
|
||||
svc.orgTree, err = OrgTree(
|
||||
svc.logger.Named("org tree"),
|
||||
gm...,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// SetUserResolver registers a callback that resolves user properties by user ID.
|
||||
// The resolved user data is injected into the expression scope as "user" when
|
||||
// evaluating contextual role expressions.
|
||||
func (svc *service) SetUserResolver(fn UserResolver) {
|
||||
svc.userResolver = fn
|
||||
}
|
||||
|
||||
// InvalidateUser removes a single user entry from the resolver cache.
|
||||
func (svc *service) InvalidateUser(userID uint64) {
|
||||
svc.userCache.delete(userID)
|
||||
}
|
||||
|
||||
// InvalidateAllUsers clears the entire user resolver cache.
|
||||
func (svc *service) InvalidateAllUsers() {
|
||||
svc.userCache.purge()
|
||||
}
|
||||
|
||||
// cachedUserResolver wraps userResolver with the userPropertyCache.
|
||||
// Returns nil when no resolver is configured.
|
||||
func (svc *service) cachedUserResolver() UserResolver {
|
||||
if svc.userResolver == nil {
|
||||
return nil
|
||||
}
|
||||
return func(userID uint64) map[string]interface{} {
|
||||
if v, ok := svc.userCache.load(userID); ok {
|
||||
return v
|
||||
}
|
||||
result := svc.userResolver(userID)
|
||||
svc.userCache.store(userID, result)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// Can function performs permission check for roles in context
|
||||
//
|
||||
// First extracts roles from context, then
|
||||
// use Check() to test against permission rules and
|
||||
// iterate over all fallback functions
|
||||
//
|
||||
// System user is always allowed to do everything
|
||||
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) (userAccess Access) {
|
||||
if !svc.checkValidity(ses, op, res) {
|
||||
return Inherit
|
||||
}
|
||||
|
||||
sesRoles := getSessionRoles(ses, res, svc.roles, svc.cachedUserResolver())
|
||||
|
||||
// @todo probably move this somewhere
|
||||
if member(sesRoles, BypassRole) {
|
||||
return Allow
|
||||
}
|
||||
|
||||
treeAccess := svc.checkMemberBranch(ses, op, res)
|
||||
userAccess = check(svc.indexed, sesRoles, op, res.RbacResource(), nil)
|
||||
|
||||
out := svc.mergeAccess(treeAccess, userAccess)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (svc *service) checkValidity(ses Session, op string, res Resource) bool {
|
||||
// prevent use of wildcard resources for checking permissions
|
||||
if hasWildcards(res.RbacResource()) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (svc *service) checkMemberBranch(ses Session, op string, res Resource) Access {
|
||||
mm, err := svc.orgTree.MemberBranch(id.MustNumID(ses.Identity()))
|
||||
if err != nil {
|
||||
return Deny
|
||||
}
|
||||
|
||||
return svc.evalSubBranch(mm, op, res.RbacResource())
|
||||
}
|
||||
|
||||
// @note being ery explicit with branches for the sake of clarity
|
||||
func (svc *service) mergeAccess(treeAccess, userAccess Access) Access {
|
||||
if userAccess == Deny {
|
||||
return Deny
|
||||
}
|
||||
|
||||
if userAccess == Allow {
|
||||
return Allow
|
||||
}
|
||||
|
||||
// @note userAccess will always be Inherit here; adding a little guard
|
||||
// just in case we ever expand this
|
||||
if userAccess != Inherit {
|
||||
panic("new edge case; to address")
|
||||
}
|
||||
|
||||
if treeAccess == Allow {
|
||||
return Allow
|
||||
}
|
||||
|
||||
if treeAccess == Deny {
|
||||
return Deny
|
||||
}
|
||||
|
||||
return Inherit
|
||||
}
|
||||
|
||||
func (svc *service) evalSubBranch(nn []*groupNode, op, res string) (a Access) {
|
||||
aux := make([]Access, len(nn))
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(nn))
|
||||
|
||||
for i, n := range nn {
|
||||
go func() {
|
||||
aux[i] = svc.evalNode(n, op, res)
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
for _, x := range aux {
|
||||
if x == Allow {
|
||||
return Allow
|
||||
}
|
||||
}
|
||||
|
||||
return Inherit
|
||||
}
|
||||
|
||||
func (svc *service) evalNode(n *groupNode, op, res string) (a Access) {
|
||||
thingy := make(map[uint64]bool, len(n.roles))
|
||||
for _, r := range n.roles {
|
||||
thingy[r.Number()] = true
|
||||
}
|
||||
|
||||
return check(svc.indexed, partRoles{
|
||||
CommonRole: thingy,
|
||||
}, op, res, nil)
|
||||
}
|
||||
|
||||
// func (svc *service) grabCtxRoles(ses Session, res Resource)
|
||||
|
||||
// Trace checks RBAC rules and returns all decision trace log
|
||||
func (svc *service) Trace(ses Session, op string, res Resource) *Trace {
|
||||
var (
|
||||
t = new(Trace)
|
||||
)
|
||||
|
||||
if hasWildcards(res.RbacResource()) {
|
||||
// a special case for when user has contextual roles
|
||||
// AND trace is done on a resource with wildcards
|
||||
ctxRolesDebug := partRoles{ContextRole: make(map[uint64]bool)}
|
||||
for _, memberOf := range ses.Roles() {
|
||||
for _, role := range svc.roles {
|
||||
if role.kind != ContextRole {
|
||||
continue
|
||||
}
|
||||
|
||||
if role.id != memberOf {
|
||||
continue
|
||||
}
|
||||
|
||||
// member of contextual role
|
||||
//
|
||||
// this is a tricky situation:
|
||||
// when doing regular check this is an unlikely scenario since
|
||||
// check can not be done on a resource with wildcards
|
||||
//
|
||||
// all contextual roles we're members off will be collected
|
||||
ctxRolesDebug[ContextRole][memberOf] = true
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if len(ctxRolesDebug[ContextRole]) > 0 {
|
||||
// session has at least one contextual role
|
||||
// and since we're checking this on a wildcard resource
|
||||
// there is no need to procede with RBAC check
|
||||
baseTraceInfo(t, res.RbacResource(), op, ctxRolesDebug)
|
||||
resolve(t, Inherit, unknownContext)
|
||||
return t
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
fRoles = getSessionRoles(ses, res, svc.roles, svc.cachedUserResolver())
|
||||
)
|
||||
|
||||
_ = check(svc.indexed, fRoles, op, res.RbacResource(), t)
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// Grant appends and/or overwrites internal rules slice
|
||||
//
|
||||
// All rules with Inherit are removed
|
||||
func (svc *service) Grant(ctx context.Context, rules ...*Rule) (err error) {
|
||||
svc.l.Lock()
|
||||
defer svc.l.Unlock()
|
||||
|
||||
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...)
|
||||
svc.indexed = buildRuleIndex(svc.rules)
|
||||
}
|
||||
|
||||
// Watch reloads RBAC rules in intervals and on request
|
||||
func (svc *service) Watch(ctx context.Context) {
|
||||
go func() {
|
||||
defer sentry.Recover()
|
||||
|
||||
var ticker = time.NewTicker(watchInterval)
|
||||
var debugTicker = time.NewTicker(debugWatchInterval)
|
||||
|
||||
defer ticker.Stop()
|
||||
defer debugTicker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
svc.Reload(ctx)
|
||||
case <-svc.f:
|
||||
svc.Reload(ctx)
|
||||
case <-debugTicker.C:
|
||||
// @todo truncate and bind to a level
|
||||
// svc.LogDebug(ctx)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
svc.logger.Debug("watcher initialized")
|
||||
}
|
||||
|
||||
func (svc *service) AddGroupRole(group id.ID, roles ...id.ID) (err error) {
|
||||
return svc.orgTree.AddGroupRole(group, roles...)
|
||||
}
|
||||
|
||||
func (svc *service) RemoveGroupRole(group id.ID, roles ...id.ID) (err error) {
|
||||
return svc.orgTree.RemoveGroupRole(group, roles...)
|
||||
}
|
||||
|
||||
func (svc *service) AssignGroupMembers(group id.ID, members ...id.ID) (err error) {
|
||||
return svc.orgTree.AssignGroupMembers(group, members...)
|
||||
}
|
||||
|
||||
func (svc *service) RemoveGroupMembers(group id.ID, members ...id.ID) (err error) {
|
||||
return svc.orgTree.RemoveGroupMembers(group, members...)
|
||||
}
|
||||
|
||||
func (svc *service) AddNode(id id.ID, handle string, paths ...GroupNodePath) (err error) {
|
||||
return svc.orgTree.AddNode(id, handle, paths...)
|
||||
}
|
||||
|
||||
func (svc *service) UpdateNode(id id.ID, handle string, paths ...GroupNodePath) (err error) {
|
||||
return svc.orgTree.UpdateNode(id, handle, paths...)
|
||||
}
|
||||
func (svc *service) RemoveNode(id id.ID) (err error) {
|
||||
return svc.orgTree.RemoveNode(id)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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 = &ruleIndex{}
|
||||
}
|
||||
|
||||
func (svc *service) reloadRules(ctx context.Context) {
|
||||
rr, _, err := svc.store.SearchRbacRules(ctx, RuleFilter{})
|
||||
svc.logger.Debug(
|
||||
"reloading rules",
|
||||
zap.Error(err),
|
||||
zap.Int("before", len(svc.rules)),
|
||||
zap.Int("after", len(rr)),
|
||||
)
|
||||
|
||||
if err == nil {
|
||||
svc.setRules(rr)
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *service) setRules(rr RuleSet) {
|
||||
svc.rules = rr
|
||||
svc.indexed = buildRuleIndex(rr)
|
||||
}
|
||||
|
||||
// UpdateRoles updates RBAC roles
|
||||
//
|
||||
// Warning: this REPLACES all existing roles that are recognized by RBAC subsystem
|
||||
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.roles)),
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
deletable, updatable, final := flushable(svc.rules)
|
||||
|
||||
err = svc.store.DeleteRbacRule(ctx, deletable...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = svc.store.UpsertRbacRule(ctx, updatable...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// SignificantRoles returns two list of significant roles.
|
||||
//
|
||||
// See sigRoles on rules for more details
|
||||
func (svc *service) SignificantRoles(res Resource, op string) (aRR, dRR []uint64) {
|
||||
svc.l.Lock()
|
||||
defer svc.l.Unlock()
|
||||
|
||||
return svc.rules.sigRoles(res.RbacResource(), op)
|
||||
}
|
||||
|
||||
// CloneRulesByRoleID clone all rules of a Role S to a specific Role T by removing its existing rules
|
||||
func (svc *service) CloneRulesByRoleID(ctx context.Context, fromRoleID uint64, toRoleID ...uint64) (err error) {
|
||||
var (
|
||||
updatedRules RuleSet
|
||||
)
|
||||
|
||||
// Make sure rules of fromRoleID stays intact
|
||||
rr := svc.FindRulesByRoleID(fromRoleID)
|
||||
|
||||
for _, roleID := range toRoleID {
|
||||
// Remove existing rules
|
||||
existingRules := svc.FindRulesByRoleID(roleID)
|
||||
for _, rule := range existingRules {
|
||||
// Make sure to remove existing rules
|
||||
rule.Access = Inherit
|
||||
}
|
||||
updatedRules = append(updatedRules, existingRules...)
|
||||
|
||||
// Clone rules from role S to role T
|
||||
for _, rule := range rr {
|
||||
// Make sure everything is properly set
|
||||
r := *rule
|
||||
r.RoleID = roleID
|
||||
updatedRules = append(updatedRules, &r)
|
||||
}
|
||||
}
|
||||
|
||||
return svc.Grant(ctx, updatedRules...)
|
||||
}
|
||||
|
||||
func (svc *service) LogDebug(ctx context.Context) {
|
||||
nodes := make([]map[string]any, 0, 4)
|
||||
for _, n := range svc.orgTree.root.inline() {
|
||||
nodes = append(nodes, n.format())
|
||||
}
|
||||
|
||||
children := make(map[string]string, 4)
|
||||
for u, g := range svc.orgTree.memberGroupIndex {
|
||||
children[u.Value()] = g.id.Value()
|
||||
}
|
||||
|
||||
svc.logger.Debug("internal org tree state", zap.Any("nodes", nodes), zap.Any("role membership", children))
|
||||
}
|
||||
Reference in New Issue
Block a user