Documentation
¶
Overview ¶
Package role owns RBAC roles, assignments, permission resolution, and role HTTP routes.
Index ¶
- Constants
- func RegisterRoles(api huma.API, roleService *RoleService)
- type ApiKeyPermission
- type CreateRoleInput
- type CreateRoleOutput
- type DeleteRoleInput
- type DeleteRoleOutput
- type Dependencies
- type GetRoleInput
- type GetRoleOutput
- type ListRolesInput
- type ListRolesOutput
- type ListUserRoleAssignmentsInput
- type ListUserRoleAssignmentsOutput
- type Module
- type OidcRoleMapping
- type PermissionsManifestOutput
- type Role
- type RoleHandler
- func (h *RoleHandler) CreateRole(ctx context.Context, input *CreateRoleInput) (*CreateRoleOutput, error)
- func (h *RoleHandler) DeleteRole(ctx context.Context, input *DeleteRoleInput) (*DeleteRoleOutput, error)
- func (h *RoleHandler) GetPermissionsManifest(_ context.Context, _ *struct{}) (*PermissionsManifestOutput, error)
- func (h *RoleHandler) GetRole(ctx context.Context, input *GetRoleInput) (*GetRoleOutput, error)
- func (h *RoleHandler) ListRoles(ctx context.Context, input *ListRolesInput) (*ListRolesOutput, error)
- func (h *RoleHandler) ListUserRoleAssignments(ctx context.Context, input *ListUserRoleAssignmentsInput) (*ListUserRoleAssignmentsOutput, error)
- func (h *RoleHandler) SetUserRoleAssignments(ctx context.Context, input *SetUserRoleAssignmentsInput) (*SetUserRoleAssignmentsOutput, error)
- func (h *RoleHandler) UpdateRole(ctx context.Context, input *UpdateRoleInput) (*UpdateRoleOutput, error)
- type RoleService
- func (s *RoleService) AssertGlobalAdminExists(ctx context.Context) error
- func (s *RoleService) BackfillLegacyRoleAssignments(ctx context.Context) error
- func (s *RoleService) CountGlobalAdminsExcludingUser(ctx context.Context, excludedUserID string) (int, error)
- func (s *RoleService) CountUsersAssignedToRole(ctx context.Context, roleID string) (int, error)
- func (s *RoleService) CreateOidcMapping(ctx context.Context, claimValue, roleID string, environmentID *string) (*OidcRoleMapping, error)
- func (s *RoleService) CreateRole(ctx context.Context, name string, description *string, permissions []string) (*Role, error)
- func (s *RoleService) DeleteOidcMapping(ctx context.Context, id string) error
- func (s *RoleService) DeleteRole(ctx context.Context, id string) error
- func (s *RoleService) EnsureBuiltInRoles(ctx context.Context) error
- func (s *RoleService) GetOidcMapping(ctx context.Context, id string) (*OidcRoleMapping, error)
- func (s *RoleService) GetRole(ctx context.Context, id string) (*Role, error)
- func (s *RoleService) InvalidateApiKey(apiKeyID string)
- func (s *RoleService) InvalidateUser(userID string)
- func (s *RoleService) ListAllRoles(ctx context.Context) ([]Role, error)
- func (s *RoleService) ListOidcMappings(ctx context.Context) ([]OidcRoleMapping, error)
- func (s *RoleService) ListRoles(ctx context.Context, params pagination.QueryParams) ([]Role, pagination.Response, error)
- func (s *RoleService) ListUserAssignments(ctx context.Context, userID string) ([]UserRoleAssignment, error)
- func (s *RoleService) ReconcileEnvOidcMappings(ctx context.Context, rawSpec string) error
- func (s *RoleService) ReplaceOidcAssignments(ctx context.Context, userID string, desired []UserRoleAssignment) error
- func (s *RoleService) ResolveApiKeyPermissions(ctx context.Context, apiKeyID string) (*authz.PermissionSet, error)
- func (s *RoleService) ResolvePermissions(ctx context.Context, user *common.User) (*authz.PermissionSet, error)
- func (s *RoleService) ResolveUserPermissionsInDB(_ context.Context, tx *gorm.DB, userID string) (*authz.PermissionSet, error)
- func (s *RoleService) SetApiKeyPermissions(ctx context.Context, apiKeyID string, grants []ApiKeyPermission) error
- func (s *RoleService) SetApiKeyPermissionsInDB(ctx context.Context, tx *gorm.DB, apiKeyID string, grants []ApiKeyPermission) error
- func (s *RoleService) SetUserAssignments(ctx context.Context, userID string, desired []UserRoleAssignment) error
- func (s *RoleService) UpdateOidcMapping(ctx context.Context, id, claimValue, roleID string, environmentID *string) (*OidcRoleMapping, error)
- func (s *RoleService) UpdateRole(ctx context.Context, id, name string, description *string, ...) (*Role, error)
- func (s *RoleService) ValidatePermissionsAgainstCaller(caller *authz.PermissionSet, desired []string) error
- func (s *RoleService) ValidateRoleAssignmentAgainstCaller(ctx context.Context, caller *authz.PermissionSet, roleID string, ...) error
- type SetUserRoleAssignmentsInput
- type SetUserRoleAssignmentsOutput
- type UpdateRoleInput
- type UpdateRoleOutput
- type UserRoleAssignment
Constants ¶
const ( RoleAssignmentSourceManual = "manual" RoleAssignmentSourceOidc = "oidc" )
Assignment source values stored in UserRoleAssignment.Source.
const ( OidcMappingSourceManual = "manual" OidcMappingSourceEnv = "env" )
Variables ¶
This section is empty.
Functions ¶
func RegisterRoles ¶
func RegisterRoles(api huma.API, roleService *RoleService)
Types ¶
type ApiKeyPermission ¶ added in v2.8.1
type ApiKeyPermission struct {
database.BaseModel
ApiKeyID string `json:"apiKeyId" gorm:"column:api_key_id;not null;index"`
Permission string `json:"permission" gorm:"column:permission;not null"`
EnvironmentID *string `json:"environmentId,omitempty" gorm:"column:environment_id"`
}
ApiKeyPermission is one permission grant on an API key, optionally scoped to a single environment. Permissions are stored per-row (rather than as a JSON column on api_keys) so we can index by (api_key_id, permission) for fast lookups in the auth bridge.
func (ApiKeyPermission) TableName ¶ added in v2.8.1
func (ApiKeyPermission) TableName() string
type CreateRoleInput ¶
type CreateRoleInput struct {
Body roletypes.CreateRole
}
type CreateRoleOutput ¶
type CreateRoleOutput struct {
Body base.ApiResponse[roletypes.Role]
}
type DeleteRoleInput ¶
type DeleteRoleInput struct {
ID string `path:"id" doc:"Role ID"`
}
type DeleteRoleOutput ¶
type DeleteRoleOutput struct {
Body base.ApiResponse[base.MessageResponse]
}
type Dependencies ¶
Dependencies are the collaborators the role domain needs.
type GetRoleInput ¶
type GetRoleInput struct {
ID string `path:"id" doc:"Role ID"`
}
type GetRoleOutput ¶
type GetRoleOutput struct {
Body base.ApiResponse[roletypes.Role]
}
type ListRolesInput ¶
type ListRolesInput struct {
Search string `query:"search" doc:"Search by role name or description"`
Sort string `query:"sort" doc:"Column to sort by"`
Order string `query:"order" default:"asc" doc:"Sort direction (asc or desc)"`
Start int `query:"start" default:"0" doc:"Start index for pagination"`
Limit int `query:"limit" default:"20" doc:"Items per page"`
}
type ListUserRoleAssignmentsInput ¶
type ListUserRoleAssignmentsInput struct {
UserID string `path:"userId" doc:"User ID"`
}
type ListUserRoleAssignmentsOutput ¶
type ListUserRoleAssignmentsOutput struct {
Body base.ApiResponse[[]roletypes.RoleAssignment]
}
type Module ¶
type Module struct {
// contains filtered or unexported fields
}
Module owns role persistence and its HTTP surface.
func (*Module) RegisterRoutes ¶
RegisterRoutes mounts role endpoints for runtime and schema discovery.
func (*Module) Service ¶
func (m *Module) Service() *RoleService
Service exposes role operations to authentication and authorization collaborators.
type OidcRoleMapping ¶ added in v2.8.1
type OidcRoleMapping struct {
database.BaseModel
ClaimValue string `json:"claimValue" gorm:"column:claim_value;not null;index"`
RoleID string `json:"roleId" gorm:"column:role_id;not null;index"`
EnvironmentID *string `json:"environmentId,omitempty" gorm:"column:environment_id"`
Source string `json:"source" gorm:"column:source;not null;default:'manual'"`
}
OidcRoleMapping maps an OIDC group/claim value to a role assignment. On every OIDC login, the auth service replaces all source='oidc' rows on the user with assignments derived from the mappings whose ClaimValue matches a claim returned by the IdP.
Source distinguishes UI/API-managed rows (the default) from env-declared rows reconciled at boot from OIDC_ROLE_MAPPINGS. Env-managed rows are read-only via the API — they can only be changed by editing the env var and restarting.
func (OidcRoleMapping) TableName ¶ added in v2.8.1
func (OidcRoleMapping) TableName() string
type PermissionsManifestOutput ¶
type PermissionsManifestOutput struct {
Body base.ApiResponse[roletypes.PermissionsManifest]
}
type Role ¶ added in v2.8.1
type Role struct {
database.BaseModel
Name string `json:"name" gorm:"column:name;not null;uniqueIndex" sortable:"true"`
Description *string `json:"description,omitempty" gorm:"column:description"`
Permissions database.StringSlice `json:"permissions" gorm:"column:permissions;type:text;not null"`
BuiltIn bool `json:"builtIn" gorm:"column:built_in;not null;default:false" sortable:"true"`
}
Role is a named permission set. Built-in roles (Admin, Editor, Deployer, Viewer) are seeded by migration 054 and cannot be edited or deleted.
type RoleHandler ¶
type RoleHandler struct {
// contains filtered or unexported fields
}
func (*RoleHandler) CreateRole ¶
func (h *RoleHandler) CreateRole(ctx context.Context, input *CreateRoleInput) (*CreateRoleOutput, error)
func (*RoleHandler) DeleteRole ¶
func (h *RoleHandler) DeleteRole(ctx context.Context, input *DeleteRoleInput) (*DeleteRoleOutput, error)
func (*RoleHandler) GetPermissionsManifest ¶
func (h *RoleHandler) GetPermissionsManifest(_ context.Context, _ *struct{}) (*PermissionsManifestOutput, error)
func (*RoleHandler) GetRole ¶
func (h *RoleHandler) GetRole(ctx context.Context, input *GetRoleInput) (*GetRoleOutput, error)
func (*RoleHandler) ListRoles ¶
func (h *RoleHandler) ListRoles(ctx context.Context, input *ListRolesInput) (*ListRolesOutput, error)
func (*RoleHandler) ListUserRoleAssignments ¶
func (h *RoleHandler) ListUserRoleAssignments(ctx context.Context, input *ListUserRoleAssignmentsInput) (*ListUserRoleAssignmentsOutput, error)
func (*RoleHandler) SetUserRoleAssignments ¶
func (h *RoleHandler) SetUserRoleAssignments(ctx context.Context, input *SetUserRoleAssignmentsInput) (*SetUserRoleAssignmentsOutput, error)
func (*RoleHandler) UpdateRole ¶
func (h *RoleHandler) UpdateRole(ctx context.Context, input *UpdateRoleInput) (*UpdateRoleOutput, error)
type RoleService ¶
type RoleService struct {
// contains filtered or unexported fields
}
RoleService owns role definitions, user role assignments, OIDC role mappings, and API key permissions. It resolves a caller's effective PermissionSet on demand and caches the result per-user / per-key for a short TTL to keep the hot path off the database.
func NewRoleService ¶
func NewRoleService(db *database.DB) *RoleService
func (*RoleService) AssertGlobalAdminExists ¶
func (s *RoleService) AssertGlobalAdminExists(ctx context.Context) error
AssertGlobalAdminExists returns common.ErrNoGlobalAdminRemains if zero non-service users resolve to global administrator permissions. Called at boot after the backfill migration; also called from inside mutation paths.
func (*RoleService) BackfillLegacyRoleAssignments ¶
func (s *RoleService) BackfillLegacyRoleAssignments(ctx context.Context) error
BackfillLegacyRoleAssignments migrates the pre-RBAC users.roles JSON column into rows in user_role_assignments. Safe to call on every boot: a no-op once the column is gone.
Users with "admin" in their legacy roles get a global Admin assignment; every other user gets a global Viewer assignment. The NULL environment_id lands the perms in PermissionSet.Global, which is what ps.Allows(perm, "") consults for org-level checks (list environments, read settings, list users, etc.) AND for env-scoped checks at the union step. Inserting per-environment viewer rows instead would lock non-admins out of the settings area entirely.
Lives here (not as a SQL migration) so the column-existence check is trivial in Go and the same code path covers both postgres and sqlite. Idempotent via ON CONFLICT DO NOTHING on the (user_id, role_id, env) unique index, so a half-finished prior run can be safely retried.
func (*RoleService) CountGlobalAdminsExcludingUser ¶
func (s *RoleService) CountGlobalAdminsExcludingUser(ctx context.Context, excludedUserID string) (int, error)
CountGlobalAdminsExcludingUser returns the number of non-service users (other than excludedUserID) whose resolved global permissions satisfy IsGlobalAdmin. Used as the authoritative check for "removing this user / demoting this assignment would leave the system with no admin."
func (*RoleService) CountUsersAssignedToRole ¶
CountUsersAssignedToRole returns how many distinct users hold an assignment to the given role (any source, any environment scope).
func (*RoleService) CreateOidcMapping ¶
func (s *RoleService) CreateOidcMapping(ctx context.Context, claimValue, roleID string, environmentID *string) (*OidcRoleMapping, error)
func (*RoleService) CreateRole ¶
func (*RoleService) DeleteOidcMapping ¶
func (s *RoleService) DeleteOidcMapping(ctx context.Context, id string) error
func (*RoleService) DeleteRole ¶
func (s *RoleService) DeleteRole(ctx context.Context, id string) error
func (*RoleService) EnsureBuiltInRoles ¶
func (s *RoleService) EnsureBuiltInRoles(ctx context.Context) error
EnsureBuiltInRoles overwrites the permission set on every built-in role to match the Go constants. Idempotent. Called at boot after migrations succeed.
func (*RoleService) GetOidcMapping ¶
func (s *RoleService) GetOidcMapping(ctx context.Context, id string) (*OidcRoleMapping, error)
func (*RoleService) InvalidateApiKey ¶
func (s *RoleService) InvalidateApiKey(apiKeyID string)
InvalidateApiKey drops the cached PermissionSet for one API key.
func (*RoleService) InvalidateUser ¶
func (s *RoleService) InvalidateUser(userID string)
InvalidateUser drops the cached PermissionSet for one user. Called from auth_service after a login that mutates assignments, and from any mutation path that doesn't already invalidate explicitly.
func (*RoleService) ListAllRoles ¶
func (s *RoleService) ListAllRoles(ctx context.Context) ([]Role, error)
func (*RoleService) ListOidcMappings ¶
func (s *RoleService) ListOidcMappings(ctx context.Context) ([]OidcRoleMapping, error)
func (*RoleService) ListRoles ¶
func (s *RoleService) ListRoles(ctx context.Context, params pagination.QueryParams) ([]Role, pagination.Response, error)
func (*RoleService) ListUserAssignments ¶
func (s *RoleService) ListUserAssignments(ctx context.Context, userID string) ([]UserRoleAssignment, error)
func (*RoleService) ReconcileEnvOidcMappings ¶
func (s *RoleService) ReconcileEnvOidcMappings(ctx context.Context, rawSpec string) error
ReconcileEnvOidcMappings replaces every source='env' row in oidc_role_mappings with the set declared by `rawSpec` (a JSON array of role.OidcRoleMappingSpec). Called once at boot. Behavior is declarative:
- rawSpec empty / unset → leaves DB rows alone (purely UI-managed mode).
- rawSpec is `[]` → wipes any previously-env-managed rows.
- rawSpec is a valid JSON array → upserts each spec, deletes stale env rows.
Manual rows (source='manual') are never touched. Bad JSON or an unknown role ID returns an error so a misconfigured deployment fails loudly rather than silently dropping mappings.
func (*RoleService) ReplaceOidcAssignments ¶
func (s *RoleService) ReplaceOidcAssignments(ctx context.Context, userID string, desired []UserRoleAssignment) error
ReplaceOidcAssignments replaces the user's source='oidc' assignments. Manual assignments are untouched. An OIDC mapping referencing a since-deleted role or environment fails with a typed error; the caller logs and continues login so the user simply receives no OIDC-derived assignments. Enforces the global-admin guard after the swap.
func (*RoleService) ResolveApiKeyPermissions ¶
func (s *RoleService) ResolveApiKeyPermissions(ctx context.Context, apiKeyID string) (*authz.PermissionSet, error)
ResolveApiKeyPermissions returns the PermissionSet for an API key. Caches per-key. Falls back to an empty set (deny-all) if the key has no perms.
func (*RoleService) ResolvePermissions ¶
func (s *RoleService) ResolvePermissions(ctx context.Context, user *common.User) (*authz.PermissionSet, error)
ResolvePermissions returns the effective PermissionSet for a user, caching the result per-user for permissionCacheTTL.
func (*RoleService) ResolveUserPermissionsInDB ¶
func (s *RoleService) ResolveUserPermissionsInDB(_ context.Context, tx *gorm.DB, userID string) (*authz.PermissionSet, error)
ResolveUserPermissionsInDB resolves permissions using the supplied transaction or database handle.
func (*RoleService) SetApiKeyPermissions ¶
func (s *RoleService) SetApiKeyPermissions(ctx context.Context, apiKeyID string, grants []ApiKeyPermission) error
SetApiKeyPermissions replaces every permission row on the given API key atomically. Validation that the granted permissions don't exceed the creator's capabilities happens in the handler layer.
func (*RoleService) SetApiKeyPermissionsInDB ¶
func (s *RoleService) SetApiKeyPermissionsInDB(ctx context.Context, tx *gorm.DB, apiKeyID string, grants []ApiKeyPermission) error
SetApiKeyPermissionsInDB replaces permission rows using the supplied transaction or database handle.
func (*RoleService) SetUserAssignments ¶
func (s *RoleService) SetUserAssignments(ctx context.Context, userID string, desired []UserRoleAssignment) error
SetUserAssignments replaces the user's source='manual' assignments with the given desired set. Source='oidc' rows are preserved (use ReplaceOidcAssignments for those). Enforces the global-admin guard.
func (*RoleService) UpdateOidcMapping ¶
func (s *RoleService) UpdateOidcMapping(ctx context.Context, id, claimValue, roleID string, environmentID *string) (*OidcRoleMapping, error)
func (*RoleService) UpdateRole ¶
func (*RoleService) ValidatePermissionsAgainstCaller ¶
func (s *RoleService) ValidatePermissionsAgainstCaller(caller *authz.PermissionSet, desired []string) error
ValidatePermissionsAgainstCaller rejects any permission in `desired` that the caller does not hold at global scope. Sudo callers (agent / env access tokens, bootstrap paths) bypass entirely. Holding a permission only inside a specific environment is intentionally insufficient: roles are reusable templates that can later be assigned globally, so an env-scoped grant must not let the caller mint a global-capable role.
Unknown permission strings are rejected first with an UnknownPermissionError so a caller typo-ing a permission gets a descriptive 400 instead of a misleading 403 from the escalation guard below (which would always fire on an unknown perm because no PermissionSet contains it). This also gives the escalation loop a clean invariant: every perm reaching it is real.
Callers should run this before persisting role permissions to defend against privilege escalation if the role mutation endpoints are ever exposed beyond global admins.
func (*RoleService) ValidateRoleAssignmentAgainstCaller ¶
func (s *RoleService) ValidateRoleAssignmentAgainstCaller(ctx context.Context, caller *authz.PermissionSet, roleID string, environmentID *string) error
ValidateRoleAssignmentAgainstCaller rejects assigning a role at the requested scope when the caller does not hold every permission in that role at that same scope.
type SetUserRoleAssignmentsInput ¶
type SetUserRoleAssignmentsInput struct {
UserID string `path:"userId" doc:"User ID"`
Body roletypes.SetUserAssignments
}
type SetUserRoleAssignmentsOutput ¶
type SetUserRoleAssignmentsOutput struct {
Body base.ApiResponse[[]roletypes.RoleAssignment]
}
type UpdateRoleInput ¶
type UpdateRoleInput struct {
ID string `path:"id" doc:"Role ID"`
Body roletypes.UpdateRole
}
type UpdateRoleOutput ¶
type UpdateRoleOutput struct {
Body base.ApiResponse[roletypes.Role]
}
type UserRoleAssignment ¶ added in v2.8.1
type UserRoleAssignment struct {
database.BaseModel
UserID string `json:"userId" gorm:"column:user_id;not null;index"`
RoleID string `json:"roleId" gorm:"column:role_id;not null;index"`
EnvironmentID *string `json:"environmentId,omitempty" gorm:"column:environment_id;index"`
Source string `json:"source" gorm:"column:source;not null;default:'manual'"`
}
UserRoleAssignment binds a user to a role, optionally scoped to one environment. EnvironmentID == nil means "global" — the role's permissions apply across all environments AND to org-level resources.
Source distinguishes manual assignments (managed by admins via the UI) from assignments synthesized from OIDC group mappings on every login.
func (UserRoleAssignment) TableName ¶ added in v2.8.1
func (UserRoleAssignment) TableName() string