Documentation
¶
Overview ¶
Package provision is what SCIM provisioning means to the hub (Task 20415): which hub identity a provisioned user is, what deactivating one severs, what reactivating one restores, what deleting one destroys, and what a provisioned group grants.
pkg/scim speaks the protocol and decides nothing. This package decides, and leaves the HTTP surface — authentication, rate limiting, the routes — to pkg/ui, and storage to pkg/statedb.
The gap it closes ¶
"Disabling an account at the identity provider does not, on its own, sever anything this hub has already issued" was a sentence in the runbook, and true: sessions ran to their absolute TTL, API tokens to their ExpiresAt, glasses links for up to 30 days, and leases, running tasks and stored credentials until an administrator ran `cloop hub user offboard`. With an identity provider provisioning the hub over SCIM, the provider's own deactivation is the trigger:
- active:false suspends the identity. In one transaction the hub writes a deny binding for every identifier it resolved, deletes the sessions, revokes the API tokens and glasses links, and suspends the grants over the person's personal secrets; then it releases their leases and stops their running tasks. Memberships and stored credentials are kept.
- active:true lifts the suspension: the deny bindings it wrote and the grants it suspended, and nothing else. What it destroyed stays destroyed.
- DELETE, or ui.oidc.scim.offboard_after_days of inactivity, runs the full offboarding (pkg/offboard) — under a legal hold when one is placed.
- A provisioned group is a claim source a role binding can name (claim: scim_group), read at every authorization decision.
Who a provisioned user is ¶
ui.oidc.scim.match_claim says which claim of a signed-in identity a provisioned user is matched against: email (the default), preferred_username, oid or sub. A match that addresses more than one hub identity is never resolved by guessing — see targetFor.
Index ¶
- Constants
- Variables
- func FindCredential(db *statedb.DB, ref string) (statedb.SCIMCredentialRow, error)
- func HoldIdentity(raw string) (string, error)
- func ParseCredential(raw string) (id, secret string, ok bool)
- func PlaceHold(db *statedb.DB, identity, holdReason string, ac AdminContext, now time.Time) (statedb.LegalHoldRow, error)
- func ReleaseHold(db *statedb.DB, identity string, ac AdminContext, now time.Time) (statedb.LegalHoldRow, bool, error)
- func RevokeCredential(db *statedb.DB, ref string, ac AdminContext, now time.Time) (statedb.SCIMCredentialRow, bool, error)
- func SubjectValue(claim MatchClaim, s *authz.Subject) string
- func UserValue(claim MatchClaim, row statedb.SCIMUserRow) string
- func ValidCredentialName(name string) error
- type Actor
- type AdminContext
- type Config
- type Directory
- type DirectoryOption
- type DirectoryStats
- type Hub
- type MatchClaim
- type Minted
- type RateLimiter
- type Result
- type Service
- func (s *Service) CreateGroup(ctx context.Context, a Actor, body []byte, base string) (Result, error)
- func (s *Service) CreateUser(ctx context.Context, a Actor, body []byte, base string) (Result, error)
- func (s *Service) DeleteGroup(ctx context.Context, a Actor, id, ifMatch string) (Result, error)
- func (s *Service) DeleteUser(ctx context.Context, a Actor, id, ifMatch string) (Result, error)
- func (s *Service) GetGroup(id string, q url.Values, base string) (Result, error)
- func (s *Service) GetUser(id string, q url.Values, base string) (Result, error)
- func (s *Service) ListGroups(q url.Values, base string) (scim.ListResponse, error)
- func (s *Service) ListUsers(q url.Values, base string) (scim.ListResponse, error)
- func (s *Service) OffboardExpired(ctx context.Context) (int, error)
- func (s *Service) PatchGroup(ctx context.Context, a Actor, id string, body []byte, ifMatch, base string) (Result, error)
- func (s *Service) PatchUser(ctx context.Context, a Actor, id string, body []byte, ifMatch, base string) (Result, error)
- func (s *Service) ReplaceGroup(ctx context.Context, a Actor, id string, body []byte, ifMatch, base string) (Result, error)
- func (s *Service) ReplaceUser(ctx context.Context, a Actor, id string, body []byte, ifMatch, base string) (Result, error)
- type Severance
- type Verified
- type Verifier
Constants ¶
const CredentialPrefix = "cloop_scim_"
CredentialPrefix starts every SCIM credential. Exported so a scanner and the redaction registry have one constant to match.
const DefaultCheckEvery = 2 * time.Second
DefaultCheckEvery is how often a cache asks the database whether the directory changed, when nothing told it so.
const DefaultRatePerMinute = 600
DefaultRatePerMinute is a SCIM credential's request budget when ui.oidc.scim.rate_limit_per_minute is unset: enough for an identity provider's initial cycle over a directory of thousands, which it paces on 429s anyway, and a ceiling on what a stolen credential can do in a minute.
const MaxGrace = 7 * 24 * time.Hour
MaxGrace bounds how long a rotated-out secret may stay valid.
Variables ¶
var ( ErrNoCredential = errors.New("provision: not a SCIM credential") ErrBadCredential = errors.New("provision: unknown or wrong SCIM credential") ErrRevoked = errors.New("provision: the SCIM credential was revoked") )
Credential errors. Every one of them is the same 401 to a client.
var ErrInvalid = errors.New("provision: invalid")
ErrInvalid wraps a request the hub refuses for a reason of its own, not the protocol's: a credential name that is malformed or taken, a hold with no reason.
var MatchClaims = []MatchClaim{MatchEmail, MatchUsername, MatchOID, MatchSub}
MatchClaims lists the valid match claims, the default first.
Functions ¶
func FindCredential ¶
FindCredential resolves a credential by id, or by the name of a live one.
func HoldIdentity ¶
HoldIdentity reads the identity a hold is placed on: an email, lowercased, or a subject, as sub:<subject>. A bare value with no "@" is a subject.
func ParseCredential ¶
ParseCredential splits a credential into its id and secret without touching storage; ok is false for anything not shaped like one.
func PlaceHold ¶
func PlaceHold(db *statedb.DB, identity, holdReason string, ac AdminContext, now time.Time) (statedb.LegalHoldRow, error)
PlaceHold places a legal hold on identity, or restates the reason of one already placed.
func ReleaseHold ¶
func ReleaseHold(db *statedb.DB, identity string, ac AdminContext, now time.Time) (statedb.LegalHoldRow, bool, error)
ReleaseHold releases the hold on identity, reporting whether one was placed.
func RevokeCredential ¶
func RevokeCredential(db *statedb.DB, ref string, ac AdminContext, now time.Time) (statedb.SCIMCredentialRow, bool, error)
RevokeCredential revokes a credential, both its secrets at once. A credential already revoked is reported, not an error.
func SubjectValue ¶
func SubjectValue(claim MatchClaim, s *authz.Subject) string
SubjectValue is the value a signed-in subject is matched by under claim.
func UserValue ¶
func UserValue(claim MatchClaim, row statedb.SCIMUserRow) string
UserValue is the value a provisioned user is matched by under claim: what its identity must carry in the claim to be this user. "" means the user cannot be matched.
func ValidCredentialName ¶
ValidCredentialName reports whether name may name a credential: it becomes part of the audit actor, scim:<name>, so it is short and plain.
Types ¶
type AdminContext ¶
type AdminContext struct {
Actor string
Reason string
// Via is cli or ui; OSUser the shell user for a CLI change.
Via string
OSUser string
}
AdminContext is who administers a credential or a hold, for the trail.
type Config ¶
type Config struct {
// MatchClaim says which claim a provisioned user is matched against.
MatchClaim MatchClaim
// OffboardAfter is how long a user stays suspended before the full
// offboarding runs. Zero never runs it: only a DELETE does.
OffboardAfter time.Duration
}
Config is a hub's provisioning policy, ui.oidc.scim.
type Directory ¶
type Directory struct {
// contains filtered or unexported fields
}
Directory is a cached, indexed copy of the provisioned directory.
func NewDirectory ¶
func NewDirectory(db *statedb.DB, claim MatchClaim, opts ...DirectoryOption) (*Directory, error)
NewDirectory reads the directory once, eagerly: a hub that cannot read it at startup must not come up granting groups from nothing.
func (*Directory) Claim ¶
func (d *Directory) Claim() MatchClaim
Claim reports the match claim the directory indexes by.
func (*Directory) DirectoryGroups ¶
DirectoryGroups implements authz.DirectorySource.
func (*Directory) Invalidate ¶
func (d *Directory) Invalidate()
Invalidate makes the next read check the version, and reload on a change.
func (*Directory) Stats ¶
func (d *Directory) Stats() DirectoryStats
Stats reports the directory's summary.
type DirectoryOption ¶
type DirectoryOption func(*Directory)
DirectoryOption configures a Directory.
func WithCheckEvery ¶
func WithCheckEvery(d time.Duration) DirectoryOption
WithCheckEvery sets how often the version is checked.
func WithDirectoryClock ¶
func WithDirectoryClock(now func() time.Time) DirectoryOption
WithDirectoryClock replaces the clock, for tests.
func WithDirectoryErrors ¶
func WithDirectoryErrors(fn func(error)) DirectoryOption
WithDirectoryErrors installs a sink for failed refreshes: the cache keeps serving its last good copy, and the hub logs why.
type DirectoryStats ¶
type DirectoryStats struct {
Version int64 `json:"version"`
Users int `json:"users"`
Active int `json:"active"`
Inactive int `json:"inactive"`
Groups int `json:"groups"`
Memberships int `json:"memberships"`
// Unmatchable users carry no value for the match claim, and so are no
// hub identity at all; Ambiguous users share their value with another.
Unmatchable int `json:"unmatchable"`
Ambiguous int `json:"ambiguous"`
}
DirectoryStats summarise the directory, for Settings and the doctor.
type Hub ¶
type Hub interface {
// Collaborators returns what an offboarding reaches in this hub process
// — its live sessions, leases, tasks, project registry, secret store and
// Claude homes — for the control plane db. DB, Identity, Target, Reason,
// Actor and Via are the caller's to set.
Collaborators(db *statedb.DB) offboard.Options
// Severed is told, after a change committed, what it ended or restored
// behind the hub's caches, so the hub — and its other members — stop
// honouring it now rather than at their next refresh.
Severed(Severance)
// DirectoryChanged is told after every committed directory change.
DirectoryChanged()
}
Hub is the running hub's side of a provisioning change: what lives in its process rather than in the database.
type MatchClaim ¶
type MatchClaim string
MatchClaim is the claim a provisioned user is matched against.
const ( // MatchEmail matches the user's primary email against the email claim // (or an address-shaped preferred_username, as sign-in reads one). MatchEmail MatchClaim = "email" // MatchUsername matches userName against preferred_username. MatchUsername MatchClaim = "preferred_username" // MatchOID matches externalId against Entra ID's oid claim — what // Entra provisions as externalId when objectId is mapped to it. MatchOID MatchClaim = "oid" // MatchSub matches externalId against the subject — what Okta provisions // as externalId. MatchSub MatchClaim = "sub" )
func ParseMatchClaim ¶
func ParseMatchClaim(s string) (MatchClaim, error)
ParseMatchClaim reads a configured match claim. Empty is MatchEmail.
type Minted ¶
type Minted struct {
Row statedb.SCIMCredentialRow
Plaintext string
}
Minted is a credential and, once, its plaintext.
func MintCredential ¶
MintCredential creates a credential. The plaintext is returned here and never again.
type RateLimiter ¶
type RateLimiter struct {
// contains filtered or unexported fields
}
RateLimiter is a token bucket per credential.
func NewRateLimiter ¶
func NewRateLimiter(perMinute int) *RateLimiter
NewRateLimiter allows perMinute requests a minute per credential, in bursts of up to a sixth of that (at least ten). A non-positive perMinute is DefaultRatePerMinute.
func (*RateLimiter) Allow ¶
func (l *RateLimiter) Allow(key string) (bool, time.Duration)
Allow takes one request from key's bucket. When the bucket is empty it reports false and how long until a request would be admitted.
func (*RateLimiter) PerMinute ¶
func (l *RateLimiter) PerMinute() int
PerMinute reports the configured rate.
func (*RateLimiter) SetClock ¶
func (l *RateLimiter) SetClock(now func() time.Time)
SetClock replaces the clock, for tests.
type Result ¶
type Result struct {
// Status is the HTTP status.
Status int
// Resource is the representation returned, nil for 204.
Resource map[string]any
// Location is the resource's URL, for a 201.
Location string
// Version is the resource's version, for the ETag header; 0 for none.
Version int64
}
Result is one operation's response.
type Service ¶
type Service struct {
DB *statedb.DB
Config Config
Directory *Directory
Hub Hub
// Now is the clock; nil is time.Now.
Now func() time.Time
// contains filtered or unexported fields
}
Service applies SCIM requests to the directory and the hub.
func (*Service) CreateGroup ¶
func (s *Service) CreateGroup(ctx context.Context, a Actor, body []byte, base string) (Result, error)
CreateGroup serves POST /Groups.
func (*Service) CreateUser ¶
func (s *Service) CreateUser(ctx context.Context, a Actor, body []byte, base string) (Result, error)
CreateUser serves POST /Users.
func (*Service) DeleteGroup ¶
DeleteGroup serves DELETE /Groups/{id}.
func (*Service) DeleteUser ¶
DeleteUser serves DELETE /Users/{id}: the full offboarding, honouring a legal hold, and then the resource is gone.
func (*Service) ListGroups ¶
ListGroups serves GET /Groups.
func (*Service) OffboardExpired ¶
OffboardExpired runs the full offboarding for every user inactive for longer than Config.OffboardAfter, and reports how many it offboarded.
func (*Service) PatchGroup ¶
func (s *Service) PatchGroup(ctx context.Context, a Actor, id string, body []byte, ifMatch, base string) (Result, error)
PatchGroup serves PATCH /Groups/{id}: renames, and member additions and removals in every dialect pkg/scim reads. It answers 204, as RFC 7644 allows, rather than a group of thousands of members nobody asked to see.
func (*Service) PatchUser ¶
func (s *Service) PatchUser(ctx context.Context, a Actor, id string, body []byte, ifMatch, base string) (Result, error)
PatchUser serves PATCH /Users/{id}: Entra ID's deactivation and attribute updates, and Okta's deactivation.
type Severance ¶
type Severance struct {
Actor string
// RoleBindings: deny bindings were written or lifted.
RoleBindings bool
// Tokens are API tokens and glasses links revoked in the transaction.
Tokens []string
// Sessions are session rows deleted in the transaction — only on a hub
// with no running authenticator, which otherwise ends them itself.
Sessions []string
// GrantsSuspended and GrantsResumed are grants over the person's
// personal secrets.
GrantsSuspended []string
GrantsResumed []string
// Offboard is the report of a full offboarding, when one ran.
Offboard *offboard.Report
}
Severance is what one change ended or restored.
type Verified ¶
type Verified struct {
Actor Actor
// Previous reports that the secret presented is the one a rotation
// replaced, still inside its grace period.
Previous bool
}
Verified is a credential that authenticated a request.