// Copyright IBM Corp. 2020, 2026 // SPDX-License-Identifier: BUSL-1.1 package iam import ( "context" "repo.anhonesthost.net/CyberCoveLLC/threshold/globals" "repo.anhonesthost.net/CyberCoveLLC/threshold/internal/db" "repo.anhonesthost.net/CyberCoveLLC/threshold/internal/db/timestamp" "repo.anhonesthost.net/CyberCoveLLC/threshold/internal/errors" "repo.anhonesthost.net/CyberCoveLLC/threshold/internal/iam/store" "repo.anhonesthost.net/CyberCoveLLC/threshold/internal/oplog" "repo.anhonesthost.net/CyberCoveLLC/threshold/internal/types/action" "repo.anhonesthost.net/CyberCoveLLC/threshold/internal/types/resource" "google.golang.org/protobuf/proto" ) const ( defaultGlobalRoleTableName = "iam_role_global" defaultOrgRoleTableName = "iam_role_org" defaultProjectRoleTableName = "iam_role_project" ) // roleGrantScopeUpdater represents an internal scope type specific role structs that // support grant scope columns. Currently this only applies to globalRole and orgRole // this is used in SetRoleGrantScope, AddRoleGrantScope, DeleteRoleGrantScope type roleGrantScopeUpdater interface { // setVersion sets value of `Version` of this role. This is used in // `repository_grant_scope` operations where version column of the associated role // has to increase when grant scopes list is changed (add/remove) which is done in a different table. // This version bump cannot be done by automatically with trigger and is being handled in the application code setVersion(version uint32) // setThisGrantScope sets value of `GrantThisRoleScope` of this role which control // whether this role has 'this' scope granted to it setGrantThisRoleScope(grantThis bool) // setGrantScope sets value of `GrantScope` column of this role. The allowed values depends on the scope // that the role is in // - global-role: ['descendants', 'children'] // - org-role: ['children'] // - project-role: [] (None) // This value controls whether hierarchical grant scope is granted to this role // This method may return error when role does not support hierarchical grant scope (project role) setGrantScope(ctx context.Context, specialGrant string) error // removeHierarchicalGrantScope removes all hierarchical grant scopes from a role ['children', 'descendants'] // and sets 'grant_scope' column to 'individual' if required // - global-role: may remove ['descendants', 'children'] // - org-role: may remove ['children'] // - project-role: no-op as hierarchical grant scope isn't supported for project roles removeGrantScope() // GrantThisRoleScope return value of `GrantScopeThis` column as *RoleGrantScope. // Prior to the grants refactor, `this` grant scope is granted to a role by // inserting a row to 'role_grant_scope' table, but we've moved on to storing // 'this' grant as a dedicated column in the type-specific role tables grantThisRoleScope() (*RoleGrantScope, bool) // grantScope returns hierarchical grant scopes ['descendants', 'children'] if available. // returns nil, false if grant_scope is 'individual' grantScope() (*RoleGrantScope, bool) } // Roles are granted permissions and assignable to Users and Groups. type Role struct { PublicId string ScopeId string Name string Description string CreateTime *timestamp.Timestamp UpdateTime *timestamp.Timestamp Version uint32 GrantScopes []*RoleGrantScope `gorm:"-"` } func (role *Role) GetPublicId() string { if role == nil { return "" } return role.PublicId } func (role *Role) GetScopeId() string { if role == nil { return "" } return role.ScopeId } func (role *Role) GetName() string { if role == nil { return "" } return role.Name } func (role *Role) GetDescription() string { if role == nil { return "" } return role.Description } func (role *Role) GetCreateTime() *timestamp.Timestamp { if role == nil { return nil } return role.CreateTime } func (role *Role) GetUpdateTime() *timestamp.Timestamp { if role == nil { return nil } return role.UpdateTime } func (role *Role) GetVersion() uint32 { if role == nil { return 0 } return role.Version } // ensure that Role implements the interfaces of: Resource, Cloneable, and db.VetForWriter. var ( _ roleGrantScopeUpdater = (*globalRole)(nil) _ Resource = (*globalRole)(nil) _ Cloneable = (*globalRole)(nil) _ db.VetForWriter = (*globalRole)(nil) _ oplog.ReplayableMessage = (*globalRole)(nil) _ roleGrantScopeUpdater = (*orgRole)(nil) _ Resource = (*orgRole)(nil) _ Cloneable = (*orgRole)(nil) _ db.VetForWriter = (*orgRole)(nil) _ oplog.ReplayableMessage = (*orgRole)(nil) _ roleGrantScopeUpdater = (*projectRole)(nil) _ Resource = (*projectRole)(nil) _ Cloneable = (*projectRole)(nil) _ db.VetForWriter = (*projectRole)(nil) _ oplog.ReplayableMessage = (*projectRole)(nil) ) // NewRole creates a new in memory role with a scope (project/org) // allowed options include: withDescription, WithName. func NewRole(ctx context.Context, scopeId string, opt ...Option) (*Role, error) { const op = "iam.NewRole" if scopeId == "" { return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope id") } opts := getOpts(opt...) r := &Role{ ScopeId: scopeId, Name: opts.withName, Description: opts.withDescription, } return r, nil } // VetForWrite implements db.VetForWrite() interface. func (role *Role) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { const op = "iam.(Role).VetForWrite" if role.PublicId == "" { return errors.New(ctx, errors.InvalidParameter, op, "missing public id") } if err := validateScopeForWrite(ctx, r, role, opType, opt...); err != nil { return errors.Wrap(ctx, err, op) } return nil } func (role *Role) getResourceType() resource.Type { return resource.Role } // GetScope returns the scope for the Role. func (role *Role) GetScope(ctx context.Context, r db.Reader) (*Scope, error) { return LookupScope(ctx, r, role) } // GetResourceType returns the type of the Role. func (*Role) GetResourceType() resource.Type { return resource.Role } // Actions returns the available actions for Role. func (*Role) Actions() map[string]action.Type { ret := CrudlActions() ret[action.AddGrants.String()] = action.AddGrants ret[action.RemoveGrants.String()] = action.RemoveGrants ret[action.SetGrants.String()] = action.SetGrants ret[action.AddPrincipals.String()] = action.AddPrincipals ret[action.RemovePrincipals.String()] = action.RemovePrincipals ret[action.SetPrincipals.String()] = action.SetPrincipals return ret } type deletedRole struct { PublicId string `gorm:"primary_key"` DeleteTime *timestamp.Timestamp } // TableName returns the tablename to override the default gorm table name func (s *deletedRole) TableName() string { return "iam_role_deleted" } // globalRole is a type embedding store.GlobalRole used to interact with iam_role_global table which contains // all iam_role entries that are created in global-level scopes through gorm. type globalRole struct { *store.GlobalRole GrantScopes []*RoleGrantScope `gorm:"-"` tableName string `gorm:"-"` } func (g *globalRole) removeGrantScope() { if g != nil { g.GrantScope = globals.GrantScopeIndividual } } func (g *globalRole) setVersion(version uint32) { if g == nil { return } g.Version = version } func (g *globalRole) setGrantThisRoleScope(grantThis bool) { if g == nil { return } g.GrantThisRoleScope = grantThis } func (g *globalRole) setGrantScope(ctx context.Context, specialGrant string) error { if g != nil { g.GrantScope = specialGrant } return nil } func (g *globalRole) grantThisRoleScope() (*RoleGrantScope, bool) { if g == nil { return &RoleGrantScope{}, false } if !g.GrantThisRoleScope { return &RoleGrantScope{}, false } return &RoleGrantScope{ CreateTime: g.GrantThisRoleScopeUpdateTime, RoleId: g.PublicId, ScopeIdOrSpecial: globals.GrantScopeThis, }, true } func (g *globalRole) grantScope() (*RoleGrantScope, bool) { if g == nil { return nil, false } if g.GrantScope == globals.GrantScopeIndividual { return nil, false } return &RoleGrantScope{ CreateTime: g.GrantScopeUpdateTime, RoleId: g.PublicId, ScopeIdOrSpecial: g.GrantScope, }, true } func (g *globalRole) TableName() string { if g.tableName != "" { return g.tableName } return defaultGlobalRoleTableName } func (g *globalRole) SetTableName(n string) { g.tableName = n } func (g *globalRole) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { const op = "iam.(globalRole).VetForWrite" if g.PublicId == "" { return errors.New(ctx, errors.InvalidParameter, op, "missing public id") } if err := validateScopeForWrite(ctx, r, g, opType, opt...); err != nil { return errors.Wrap(ctx, err, op) } return nil } func allocGlobalRole() globalRole { return globalRole{ GlobalRole: &store.GlobalRole{}, } } func (g *globalRole) Clone() any { cp := proto.Clone(g.GlobalRole) ret := &globalRole{ GlobalRole: cp.(*store.GlobalRole), } for _, grantScope := range g.GrantScopes { ret.GrantScopes = append(ret.GrantScopes, grantScope.Clone().(*RoleGrantScope)) } return ret } func (g *globalRole) GetScope(ctx context.Context, r db.Reader) (*Scope, error) { return LookupScope(ctx, r, g) } func (g *globalRole) GetResourceType() resource.Type { return resource.Role } func (g *globalRole) getResourceType() resource.Type { return resource.Role } func (g *globalRole) Actions() map[string]action.Type { ret := CrudlActions() ret[action.AddGrants.String()] = action.AddGrants ret[action.RemoveGrants.String()] = action.RemoveGrants ret[action.SetGrants.String()] = action.SetGrants ret[action.AddPrincipals.String()] = action.AddPrincipals ret[action.RemovePrincipals.String()] = action.RemovePrincipals ret[action.SetPrincipals.String()] = action.SetPrincipals return ret } func (g *globalRole) toRole() *Role { if g == nil { return nil } ret := &Role{ PublicId: g.GetPublicId(), ScopeId: g.GetScopeId(), Name: g.GetName(), Description: g.GetDescription(), CreateTime: g.GetCreateTime(), UpdateTime: g.GetUpdateTime(), Version: g.GetVersion(), } for _, grantScope := range g.GrantScopes { ret.GrantScopes = append(ret.GrantScopes, grantScope.Clone().(*RoleGrantScope)) } return ret } // orgRole is a type embedding store.OrgRole used to interact with iam_role_org table which contains // all iam_role entries that are created in org-level scopes through gorm. type orgRole struct { *store.OrgRole GrantScopes []*RoleGrantScope `gorm:"-"` tableName string `gorm:"-"` } func (o *orgRole) removeGrantScope() { if o != nil { o.GrantScope = globals.GrantScopeIndividual } } func (o *orgRole) setVersion(version uint32) { if o == nil { return } o.Version = version } func (o *orgRole) setGrantThisRoleScope(grantThis bool) { if o == nil { return } o.GrantThisRoleScope = grantThis } func (o *orgRole) setGrantScope(ctx context.Context, specialGrant string) error { if o != nil { o.GrantScope = specialGrant } return nil } func (o *orgRole) grantThisRoleScope() (*RoleGrantScope, bool) { if o == nil { return &RoleGrantScope{}, false } if !o.GrantThisRoleScope { return &RoleGrantScope{}, false } return &RoleGrantScope{ CreateTime: o.GrantThisRoleScopeUpdateTime, RoleId: o.PublicId, ScopeIdOrSpecial: globals.GrantScopeThis, }, true } func (o *orgRole) grantScope() (*RoleGrantScope, bool) { if o == nil { return nil, false } if o.GrantScope == globals.GrantScopeIndividual { return nil, false } return &RoleGrantScope{ CreateTime: o.GrantScopeUpdateTime, RoleId: o.PublicId, ScopeIdOrSpecial: o.GrantScope, }, true } func (o *orgRole) TableName() string { if o.tableName != "" { return o.tableName } return defaultOrgRoleTableName } func (o *orgRole) SetTableName(n string) { o.tableName = n } func (o *orgRole) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { const op = "iam.(orgRole).VetForWrite" if o.PublicId == "" { return errors.New(ctx, errors.InvalidParameter, op, "missing public id") } if err := validateScopeForWrite(ctx, r, o, opType, opt...); err != nil { return errors.Wrap(ctx, err, op) } return nil } func allocOrgRole() orgRole { return orgRole{ OrgRole: &store.OrgRole{}, } } func (o *orgRole) Clone() any { cp := proto.Clone(o.OrgRole) ret := &orgRole{ OrgRole: cp.(*store.OrgRole), } for _, grantScope := range o.GrantScopes { ret.GrantScopes = append(ret.GrantScopes, grantScope.Clone().(*RoleGrantScope)) } return ret } func (o *orgRole) GetScope(ctx context.Context, r db.Reader) (*Scope, error) { return LookupScope(ctx, r, o) } func (o *orgRole) GetResourceType() resource.Type { return resource.Role } func (o *orgRole) getResourceType() resource.Type { return resource.Role } func (o *orgRole) Actions() map[string]action.Type { ret := CrudlActions() ret[action.AddGrants.String()] = action.AddGrants ret[action.RemoveGrants.String()] = action.RemoveGrants ret[action.SetGrants.String()] = action.SetGrants ret[action.AddPrincipals.String()] = action.AddPrincipals ret[action.RemovePrincipals.String()] = action.RemovePrincipals ret[action.SetPrincipals.String()] = action.SetPrincipals return ret } func (o *orgRole) toRole() *Role { if o == nil { return nil } ret := &Role{ PublicId: o.GetPublicId(), ScopeId: o.GetScopeId(), Name: o.GetName(), Description: o.GetDescription(), CreateTime: o.GetCreateTime(), UpdateTime: o.GetUpdateTime(), Version: o.GetVersion(), } for _, grantScope := range o.GrantScopes { ret.GrantScopes = append(ret.GrantScopes, grantScope.Clone().(*RoleGrantScope)) } return ret } // projectRole is a type embedding store.ProjectRole used to interact with iam_role_project table which contains // all iam_role entries that are created in project-level scopes through gorm. type projectRole struct { *store.ProjectRole GrantScopes []*RoleGrantScope `gorm:"-"` tableName string `gorm:"-"` } func (p *projectRole) removeGrantScope() { // no-op since hierarchical isn't supported by project roles return } func (p *projectRole) setGrantScope(ctx context.Context, specialGrant string) error { const op = "iam.(projectRole).setGrantScope" return errors.New(ctx, errors.InvalidParameter, op, "hierarchical grant scope is not allowed for project role") } func (p *projectRole) grantScope() (*RoleGrantScope, bool) { return &RoleGrantScope{}, false } func (p *projectRole) setVersion(version uint32) { if p == nil { return } p.Version = version } func (p *projectRole) setGrantThisRoleScope(grantThis bool) { if p == nil { return } p.GrantThisRoleScope = grantThis } func (p *projectRole) grantThisRoleScope() (*RoleGrantScope, bool) { if p == nil { return &RoleGrantScope{}, false } if !p.GrantThisRoleScope { return &RoleGrantScope{}, false } return &RoleGrantScope{ CreateTime: p.GrantThisRoleScopeUpdateTime, RoleId: p.PublicId, ScopeIdOrSpecial: globals.GrantScopeThis, }, true } func (p *projectRole) TableName() string { if p.tableName != "" { return p.tableName } return defaultProjectRoleTableName } func (p *projectRole) SetTableName(n string) { p.tableName = n } func (p *projectRole) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { const op = "iam.(projectRole).VetForWrite" if p.PublicId == "" { return errors.New(ctx, errors.InvalidParameter, op, "missing public id") } if err := validateScopeForWrite(ctx, r, p, opType, opt...); err != nil { return errors.Wrap(ctx, err, op) } return nil } func allocProjectRole() projectRole { return projectRole{ ProjectRole: &store.ProjectRole{}, } } func (p *projectRole) Clone() any { cp := proto.Clone(p.ProjectRole) ret := &projectRole{ ProjectRole: cp.(*store.ProjectRole), } for _, grantScope := range p.GrantScopes { ret.GrantScopes = append(ret.GrantScopes, grantScope.Clone().(*RoleGrantScope)) } return ret } func (p *projectRole) GetScope(ctx context.Context, r db.Reader) (*Scope, error) { return LookupScope(ctx, r, p) } func (p *projectRole) GetResourceType() resource.Type { return resource.Role } func (p *projectRole) getResourceType() resource.Type { return resource.Role } func (p *projectRole) Actions() map[string]action.Type { ret := CrudlActions() ret[action.AddGrants.String()] = action.AddGrants ret[action.RemoveGrants.String()] = action.RemoveGrants ret[action.SetGrants.String()] = action.SetGrants ret[action.AddPrincipals.String()] = action.AddPrincipals ret[action.RemovePrincipals.String()] = action.RemovePrincipals ret[action.SetPrincipals.String()] = action.SetPrincipals return ret } func (p *projectRole) toRole() *Role { if p == nil { return nil } ret := &Role{ PublicId: p.GetPublicId(), ScopeId: p.GetScopeId(), Name: p.GetName(), Description: p.GetDescription(), CreateTime: p.GetCreateTime(), UpdateTime: p.GetUpdateTime(), Version: p.GetVersion(), } for _, grantScope := range p.GrantScopes { ret.GrantScopes = append(ret.GrantScopes, grantScope.Clone().(*RoleGrantScope)) } return ret }