package rbac import ( "context" "strconv" "sync" "time" "github.com/cortezaproject/corteza/server/pkg/sentry" "go.uber.org/zap" ) type ( 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 store rbacRulesStore } // RuleFilter is a dummy struct to satisfy store codegen RuleFilter struct { Resource []string Operation string RoleID uint64 Limit uint } RoleSettings struct { Bypass []uint64 Authenticated []uint64 Anonymous []uint64 } ) var ( // Global RBAC service gRBAC *service ) const ( watchInterval = time.Hour 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, } if logger != nil { svc.logger = logger.Named("rbac") } return } // 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) (a Access) { var ( fRoles = getContextRoles(ses, res, svc.roles...) ) if hasWildcards(res.RbacResource()) { // prevent use of wildcard resources for checking permissions return Inherit } a = check(svc.indexed, fRoles, op, res.RbacResource(), nil) return } // 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 = getContextRoles(ses, res, svc.roles...) ) _ = 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) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: svc.Reload(ctx) case <-svc.f: svc.Reload(ctx) } } }() svc.logger.Debug("watcher initialized") } // 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...) }