role

package
v1.8.1 Latest Latest
Warning

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

Go to latest
Published: Oct 20, 2026 License: Apache-2.0 Imports: 9 Imported by: 1

Documentation

Overview

Package role defines the Role entity and its store interface for RBAC.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CheckParent added in v1.7.0

func CheckParent(ctx context.Context, s SlugGetter, tenantID string, r *Role, parentSlug string) error

CheckParent refuses setting parentSlug as the parent of r, in tenant tenantID and r's namespace. An empty parentSlug (no parent) always passes and reads nothing.

It returns a *ParentNotFoundError when no role in r's namespace has that slug, and a *CycleError when the parent is r itself or one of r's descendants. It walks up from the parent through ParentSlug; reaching r means the engine's inheritance walk would loop. A missing role further up the chain ends the walk, since a chain through a role that does not exist cannot lead back to r. The walk stops at the first slug it has already seen, so a cycle already in the data cannot hang it. A store failure is returned as is.

The read and the caller's write are not atomic. Two concurrent updates (a's parent set to b, b's to a) can each pass and store a two-role cycle. The evaluator bounds that: its inheritance walk skips a role it has already visited and stops at depth 20, so a check still finishes. Clearing either parent breaks the cycle.

func CheckPermissionUngranted added in v1.7.0

func CheckPermissionUngranted(ctx context.Context, s GrantLister, tenantID string, pm *permission.Permission) error

CheckPermissionUngranted refuses, with a *PermissionGrantedError, the delete of pm while any role in tenantID grants it. A store failure is returned as is.

func CheckWritable added in v1.7.0

func CheckWritable(r *Role) error

CheckWritable refuses, with a *SystemRoleError, any write to a system role: an update of any field, a delete, or a change to its grants. Assigning a system role to a subject is a membership change, not a write to the role, and does not call this. A nil role passes.

func HeldGrant added in v1.7.0

func HeldGrant(ctx context.Context, s GrantReader, tenantID string, r *Role, ref permission.Ref) (*permission.Permission, error)

HeldGrant returns the permission r grants under ref's exact (namespace, name) key, or a *GrantNotHeldError when it grants none. A detach checks this first because DetachPermission removes nothing and returns no error when the key does not match, so a detach naming the wrong namespace would report success while the grant survived. A store failure is returned as is.

Types

type CycleError added in v1.7.0

type CycleError struct {
	Slug   string
	Parent string
}

CycleError refuses a parent that would make a role its own ancestor, either directly (the role names itself) or through a chain of parents that leads back to it. It wraps wardenerr.ErrCyclicRoleInheritance (warden.ErrCyclicRoleInheritance).

func (*CycleError) Error added in v1.7.0

func (e *CycleError) Error() string

func (*CycleError) Unwrap added in v1.7.0

func (e *CycleError) Unwrap() error

type GrantLister added in v1.7.0

type GrantLister interface {
	ListRoles(ctx context.Context, filter *ListFilter) ([]*Role, error)
	ListRolePermissionsForRoles(ctx context.Context, tenantID string, roleIDs []id.RoleID) (map[id.RoleID][]*permission.Permission, error)
}

GrantLister is the two store reads GrantingRoles needs.

type GrantNotHeldError added in v1.7.0

type GrantNotHeldError struct {
	Role       string
	Permission string
}

GrantNotHeldError refuses detaching a grant the role does not hold. It wraps wardenerr.ErrNotFound, so the REST API answers it with 404 and the dashboard with NOT_FOUND.

func (*GrantNotHeldError) Error added in v1.7.0

func (e *GrantNotHeldError) Error() string

func (*GrantNotHeldError) Unwrap added in v1.7.0

func (e *GrantNotHeldError) Unwrap() error

type GrantReader added in v1.7.0

type GrantReader interface {
	ListRolePermissions(ctx context.Context, tenantID string, roleID id.RoleID) ([]*permission.Permission, error)
}

GrantReader is the one store read HeldGrant needs.

type ListFilter

type ListFilter struct {
	TenantID        string  `json:"tenant_id,omitempty"`
	NamespacePath   *string `json:"namespace_path,omitempty"`
	NamespacePrefix string  `json:"namespace_prefix,omitempty"`
	IsSystem        *bool   `json:"is_system,omitempty"`
	IsDefault       *bool   `json:"is_default,omitempty"`
	ParentSlug      *string `json:"parent_slug,omitempty"`
	Search          string  `json:"search,omitempty"`
	Limit           int     `json:"limit,omitempty"`
	Offset          int     `json:"offset,omitempty"`
}

ListFilter contains filters for listing roles.

NamespacePath does an exact-match filter; NamespacePrefix matches all descendants under a path (e.g. "engineering/" matches "engineering", "engineering/platform", "engineering/platform/sre").

type ParentNotFoundError added in v1.7.0

type ParentNotFoundError struct {
	Parent string
}

ParentNotFoundError refuses a parent slug that names no role in the child's tenant and namespace. The engine resolves a parent only there, so a role stored with such a parent would inherit nothing from it.

It deliberately does not wrap ErrRoleNotFound: the role being written was found, and the request is what names a missing parent, so it is bad input rather than a missing entity.

func (*ParentNotFoundError) Error added in v1.7.0

func (e *ParentNotFoundError) Error() string

type PermissionGrantedError added in v1.7.0

type PermissionGrantedError struct {
	Permission string
	Roles      []string
}

PermissionGrantedError refuses deleting a permission that roles still grant. Every store's DeletePermission also removes the grants, so the delete would silently strip it from those roles. Roles holds their slugs in GrantingRoles order. It wraps nothing: the REST API answers it with 409 and the dashboard with CONFLICT.

func (*PermissionGrantedError) Error added in v1.7.0

func (e *PermissionGrantedError) Error() string

type Role

type Role struct {
	ID            id.RoleID      `json:"id" db:"id"`
	TenantID      string         `json:"tenant_id" db:"tenant_id"`
	NamespacePath string         `json:"namespace_path,omitempty" db:"namespace_path"`
	AppID         string         `json:"app_id" db:"app_id"`
	Name          string         `json:"name" db:"name"`
	Description   string         `json:"description,omitempty" db:"description"`
	Slug          string         `json:"slug" db:"slug"`
	IsSystem      bool           `json:"is_system" db:"is_system"`
	IsDefault     bool           `json:"is_default" db:"is_default"`
	ParentSlug    string         `json:"parent_slug,omitempty" db:"parent_slug"`
	MaxMembers    int            `json:"max_members,omitempty" db:"max_members"`
	Metadata      map[string]any `json:"metadata,omitempty" db:"metadata"`
	CreatedBy     string         `json:"created_by,omitempty" db:"created_by"`
	UpdatedBy     string         `json:"updated_by,omitempty" db:"updated_by"`
	CreatedAt     time.Time      `json:"created_at" db:"created_at"`
	UpdatedAt     time.Time      `json:"updated_at" db:"updated_at"`
}

Role represents an authorization role that can be assigned to subjects.

Inheritance is expressed by ParentSlug, the slug of another role in the same tenant and namespace. Slugs are stable, human-readable, and unique per (tenant_id, namespace_path, slug) — making role hierarchies declarable in source-controlled config without knowing typeids upfront.

NamespacePath locates the role within the tenant's namespace tree (e.g. "engineering/platform"). Empty string is the tenant root. Resolution at Check time considers a subject's roles at the request's namespace and every ancestor of that namespace (cascading scope inheritance).

func GrantingRoles added in v1.7.0

func GrantingRoles(ctx context.Context, s GrantLister, tenantID string, pm *permission.Permission) ([]*Role, error)

GrantingRoles returns every role in tenantID whose grants include pm, matched on (namespace, name), sorted by slug, then namespace, then ID.

There is no store method for this direction, so it walks the tenant's roles a page at a time and checks each page's grants. It reads every page and never stops at a limit: CheckPermissionUngranted refuses a delete when this returns anyone, and a scan that gave up early would let the delete strip the grant from a role nobody was warned about.

type SlugGetter added in v1.7.0

type SlugGetter interface {
	GetRoleBySlug(ctx context.Context, tenantID, namespacePath, slug string) (*Role, error)
}

SlugGetter is the one store read CheckParent needs.

type Store

type Store interface {
	// CreateRole persists a new role.
	CreateRole(ctx context.Context, r *Role) error

	// GetRole retrieves a role by ID within a tenant.
	GetRole(ctx context.Context, tenantID string, roleID id.RoleID) (*Role, error)

	// GetRoles retrieves several roles by ID within a tenant in one round
	// trip. IDs that do not exist, or belong to another tenant, are omitted
	// from the result rather than reported as an error. Order is undefined.
	GetRoles(ctx context.Context, tenantID string, roleIDs []id.RoleID) ([]*Role, error)

	// GetRoleBySlug retrieves a role by tenant, namespace, and slug.
	// Slugs are unique per (tenant_id, namespace_path); the namespace
	// argument disambiguates roles that share the same slug across
	// different namespace scopes.
	GetRoleBySlug(ctx context.Context, tenantID, namespacePath, slug string) (*Role, error)

	// UpdateRole persists changes to a role. The row is matched on both the
	// ID and r.TenantID, and tenant_id is never part of the write, so an
	// update can neither reach into another tenant nor move a row between
	// tenants. Returns ErrRoleNotFound when nothing matches.
	UpdateRole(ctx context.Context, r *Role) error

	// DeleteRole removes a role by ID within a tenant.
	DeleteRole(ctx context.Context, tenantID string, roleID id.RoleID) error

	// ListRoles returns roles matching the filter.
	ListRoles(ctx context.Context, filter *ListFilter) ([]*Role, error)

	// CountRoles returns the number of roles matching the filter.
	CountRoles(ctx context.Context, filter *ListFilter) (int64, error)

	// ListRolePermissions returns the full Permission records granted to a
	// role, resolved via JOIN against warden_permissions. Returning the full
	// records avoids the engine's previous N+1 GetPermission loop in the
	// RBAC evaluator.
	ListRolePermissions(ctx context.Context, tenantID string, roleID id.RoleID) ([]*permission.Permission, error)

	// ListRolePermissionsForRoles resolves the grants of several roles in one
	// round trip, keyed by role ID. Roles that grant nothing, or that belong
	// to another tenant, are absent from the map.
	ListRolePermissionsForRoles(ctx context.Context, tenantID string, roleIDs []id.RoleID) (map[id.RoleID][]*permission.Permission, error)

	// AttachPermission links a permission to a role by natural key.
	// The (NamespacePath, Name) pair uniquely identifies a permission within
	// the role's tenant. Returns ErrRoleNotFound when the role is not in the
	// tenant.
	AttachPermission(ctx context.Context, tenantID string, roleID id.RoleID, ref permission.Ref) error

	// DetachPermission removes a permission grant from a role.
	DetachPermission(ctx context.Context, tenantID string, roleID id.RoleID, ref permission.Ref) error

	// SetRolePermissions replaces all permission grants for a role.
	SetRolePermissions(ctx context.Context, tenantID string, roleID id.RoleID, refs []permission.Ref) error

	// ListChildRoles returns direct child roles of a parent within a tenant.
	// Children are inherently per-tenant since slugs are unique per tenant.
	ListChildRoles(ctx context.Context, tenantID, parentSlug string) ([]*Role, error)

	// DeleteRolesByTenant removes all roles for a tenant.
	DeleteRolesByTenant(ctx context.Context, tenantID string) error
}

Store defines persistence operations for roles.

Every by-ID operation takes the tenant as a mandatory parameter and filters on it alongside the primary key. A call whose tenant does not own the row behaves exactly as if the row did not exist: reads and writes return ErrRoleNotFound, and nothing on disk changes.

type SystemRoleError added in v1.7.0

type SystemRoleError struct {
	Name string
}

SystemRoleError refuses a write that would change or delete a system role. It wraps wardenerr.ErrSystemRoleImmutable (warden.ErrSystemRoleImmutable).

func (*SystemRoleError) Error added in v1.7.0

func (e *SystemRoleError) Error() string

func (*SystemRoleError) Unwrap added in v1.7.0

func (e *SystemRoleError) Unwrap() error

Jump to

Keyboard shortcuts

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