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
- func ClampAccessTokenLifetime(d time.Duration) time.Duration
- func Prune(ctx context.Context, client *ent.Client, cutoff time.Time) (int, error)
- func PruneExpired(ctx context.Context, client *ent.Client) (int, error)
- func Publish(sink Sink, changes ...Change)
- type Change
- type ChangeRecorder
- type Reason
- type Sink
- type SinkFunc
- type Source
Constants ¶
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.
const Retention = MaxAccessTokenLifetime + time.Minute + time.Hour
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 ¶
ClampAccessTokenLifetime returns d limited to MaxAccessTokenLifetime. A non-positive d returns DefaultAccessTokenLifetime.
func Prune ¶
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 ¶
PruneExpired deletes the changes older than Retention.
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 ¶
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.
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.
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 ¶
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 (*Source) AccessChangesSince ¶
func (s *Source) AccessChangesSince(ctx context.Context, since time.Time) ([]relyingparty.AccessChange, error)
AccessChangesSince implements relyingparty.AccessChangeSource.