assignment

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: 4 Imported by: 2

Documentation

Overview

Package assignment defines the Assignment entity (role→subject binding).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CheckCapLowering added in v1.7.0

func CheckCapLowering(ctx context.Context, s SubjectsForRoleLister, tenantID string, roleID id.RoleID, roleName string, oldCap, newCap int, now time.Time) error

CheckCapLowering refuses changing a role's member cap from oldCap to newCap when newCap is a positive number below the role's live member count (LiveMembers at now). It returns a *CapBelowMembersError for the refusal, or the store's error if the read fails.

It reads nothing and refuses nothing unless the cap is being lowered: a cap of 0 or below means unlimited, so clearing it, raising it, keeping it unchanged, or moving between unlimited values always passes. A role can already be over its cap (BootstrapAdmin assigns without checking it, code that writes through the store directly checks nothing, a role could be filled or its cap lowered before these checks existed, and neither check is atomic), and keeping that cap unchanged never blocks an edit to the role's other fields. Moving from unlimited to a positive cap is a lowering and is checked.

The count read and the caller's write are NOT atomic. No store offers a conditional role update tied to the assignment count, so an assignment created between this read and the write can leave the role over its new cap. Like the assignment-side cap check, this is best effort under concurrency, not a guarantee.

func CheckMemberCap added in v1.7.0

func CheckMemberCap(ctx context.Context, s SubjectsForRoleLister, tenantID string, roleID id.RoleID, roleName string, maxMembers int, subjectKind, subjectID string, now time.Time) error

CheckMemberCap refuses giving the subject (subjectKind, subjectID) a role capped at maxMembers when the role already has that many live members (LiveMembers at now). It returns a *RoleFullError for the refusal, or the store's error if the read fails.

A cap of 0 or below means unlimited, and then it reads nothing. A subject who already holds the role live adds nobody, so it is never refused: a second binding for a member (another namespace or resource) passes, and a duplicate reaches the store and reports the duplicate.

The count read and the caller's insert are NOT atomic. No store offers a conditional insert, so two concurrent creates can both pass and take a role one past its cap. The cap is best effort under concurrency, not a guarantee.

func IsLive added in v1.7.0

func IsLive(a *Assignment, now time.Time) bool

IsLive reports whether a grants anything at instant now.

This is the same test the engine applies when resolving roles (store/memory/store.go's ListRolesForSubject), and it is the definition of "expired" the whole assignment surface uses. A nil ExpiresAt never expires.

func LiveMembers added in v1.7.0

func LiveMembers(held []*Assignment, now time.Time) map[Member]struct{}

LiveMembers returns the distinct subjects among held that hold a live assignment at now. It is the one definition of a role's members that the member cap is checked against, both when a subject is assigned and when the cap is changed, so the two checks cannot disagree about who counts.

Types

type Assignment

type Assignment struct {
	ID            id.AssignmentID `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"`
	RoleID        id.RoleID       `json:"role_id" db:"role_id"`
	SubjectKind   string          `json:"subject_kind" db:"subject_kind"`
	SubjectID     string          `json:"subject_id" db:"subject_id"`
	ResourceType  string          `json:"resource_type,omitempty" db:"resource_type"`
	ResourceID    string          `json:"resource_id,omitempty" db:"resource_id"`
	ExpiresAt     *time.Time      `json:"expires_at,omitempty" db:"expires_at"`
	GrantedBy     string          `json:"granted_by,omitempty" db:"granted_by"`
	Metadata      map[string]any  `json:"metadata,omitempty" db:"metadata"`
	CreatedAt     time.Time       `json:"created_at" db:"created_at"`
}

Assignment binds a role to a subject within a tenant. Optionally scoped to a specific resource (resource-level RBAC).

NamespacePath locates the assignment within the tenant's namespace tree. An assignment at namespace N grants the subject the role for checks in N and all descendants.

type CapBelowMembersError added in v1.7.0

type CapBelowMembersError struct {
	RoleName string
	Members  int
	Cap      int
}

CapBelowMembersError refuses a member cap lower than the number of live members the role already has. Every write path returns this same text: the dashboard contract as CONFLICT, REST as 409, and a DSL apply as the error that stops it.

func (*CapBelowMembersError) Error added in v1.7.0

func (e *CapBelowMembersError) Error() string

type ListFilter

type ListFilter struct {
	TenantID        string     `json:"tenant_id,omitempty"`
	NamespacePath   *string    `json:"namespace_path,omitempty"`
	NamespacePrefix string     `json:"namespace_prefix,omitempty"`
	RoleID          *id.RoleID `json:"role_id,omitempty"`
	SubjectKind     string     `json:"subject_kind,omitempty"`
	SubjectID       string     `json:"subject_id,omitempty"`
	ResourceType    string     `json:"resource_type,omitempty"`
	ResourceID      string     `json:"resource_id,omitempty"`
	Limit           int        `json:"limit,omitempty"`
	Offset          int        `json:"offset,omitempty"`
}

ListFilter contains filters for listing assignments.

type Member added in v1.7.0

type Member struct {
	Kind string
	ID   string
}

Member is one subject holding a role: a distinct (subject kind, subject id). Assignment uniqueness includes namespace, resource type and resource id, so one subject can hold several rows for one role and still be one member.

type RoleFullError added in v1.7.0

type RoleFullError struct {
	RoleName string
	Cap      int
	Members  int
}

RoleFullError refuses a new member for a role that already holds as many live members as its cap allows. The dashboard contract returns its text as CONFLICT and REST as 409.

func (*RoleFullError) Error added in v1.7.0

func (e *RoleFullError) Error() string

type Store

type Store interface {
	// CreateAssignment persists a new assignment.
	CreateAssignment(ctx context.Context, a *Assignment) error

	// GetAssignment retrieves an assignment by ID within a tenant.
	GetAssignment(ctx context.Context, tenantID string, assID id.AssignmentID) (*Assignment, error)

	// DeleteAssignment removes an assignment by ID within a tenant.
	DeleteAssignment(ctx context.Context, tenantID string, assID id.AssignmentID) error

	// ListAssignments returns assignments matching the filter.
	ListAssignments(ctx context.Context, filter *ListFilter) ([]*Assignment, error)

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

	// ListRolesForSubject returns role IDs assigned to a subject (global)
	// across the given namespace paths. Pass nil or an empty slice to match
	// any namespace (legacy/unscoped behavior).
	//
	// An assignment whose ExpiresAt has passed is excluded, as if the row
	// did not exist: the backend applies `expires_at IS NULL OR
	// expires_at > now` in the same query, so an expired grant can never
	// leak into a Check() decision between expiring and a cleanup sweep
	// (DeleteExpiredAssignments) getting around to deleting it.
	ListRolesForSubject(ctx context.Context, tenantID string, namespacePaths []string, subjectKind, subjectID string) ([]id.RoleID, error)

	// ListRolesForSubjectOnResource returns role IDs assigned to a subject
	// scoped to a specific resource, across the given namespace paths.
	//
	// Expired assignments are excluded the same way as ListRolesForSubject.
	ListRolesForSubjectOnResource(ctx context.Context, tenantID string, namespacePaths []string, subjectKind, subjectID, resourceType, resourceID string) ([]id.RoleID, error)

	// ListSubjectsForRole returns all assignments for a given role within a
	// tenant.
	ListSubjectsForRole(ctx context.Context, tenantID string, roleID id.RoleID) ([]*Assignment, error)

	// ListExpiringAssignments returns the tenant's assignments that expire
	// before the given time, oldest expiry first. Used by access reviews.
	// A limit of 0 means the backend default of 1000.
	ListExpiringAssignments(ctx context.Context, tenantID string, before time.Time, limit int) ([]*Assignment, error)

	// DeleteExpiredAssignments removes assignments that have expired before
	// the given time, in every tenant.
	DeleteExpiredAssignments(ctx context.Context, now time.Time) (int64, error)

	// DeleteExpiredAssignmentsForTenant removes one tenant's assignments that
	// have expired before the given time and reports how many rows it
	// removed. No other tenant's rows are touched. An empty tenantID returns
	// wardenerr.ErrTenantRequired and deletes nothing; it never means every
	// tenant.
	DeleteExpiredAssignmentsForTenant(ctx context.Context, tenantID string, now time.Time) (int64, error)

	// DeleteAssignmentsBySubject removes all assignments for a subject.
	DeleteAssignmentsBySubject(ctx context.Context, tenantID, subjectKind, subjectID string) error

	// DeleteAssignmentsByRole removes a tenant's assignments for a role.
	DeleteAssignmentsByRole(ctx context.Context, tenantID string, roleID id.RoleID) error

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

Store defines persistence operations for role assignments.

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 returns ErrAssignmentNotFound and changes nothing.

type SubjectsForRoleLister added in v1.7.0

type SubjectsForRoleLister interface {
	ListSubjectsForRole(ctx context.Context, tenantID string, roleID id.RoleID) ([]*Assignment, error)
}

SubjectsForRoleLister is the one store read the cap checks need.

Jump to

Keyboard shortcuts

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