role

package
v2.8.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 19, 2026 License: BSD-3-Clause Imports: 21 Imported by: 0

Documentation

Overview

Package role owns RBAC roles, assignments, permission resolution, and role HTTP routes.

Index

Constants

View Source
const (
	RoleAssignmentSourceManual = "manual"
	RoleAssignmentSourceOidc   = "oidc"
)

Assignment source values stored in UserRoleAssignment.Source.

View 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

type Dependencies struct {
	DB *database.DB
}

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 ListRolesOutput

type ListRolesOutput struct {
	Body base.Paginated[roletypes.Role]
}

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 New

func New(deps Dependencies) *Module

New builds the role domain from its dependencies.

func (*Module) RegisterRoutes

func (m *Module) RegisterRoutes(api huma.API)

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.

func (Role) TableName added in v2.8.1

func (Role) TableName() string

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

func (s *RoleService) CountUsersAssignedToRole(ctx context.Context, roleID string) (int, error)

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 (s *RoleService) CreateRole(ctx context.Context, name string, description *string, permissions []string) (*Role, error)

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

func (s *RoleService) GetRole(ctx context.Context, id string) (*Role, 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 (s *RoleService) UpdateRole(ctx context.Context, id, name string, description *string, permissions []string) (*Role, error)

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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL