accesschange

package
v0.14.0 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package accesschange records and announces reductions of a principal's access (ADR-010).

Access tokens are self-contained JWTs, so a relying party cannot see that a principal was removed from an organization, lost an application role or was deactivated until the token expires. SystemAuth therefore writes an access-change record in the transaction that makes the change (Record), and after commit announces it to a Sink (Publish). A relying party refuses tokens issued before a recorded change, which closes the window to one request in the same process, one polling interval on a replica, and the delivery time across a process boundary; the token lifetime remains the fallback bound.

Rows are kept only as long as a token issued before the change could still be valid anywhere; Prune removes older ones.

Index

Constants

View Source
const (
	// DefaultAccessTokenLifetime is the access-token lifetime when none is
	// configured.
	DefaultAccessTokenLifetime = 10 * time.Minute
	// MaxAccessTokenLifetime is the longest access-token lifetime a
	// deployment, client or application may configure.
	MaxAccessTokenLifetime = 15 * time.Minute
)

Access-token lifetimes (ADR-010 decision 1). Lifetime is the fallback bound on how long a change can go unseen, so it is short by default and capped.

Retention is how long a change is kept: the maximum access-token lifetime, the largest clock-skew margin a relying party is expected to use, and an hour of slack for pollers and replay that are behind.

Variables

This section is empty.

Functions

func ClampAccessTokenLifetime

func ClampAccessTokenLifetime(d time.Duration) time.Duration

ClampAccessTokenLifetime returns d limited to MaxAccessTokenLifetime. A non-positive d returns DefaultAccessTokenLifetime.

func Prune

func Prune(ctx context.Context, client *ent.Client, cutoff time.Time) (int, error)

Prune deletes the changes recorded before cutoff and reports how many it deleted. A change matters only while a token issued before it could be valid, so cutoff is the maximum access-token lifetime plus the relying-party clock-skew margin before now, with some slack for pollers: Retention is that.

func PruneExpired

func PruneExpired(ctx context.Context, client *ent.Client) (int, error)

PruneExpired deletes the changes older than Retention.

func Publish

func Publish(sink Sink, changes ...Change)

Publish announces changes to sink after their transaction committed. A nil sink is ignored, so services without one need no checks.

Types

type Change

type Change struct {
	// PrincipalID is the principal whose access changed.
	PrincipalID uuid.UUID
	// OrganizationID scopes the change to one organization; nil means
	// every organization.
	OrganizationID *uuid.UUID
	// ChangedAt is when the change was recorded, on the server clock. It
	// is set by Record; callers leave it zero.
	ChangedAt time.Time
	// Reason says why access changed.
	Reason Reason
}

Change is one recorded access change.

func Record

func Record(ctx context.Context, tx *ent.Tx, c Change) (Change, error)

Record writes c to sf_access_changes inside tx and returns it with ChangedAt set. Call it in the transaction that makes the change, so the record exists if and only if the change does, then hand the result to Publish after the commit.

func Since

func Since(ctx context.Context, client *ent.Client, since time.Time, limit int) ([]Change, error)

Since lists the changes recorded after since, oldest first, up to limit (0 means 1000). Times are compared in UTC, as they are stored, which SQLite needs because it compares timestamps as text. A poller or the replay endpoint reads with it.

type ChangeRecorder

type ChangeRecorder interface {
	RecordChange(principalID, organizationID string, changedAt time.Time, reason string)
}

ChangeRecorder takes a change as its parts. relyingparty.RevocationCache implements it, which keeps the relying-party package free of the identity store that this one depends on.

type Reason

type Reason string

Reason says why access changed.

The reasons form an open registry: the constants below are the complete list, sf_access_changes stores the value as a plain string, and Record rejects any value that is not registered. Adding a reason is therefore a code and documentation change with no migration: declare the constant, add it to Reasons, and add a row to the reason table in docs/design/architecture/authorization.md. TestRegistry fails when a constant is missing from Reasons, a reason has no documentation row, or a call site passes a value that is not registered.

Reasons are wire values: they appear in the back-channel access_change event and in GET /access-changes, so a value is never renamed or reused for a different meaning. Relying parties treat the reason as opaque, so a reason added later does not break an older relying party.

const (
	// MembershipRemoved: the principal was removed from, or deactivated
	// in, an organization.
	MembershipRemoved Reason = "membership_removed"
	// OrgRoleChanged: the principal's organization role changed.
	OrgRoleChanged Reason = "org_role_changed"
	// AppRoleChanged: an application role was changed or removed.
	AppRoleChanged Reason = "app_role_changed"
	// GrantReduced: a resource grant was reduced or removed (ADR-008).
	GrantReduced Reason = "grant_reduced"
	// Deactivated: the principal was deactivated.
	Deactivated Reason = "deactivated"
	// SessionsRevoked: the principal's sessions or tokens were revoked.
	SessionsRevoked Reason = "sessions_revoked"
	// ManagementGranted: the principal came under management and gave up
	// the capabilities a managed principal does not hold (ADR-009).
	ManagementGranted Reason = "management_granted"
	// ManagementEnded: the principal's management ended and its own
	// capabilities were restored (ADR-009).
	ManagementEnded Reason = "management_ended"
)

Reasons.

func Reasons

func Reasons() []Reason

Reasons returns the registered reasons.

func (Reason) Valid

func (r Reason) Valid() bool

Valid reports whether r is a registered reason.

func (Reason) Validate

func (r Reason) Validate() error

Validate returns an error naming r when it is not a registered reason.

type Sink

type Sink interface {
	AccessChange(Change)
}

Sink receives access changes after the transaction that recorded them commits. Implementations must be safe for concurrent use and must not block: a revocation cache writes to memory, and a back-channel sender queues. relyingparty.RevocationCache is adapted to a Sink by relyingparty.AccessChangeSink.

func ToSink

func ToSink(r ChangeRecorder) Sink

ToSink adapts a ChangeRecorder, such as a relyingparty.RevocationCache, to a Sink, so one cache can be passed to every service's WithAccessChangeSink option and read by the relying parties of the same process.

type SinkFunc

type SinkFunc func(Change)

SinkFunc adapts a function to Sink.

func (SinkFunc) AccessChange

func (f SinkFunc) AccessChange(c Change)

AccessChange implements Sink.

type Source

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

Source reads changes through an Ent client, for replicas of a monolith that share SystemAuth's database and already hold a client. It implements relyingparty.AccessChangeSource. relyingparty/pgstore offers the same over plain database/sql.

func NewSource

func NewSource(client *ent.Client) *Source

NewSource returns a Source reading client.

func (*Source) AccessChangesSince

func (s *Source) AccessChangesSince(ctx context.Context, since time.Time) ([]relyingparty.AccessChange, error)

AccessChangesSince implements relyingparty.AccessChangeSource.

func (*Source) List

func (s *Source) List(ctx context.Context, since time.Time, limit int) ([]Change, error)

List implements the reader systemauth serves GET /access-changes from: up to limit changes after since, oldest first.

Jump to

Keyboard shortcuts

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