rbac

package
v1.18.0 Latest Latest
Warning

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

Go to latest
Published: Jul 24, 2026 License: Apache-2.0 Imports: 21 Imported by: 13

README

pkg/rbac

This package computes effective authority inside UNI after authentication and identity propagation have already happened.

Intent

The package is the platform's effective-authority engine.

It takes authenticated actor context and delegated identity context, resolves roles and group membership from identity storage, and produces ACLs that the rest of the system can enforce.

Its main responsibilities are:

  • resolve permissions for users, service accounts, and system accounts
  • apply global, organization, and project scope
  • prevent confused-deputy behaviour when a service acts as an impersonated principal
  • constrain administrative delegation so callers cannot grant authority they do not themselves hold

This package is not just a convenience layer for handler checks. It is part of the security model.

Security Model

The package enforces several important security rules:

  • authority is derived from roles via group membership and actor type
  • permissions are additive within the allowed role set
  • protected roles are internal-only roles and are never user-facing
  • a caller may only grant a role if the caller already holds all permissions contained in that role
  • when a system service acts as an impersonated principal, the effective ACL is the intersection of the principal's ACL and the service's ACL
  • platform-administrator matching is issuer-qualified: a subject is only recognized as a platform administrator when the token's src_iss matches the registered issuer entry

Those rules prevent several different forms of privilege escalation:

  • user-facing exposure of internal platform roles
  • granting permissions the caller does not personally hold
  • confused-deputy expansion through service-to-service calls
  • cross-issuer confused-deputy: an external IdP cannot impersonate a UNI-local admin subject, and a UNI-local admin subject cannot be promoted to admin via an external token

Scope Model

The package works with the same three logical scope levels defined by identity roles:

  • global
  • organization
  • project

ACL construction and handler enforcement both follow that structure. Global permissions can satisfy organization and project checks, organization permissions can satisfy some project checks, and project permissions remain the narrowest scope.

This scoped structure is used both for direct authorization decisions and for query limiting in list operations.

Each scope check comes in three argument flavours so callers pass whatever they already hold, without re-deriving it:

  • AllowOrganizationScope / AllowProjectScope / AllowProjectScopeCreate take plain string IDs. These are deprecated (marked // Deprecated: so tooling flags new use) but retained for backwards compatibility while callers that still deal in plain strings (IDs sourced from API response bodies, and repos that pre-date the typed ID types) migrate; they will be removed once that is done.
  • …ID variants (AllowOrganizationScopeID, AllowProjectScopeID, AllowProjectScopeCreateID) take typed ids.OrganizationID / ids.ProjectID. API handlers use these, since the IDs arrive already decoded from URL path parameters.
  • …Reader variants (AllowOrganizationScopeReader, AllowProjectScopeReader, AllowProjectScopeCreateReader) take a resource implementing ids.OrganizationScopeReader / ids.ProjectScopeReader and recover the IDs from it. Callers holding a CRD use these — the label-read-and-parse happens in one place behind the interface rather than at every call.

Rule of thumb: path-parameter handler → …ID; you have a CRD object in hand → …Reader.

Built-in Roles

The role catalogue is defined in charts/identity/values.yaml and rendered into Role resources by charts/identity/templates/roles.yaml. That values file is the single source of truth; pkg/rbac resolves those roles but never invents them. There are two families.

Protected (platform) roles

Roles marked protected: true are internal-only: never returned by the user-facing role list and never grantable through the API. They are bound solely via Helm values at deployment time.

  • platform-administrator — global CRUD over every resource; can act in any organization or project.
  • region-service, kubernetes-service, compute-service, storage-service — system accounts mapped from an mTLS certificate common name (see the Actor Model). Each holds only the global permissions the corresponding service actually exercises; over-permissioning here is a security defect.
User-facing roles

These carry organization and/or project scope blocks and are the roles an administrator grants to groups.

Role organization block project block
administrator full CRUD across identity, region, storage, Kubernetes and compute
auditor read-only across all of the above
user org-wide reads, plus region:images create/delete CRUD on workloads: networks, load balancers, security groups, file storage, object storage, SSH CAs, clusters, instances
reader org-wide reads (region:images read only) read-only on those same workloads

administrator and auditor hold all their authority at organization scope. user and reader keep a thin organization-wide read baseline but place their real workload authority in the project block, so it applies only to the projects their group is linked to.

Grant relationships

A caller may grant a role only if they already hold every permission it contains, at the grant's scope or broader (AllowRole, with the downward scope flow described above). Because a grant hands out a subset of what the caller already holds, the built-in roles form a superset lattice:

administrator ─┬─ auditor ─── reader
               └─ user ────── reader
  • administrator can grant every user-facing role.
  • auditor (read-only) can grant reader (also read-only) but not user, which needs write verbs auditor lacks.
  • user can grant reader — the same project scope with fewer verbs (downscoping) — but not auditor, which needs identity:* reads user lacks.
  • reader can grant only reader.

user and auditor are incomparable, and neither can grant administrator. This lattice is locked down by TestBuiltinRoleGrantability, which drives AllowRole from the parsed chart values for every ordered role pair, asserting each allowed edge and rejecting every non-edge.

Actor Model

The package distinguishes three important actor classes:

  • users
  • service accounts
  • system accounts

Users derive access from organization membership, groups, and roles.

Service accounts derive access from their bound organization and group membership.

System accounts derive access from configured platform roles mapped from their authenticated service identity, typically an mTLS certificate common name.

When a system account carries an impersonated principal, RBAC does not simply switch to the principal's ACL. Instead, it intersects the principal ACL with the system account ACL so the service cannot exercise permissions that either side lacks.

Platform-administrator issuer-aware fast-path

User account ACL resolution includes a fast-path for platform administrators. The match is on the pair (srcIss, subject) where srcIss is the issuer URL (verbatim, as the IdP emits it) carried in the passport's src_iss claim (or the "uni" sentinel for UNI-local tokens). The match is an exact string comparison against each PlatformAdministratorSubject entry registered via --platform-administrator-subjects; the configured issuer must equal the emitted iss exactly (for Auth0, including the trailing slash).

Platform-administrator subjects should be registered in issuer::subject form when any non-UNI bearer-trust provider is configured. A bare subject (no :: prefix) defaults the issuer to the UNI sentinel, which cannot be forged by an external token because the sentinel is deliberately not a valid URL.

Bare entries are mirrored onto the legacy Auth0 issuer at server construction. When the deprecated --auth0-exchange-issuer flag is set, expandBareAdminSubjects (in pkg/server) appends, for every bare entry, a concrete issuer-qualified duplicate for that flag's issuer. This reproduces the issuer-unaware matching that predates issuer qualification: a bare entry matches both UNI-login sessions (via the retained sentinel entry) and Auth0-exchange sessions (via the mirror), and never a CRD-declared bearerTrust issuer. The mirror grants nothing the old issuer-blind match did not already grant.

Options.Validate is a startup-only, advisory migration check. When called during startup with the list of non-UNI bearerTrust issuers currently present in the operator namespace (the legacy flag issuer is deliberately excluded — the mirroring above already covers it), it reports any admin entry still in bare (UNI-sentinel) form. The caller logs a warning; startup is never rejected, since a bare entry cannot match a CRD-declared issuer and a boot-time failure would otherwise fire at an unrelated pod restart long after the first bearerTrust CRD was created. The always-on, runtime control is the issuer-qualified (srcIss, subject) match in processUserAccountACL. Operators must not rely on Options.Validate as a protection.

Invariants

  • Effective authority is computed from stored identity state, not invented ad hoc in handlers.
  • Protected roles are not part of normal user-facing role administration.
  • Role grantability is bounded by the caller's own effective permissions.
  • ACL intersection for impersonated system-account calls is deliberate least-privilege behaviour.
  • Service accounts are organization-bound and their scoped access must remain consistent with that binding.
  • Group membership is the main route from actors to roles.
  • The ACL output is both an enforcement artifact and a visibility artifact, so incorrect ACL construction affects both authorization and UX.
  • Platform-administrator matching is always issuer-qualified at runtime via (srcIss, subject). Options.Validate is a startup-only advisory warning; it does not replace the runtime control. Bare entries match only the UNI sentinel plus, via the startup mirror in pkg/server, the legacy auth0-exchange flag issuer — never a CRD-declared issuer.
  • The confused-deputy invariant: a system service acting as an impersonated principal cannot hold permissions that either the principal's ACL or the service's ACL denies. The ACL intersection enforces this regardless of which IdP authenticated the principal.

Caveats

  • The package is tightly coupled to the identity storage and scoping model, including groups, projects, organization mappings, and label-based queries.
  • Some migration-era behaviour is still present, especially compatibility with the deprecated Group.UserIDs field alongside the newer Subjects model.
  • The package contains a mix of ACL construction, scope filtering, and handler-facing convenience checks, so it is broader than a pure policy-definition layer.
  • Some pragmatic compatibility behaviour exists around scoped lookups and transition paths, so security-sensitive changes here should be reviewed in terms of end-to-end actor behaviour rather than local code shape alone.
  • Role permission sets must be distributed consistently across the role hierarchy. Grantability requires the caller to hold every permission a role contains at the same scope or broader (AllowRole; project-scoped endpoints are satisfied by project, then organization, then global authority — not flattened to an organization-only check). Granting a service's endpoints to a lower role such as user or reader without also granting them to every role above it in the grant latticeadministrator for any operation, and auditor for reads — silently makes that lower role non-grantable and invisible to those roles. Any new endpoint added to a role in charts/identity/values.yaml must be added to every role that should be able to grant it, not just the leaf roles that consume it. TestBuiltinRoleGrantability enforces this over the parsed chart values.
  • The application:* endpoints (application:applications, application:applicationsets) were removed because the application service was never implemented and never will be — they were dead configuration. The removal also fixed a live bug: they were present on platform-administrator, user, and reader but absent from the organization administrator, which broke administrator grantability of user/reader. They are gone for good; there is no service to grant access to.

TODO

  • Re-check places where globally scoped callers are allowed to skip existence verification for user-supplied scoped resource identifiers, especially create paths that accept project IDs in the request body.

Relationship To Other Packages

  • pkg/oauth2 establishes actor identity and session/token validity
  • pkg/principal carries delegated identity and impersonation signals
  • pkg/rbac converts those inputs into effective local authority
  • middleware and handlers consume the resulting ACLs to enforce access and shape responses
  • pkg/oauth2, which establishes actor identity, session validity, and local admission before RBAC resolution
  • pkg/userdb, which shields RBAC from the raw local identity storage model when resolving users, organization memberships, and service accounts
  • pkg/principal, which carries delegated identity and impersonation signals consumed here
  • pkg/apis/unikorn/v1alpha1, which defines the stored role, group, organization, project, user, and service-account resources this package resolves

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrResourceReference      = goerrors.New("resource reference error")
	ErrNoAuthz                = goerrors.New("no authorization data in userinfo")
	ErrWrongOrganizationCount = goerrors.New("expected exactly one organization ID")
	ErrNotInOrganization      = goerrors.New("subject not a member of organization")
	ErrInvalidPrincipalType   = goerrors.New("invalid impersonated principal type")
	ErrBareAdminSubject       = goerrors.New("bare platform-administrator-subjects entry with non-UNI issuer trusted; migrate to issuer::subject")
)
View Source
var (
	ErrNoMatches = goerrors.New("selector would select nothing")
)

Functions

func AddOrganizationAndProjectIDQuery added in v1.11.0

func AddOrganizationAndProjectIDQuery(ctx context.Context, selector labels.Selector, organizationQuery []string, projectQuery []string) (labels.Selector, error)

AddOrganizationAndProjectIDQuery gets all organizationIDs the user can access (or has requested explicit and has access to).

func AddOrganizationIDQuery added in v1.11.0

func AddOrganizationIDQuery(ctx context.Context, selector labels.Selector, query []string) (labels.Selector, error)

AddOrganizationIDQuery adds an organizational query selector that limits resources to be listed to those available in the ACL and optionally constrained to those in the request query using a boolean intersection.

func AddQuery added in v1.11.0

func AddQuery(selector labels.Selector, label string, vals []string) (labels.Selector, error)

AddQuery adds a set of query values to a label selector.

func AllowGlobalScope added in v0.2.20

func AllowGlobalScope(ctx context.Context, endpoint string, operation openapi.AclOperation) error

AllowGlobalScope tries to allow the requested operation at the global scope.

func AllowOrganizationScope deprecated added in v0.2.20

func AllowOrganizationScope(ctx context.Context, endpoint string, operation openapi.AclOperation, organizationID string) error

AllowOrganizationScope tries to allow the requested operation at the global scope, then the organization scope.

Deprecated: prefer the typed AllowOrganizationScopeID (for path-parameter IDs) or AllowOrganizationScopeReader (for a resource implementing ids.OrganizationScopeReader). This string overload is retained for backwards compatibility with callers that still deal in plain strings (e.g. IDs from API response bodies or pre-typed-ID repositories) and will be removed once those callers have migrated.

func AllowOrganizationScopeID added in v1.17.5

func AllowOrganizationScopeID(ctx context.Context, endpoint string, operation openapi.AclOperation, organizationID ids.OrganizationID) error

AllowOrganizationScopeID is the typed variant of AllowOrganizationScope. Use this when the caller holds an ids.OrganizationID; the string overload is retained for callers in other repos that already deal in plain strings.

func AllowOrganizationScopeReader added in v1.17.7

func AllowOrganizationScopeReader(ctx context.Context, endpoint string, operation openapi.AclOperation, scope ids.OrganizationScopeReader) error

AllowOrganizationScopeReader is the variant of AllowOrganizationScope for callers that hold a resource implementing ids.OrganizationScopeReader (e.g. a region CRD). It recovers the organization ID from the resource. API handlers holding a path-parameter ID should use AllowOrganizationScopeID instead.

func AllowProjectScope deprecated added in v0.2.20

func AllowProjectScope(ctx context.Context, endpoint string, operation openapi.AclOperation, organizationID, projectID string) error

AllowProjectScope tries to allow the requested operation at the global scope, then the organization scope, and finally at the project scope.

Deprecated: prefer the typed AllowProjectScopeID (for path-parameter IDs) or AllowProjectScopeReader (for a resource implementing ids.ProjectScopeReader). This string overload is retained for backwards compatibility with callers that still deal in plain strings (e.g. IDs from API response bodies or pre-typed-ID repositories) and will be removed once those callers have migrated.

func AllowProjectScopeCreate deprecated added in v1.16.0

func AllowProjectScopeCreate(ctx context.Context, client openapi.ClientWithResponsesInterface, endpoint string, operation openapi.AclOperation, organizationID, projectID string) error

AllowProjectScopeCreate is like AllowProjectScope but intended for v2 create operations where the project ID is supplied in the request body rather than the URL path. When access is granted via an organization-scoped ACL the project ID is untrusted user input, so this function additionally verifies the project exists via the identity API before returning nil. Global-scope callers (platform administrators) are exempt from this check and their supplied project ID is trusted directly.

Deprecated: prefer the typed AllowProjectScopeCreateID (for path-parameter IDs) or AllowProjectScopeCreateReader (for a resource implementing ids.ProjectScopeReader). This string overload is retained for backwards compatibility with callers that pre-date the typed ID types and will be removed once those callers have migrated; the typed variants delegate here after converting to strings.

func AllowProjectScopeCreateID added in v1.17.5

func AllowProjectScopeCreateID(ctx context.Context, client openapi.ClientWithResponsesInterface, endpoint string, operation openapi.AclOperation, organizationID ids.OrganizationID, projectID ids.ProjectID) error

AllowProjectScopeCreateID is the typed variant of AllowProjectScopeCreate.

func AllowProjectScopeCreateReader added in v1.17.7

func AllowProjectScopeCreateReader(ctx context.Context, client openapi.ClientWithResponsesInterface, endpoint string, operation openapi.AclOperation, scope ids.ProjectScopeReader) error

AllowProjectScopeCreateReader is the variant of AllowProjectScopeCreate for callers that hold a resource implementing ids.ProjectScopeReader. It recovers the organization and project IDs from the resource.

func AllowProjectScopeID added in v1.17.5

func AllowProjectScopeID(ctx context.Context, endpoint string, operation openapi.AclOperation, organizationID ids.OrganizationID, projectID ids.ProjectID) error

AllowProjectScopeID is the typed variant of AllowProjectScope.

func AllowProjectScopeReader added in v1.17.7

func AllowProjectScopeReader(ctx context.Context, endpoint string, operation openapi.AclOperation, scope ids.ProjectScopeReader) error

AllowProjectScopeReader is the variant of AllowProjectScope for callers that hold a resource implementing ids.ProjectScopeReader (e.g. a region CRD). It recovers the organization and project IDs from the resource. API handlers holding path-parameter IDs should use AllowProjectScopeID instead.

func AllowRole added in v0.2.21

func AllowRole(ctx context.Context, role *unikornv1.Role, organizationID ids.OrganizationID) error

AllowRole determines whether your ACL contains the same or higher privileges than the role, which is then used to determine role visibility and limit privilege escalation.

Each scope block of the target role is checked against the caller's authority at the matching scope, with the usual downward flow (global satisfies organization and project, organization satisfies project):

  • global endpoints must be held at global scope;
  • organization endpoints must be held at global or organization scope;
  • project endpoints must be held at global or organization scope, or at project scope in any project the caller can access (see allowGrantProjectScope).

The project case is why granting is subset-preserving rather than escalation-prone: a caller may only ever grant a role whose permissions they already hold at the grant's scope or broader.

func FromContext added in v0.2.20

func FromContext(ctx context.Context) *openapi.Acl

func HasNoMatches added in v1.12.0

func HasNoMatches(err error) bool

HasNoMatches is a short cut when nothing would be matched e.g. the user has no matching organization ID for the provided selector, and a list handler can just return an empty array directly.

func NewContext added in v0.2.20

func NewContext(ctx context.Context, acl *openapi.Acl) context.Context

func OrganizationIDs added in v1.11.0

func OrganizationIDs(ctx context.Context) []string

OrganizationIDs returns a list of all organization IDs from the ACL for the purposes of limiting list type API operations.

Types

type Options added in v0.2.54

type Options struct {
	PlatformAdministratorRoleIDs  []string
	PlatformAdministratorSubjects []PlatformAdministratorSubject
	SystemAccountRoleIDs          map[string]string
}

func (*Options) AddFlags added in v0.2.54

func (o *Options) AddFlags(f *pflag.FlagSet)

func (*Options) Validate added in v1.18.0

func (o *Options) Validate(trustedNonUNIIssuers []string) error

Validate reports whether the admin list still needs migrating: any bare (UNI-sentinel) entry while a non-UNI issuer is trusted. Advisory only — the caller logs the result rather than refusing to start; bare entries can never match a CRD-declared issuer, so the runtime issuer-match in processUserAccountACL remains the security control.

type PlatformAdministratorSubject added in v1.18.0

type PlatformAdministratorSubject struct {
	Issuer  string
	Subject string
}

PlatformAdministratorSubject binds an admin subject to the issuer that must have authenticated it, closing the cross-issuer confused deputy.

type PlatformAdministratorSubjectsValue added in v1.18.0

type PlatformAdministratorSubjectsValue []PlatformAdministratorSubject

PlatformAdministratorSubjectsValue parses repeated or comma-joined issuer::subject flags. A bare value (no "::") defaults the issuer to the UNI sentinel for backward compatibility with single-issuer deployments.

func (*PlatformAdministratorSubjectsValue) Set added in v1.18.0

func (*PlatformAdministratorSubjectsValue) String added in v1.18.0

func (*PlatformAdministratorSubjectsValue) Type added in v1.18.0

type RBAC

type RBAC struct {
	// contains filtered or unexported fields
}

RBAC contains all the scoping rules for services across the platform.

func New

func New(client client.Client, namespace string, options *Options) *RBAC

New creates a new RBAC client.

func (*RBAC) GetACL added in v0.1.23

func (r *RBAC) GetACL(ctx context.Context, organizationID string) (*openapi.Acl, error)

GetACL returns a granular set of permissions for a user based on their scope. This is used for API level access control and UX.

func (*RBAC) NewSuperContext added in v0.2.59

func (r *RBAC) NewSuperContext(ctx context.Context) (context.Context, error)

Jump to

Keyboard shortcuts

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