Files
upstream-corteza/server/pkg/rbac/service.go
upstream-mirror 7e385fc126
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
snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
2026-09-18 01:22:23 +05:00

532 lines
12 KiB
Go

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))
}