Documentation
¶
Overview ¶
Package managedprincipal manages managed principals: humans, such as young learners, whose accounts are created and controlled by managers who have a verified email (ADR-005). A principal is managed while it has at least one active manager relationship, whether or not it has an email of its own (ADR-009).
The package enforces the invariants: a managed principal can be created only when the deployment and the requesting client both allow it, a principal without a verified email always has at least one active manager, and every manager is an active human with an email who is not itself managed.
Index ¶
- Constants
- Variables
- func NewHandler(svc *Service, opts ...HandlerOption) http.Handler
- func RequestClientFromPrincipal(r *http.Request) (string, bool)
- type Claim
- type CreateInput
- type CreateRequest
- type Gate
- type HandlerOption
- type InviteManagerInput
- type IssueLoginTokenInput
- type IssueLoginTokenRequest
- type IssuedLoginToken
- type LoginTokenView
- type ManagedSummary
- type ManagerInput
- type ManagerInvite
- type ManagerView
- type Option
- func WithAccessChangeSink(sink accesschange.Sink) Option
- func WithAuditEmitter(e audit.Emitter) Option
- func WithAuthorizer(a authz.Authorizer) Option
- func WithAuthzSyncer(syncer authz.RelationshipSyncer, mode authz.SyncMode) Option
- func WithClaimTTL(d time.Duration) Option
- func WithDirectManagerAdds() Option
- func WithInviteTTL(d time.Duration) Option
- func WithLogger(l *slog.Logger) Option
- func WithRoles(roles ...string) Option
- type Relationship
- type Service
- func (s *Service) AcceptManagerInvite(ctx context.Context, token string, principalID uuid.UUID) (uuid.UUID, error)
- func (s *Service) AddManager(ctx context.Context, managedID uuid.UUID, m ManagerInput, actorID uuid.UUID) error
- func (s *Service) AttachVerifiedEmail(ctx context.Context, actorID, managedID uuid.UUID, email string, ...) error
- func (s *Service) Create(ctx context.Context, in CreateInput) (uuid.UUID, error)
- func (s *Service) DeclineManagerInvite(ctx context.Context, token string, principalID uuid.UUID) error
- func (s *Service) Delete(ctx context.Context, actorID, managedID uuid.UUID) error
- func (s *Service) InviteManager(ctx context.Context, in InviteManagerInput) (*ManagerInvite, error)
- func (s *Service) IsManaged(ctx context.Context, principalID uuid.UUID) (bool, error)
- func (s *Service) IssueClaim(ctx context.Context, actorID, managedID uuid.UUID) (*Claim, error)
- func (s *Service) IssueLoginToken(ctx context.Context, in IssueLoginTokenInput) (*credential.GeneratedLoginToken, error)
- func (s *Service) ListLoginTokens(ctx context.Context, actorID, managedID uuid.UUID) ([]*credential.Credential, error)
- func (s *Service) ListManagedBy(ctx context.Context, managerID, after uuid.UUID, limit int) ([]ManagedSummary, error)
- func (s *Service) ListManagers(ctx context.Context, managedID uuid.UUID) ([]Relationship, error)
- func (s *Service) ListManagersFor(ctx context.Context, actorID, managedID uuid.UUID) ([]Relationship, error)
- func (s *Service) ListSessions(ctx context.Context, actorID, managedID uuid.UUID) ([]Session, error)
- func (s *Service) ManagerRoles(ctx context.Context, managerID, managedID uuid.UUID) ([]string, error)
- func (s *Service) RedeemClaim(ctx context.Context, code string, claimantID uuid.UUID) (uuid.UUID, error)
- func (s *Service) Release(ctx context.Context, actorID, managedID uuid.UUID) error
- func (s *Service) RemoveManager(ctx context.Context, managedID uuid.UUID, m ManagerInput, actorID uuid.UUID) error
- func (s *Service) RevokeAll(ctx context.Context, actorID, managedID uuid.UUID) error
- func (s *Service) RevokeLoginToken(ctx context.Context, actorID, managedID, credentialID uuid.UUID) error
- func (s *Service) RevokeManagerInvite(ctx context.Context, actorID, inviteID uuid.UUID) error
- func (s *Service) RevokeSession(ctx context.Context, actorID, managedID uuid.UUID, kind SessionKind, ...) error
- func (s *Service) Roles() []string
- func (s *Service) VerifyEmail(ctx context.Context, managedID uuid.UUID, verifiedAt time.Time) error
- type Session
- type SessionKind
Constants ¶
const ( // ClaimCodePrefix starts every claim code. Codes use only uppercase // letters and digits, for QR codes and printed letters. ClaimCodePrefix = "SFMC" // DefaultClaimTTL is how long a claim code stays redeemable: long // enough for a letter sent home to reach a parent. DefaultClaimTTL = 7 * 24 * time.Hour // Audit event types for claims and attaching an email. EventClaimIssued = "managed_principal.claim_issued" EventClaimRedeemed = "managed_principal.claim_redeemed" EventEmailAttached = "managed_principal.email_attached" EventEmailVerified = "managed_principal.email_verified" )
const ( // InviteCodePrefix starts every manager invitation token. InviteCodePrefix = "SFMI" // DefaultInviteTTL is how long a manager invitation stays acceptable. DefaultInviteTTL = 7 * 24 * time.Hour // Audit event types for manager invitations. EventManagerInvited = "managed_principal.manager_invited" EventManagerInviteAccepted = "managed_principal.manager_invite_accepted" EventManagerInviteRevoked = "managed_principal.manager_invite_revoked" EventManagerInviteDeclined = "managed_principal.manager_invite_declined" )
const ( // RoleGuardian has full authority: consent, deletion, billing, sign-in // credentials, and adding or removing other managers. RoleGuardian = "guardian" // RoleEducator has operational authority: sign-in credentials and // reading the profile, without deletion, billing, or consent. RoleEducator = "educator" )
Built-in manager roles.
const ( EventCreated = "managed_principal.created" EventManagerAdded = "managed_principal.manager_added" EventManagerRemoved = "managed_principal.manager_removed" EventTokenIssued = "managed_principal.login_token_issued" // EventReleased records that a principal's management ended. EventReleased = "managed_principal.released" )
Audit event types emitted by the service.
const ( EventTokenRevoked = "managed_principal.login_token_revoked" EventSessionRevoked = "managed_principal.session_revoked" EventAllRevoked = "managed_principal.all_revoked" )
Audit event types for manager visibility actions.
const EventDeleted = "managed_principal.deleted"
EventDeleted records that a managed principal was deleted.
const MaxListLimit = 200
MaxListLimit bounds a page of ListManagedBy.
const Scope = "systemforge:managed_principals"
Scope is the OAuth scope an access token needs to call the managed-principal API that SystemAuth serves to relying parties.
Variables ¶
var ( // ErrInvalidClaim is returned for every claim code that cannot be // redeemed: unknown, malformed, expired, or already redeemed. ErrInvalidClaim = errors.New("managedprincipal: invalid claim code") // ErrEmailInUse is returned when attaching an email another human // already has. ErrEmailInUse = errors.New("managedprincipal: email is already in use") // ErrHasEmail is returned when attaching an email to a principal that // already has one. ErrHasEmail = errors.New("managedprincipal: principal already has an email") // ErrEmailRequired is returned when attaching an empty email, or // verifying a principal that has none. ErrEmailRequired = errors.New("managedprincipal: email is required") )
var ( // ErrInvalidInvite is returned for every invitation that cannot be // accepted or declined: unknown, malformed, expired, revoked, already // accepted or declined, or addressed to someone other than the // signed-in principal. ErrInvalidInvite = errors.New("managedprincipal: invalid manager invitation") // ErrAlreadyManaged is returned when accepting an invitation for a // principal that already has a manager. Its managers change the // relationships; an invitation is the way into management. ErrAlreadyManaged = errors.New("managedprincipal: principal is already managed") // ErrManagesOthers is returned when a principal that manages others // would enter management: a managed principal cannot manage. ErrManagesOthers = errors.New("managedprincipal: principal manages other principals") // ErrInviteEmailRequired is returned when inviting without an address. ErrInviteEmailRequired = errors.New("managedprincipal: invitee email is required") )
var ( // ErrNotAllowed is returned when the deployment or the requesting // client does not allow managed principals. ErrNotAllowed = errors.New("managedprincipal: managed principals are not enabled for this client") // ErrDisplayNameRequired is returned when creating a managed principal // without a display name. ErrDisplayNameRequired = errors.New("managedprincipal: display name is required") // ErrNoManagers is returned when creating a managed principal without // a manager. ErrNoManagers = errors.New("managedprincipal: at least one manager is required") // ErrInvalidManager is returned when a manager is not an active human // with an email, or is itself managed. ErrInvalidManager = errors.New("managedprincipal: manager must be an active human with an email") // ErrInvalidRole is returned for a manager role that is not registered. ErrInvalidRole = errors.New("managedprincipal: unknown manager role") // ErrNotManaged is returned when the principal is not a human, or has // no active manager, for operations that apply only to managed // principals. ErrNotManaged = errors.New("managedprincipal: principal is not managed (it has no active manager)") // ErrLastManager is returned when removing the last active manager of // a principal that has no verified email. Management ends only for a // principal that can be reached and verified (ADR-009). ErrLastManager = errors.New("managedprincipal: cannot remove the last manager of a principal without a verified email") // ErrNotManager is returned when the acting principal is not an // active manager of the managed principal. ErrNotManager = errors.New("managedprincipal: not an active manager of this principal") // ErrForbidden is returned when the acting principal lacks the // permission an operation requires. ErrForbidden = errors.New("managedprincipal: forbidden") // ErrConsentRequired means a manager would be added without agreeing; // managers join by invitation or claim unless the service was built // with WithDirectManagerAdds. ErrConsentRequired = errors.New("managedprincipal: a manager must accept an invitation or redeem a claim") // ErrNotFound is returned when a principal or relationship does not exist. ErrNotFound = errors.New("managedprincipal: not found") )
var DefaultCodeLimit = ratelimit.PerMinute(10)
DefaultCodeLimit is the default per-caller limit on redeeming claim codes and answering manager invitations. Codes carry 160 random bits, so the limit is defense in depth, sized for a person entering a code by hand.
var ErrEmailNotVerified = errors.New("managedprincipal: release requires a verified email")
ErrEmailNotVerified is returned by Release when the principal has no verified email: management ends only for a principal that can be reached and has proven it controls its address (ADR-009).
Functions ¶
func NewHandler ¶
func NewHandler(svc *Service, opts ...HandlerOption) http.Handler
NewHandler returns the managed-principal API. Mount it under a prefix, such as /managed-principals, behind requestprincipal.Middleware, which identifies the calling principal. Every route goes through the same service operations, and so the same authorizer checks, as the Go API.
Managing a principal:
GET / the caller's managed principals (?after=&limit=)
POST / create (actor must be a manager)
GET /{principalID}/managers view_profile
POST /{principalID}/managers add a manager; manage_managers
DELETE /{principalID}/managers/{managerID}/{role} remove a manager; manage_managers
POST /{principalID}/release end management; manage_managers
DELETE /{principalID} delete; manage_account
POST /{principalID}/claims issue a claim code; manage_credentials
POST /claims/redeem the caller redeems a claim code
Consent to be managed:
POST /manager-invites the caller offers to manage a verified address
POST /manager-invites/accept the caller accepts, entering management
POST /manager-invites/decline the caller declines
DELETE /manager-invites/{inviteID} the inviter withdraws it
Credentials and sessions, each requiring manage_credentials on the managed principal:
GET /{principalID}/login-tokens
POST /{principalID}/login-tokens issue; the caller is an active manager
DELETE /{principalID}/login-tokens/{credentialID}
GET /{principalID}/sessions
DELETE /{principalID}/sessions/{kind}/{sessionID}
POST /{principalID}/revoke-all
Options put the handler in relying-party mode (WithRequestClient) and rate limit the code routes (WithCodeRateLimit).
Types ¶
type CreateInput ¶
type CreateInput struct {
// ClientID is the OAuth client making the request; it must allow
// managed principals.
ClientID string
// DisplayName is the name shown for the managed principal.
DisplayName string
// Email optionally gives the principal an address of its own. It is
// stored unverified: it counts toward ending management only once
// verified (VerifyEmail). Management does not depend on it (ADR-009).
Email string
// Managers must contain at least one manager.
Managers []ManagerInput
// ActorID is the principal performing the action. It must be one of the
// managers, or a platform administrator.
ActorID uuid.UUID
}
CreateInput describes a managed principal to create.
type CreateRequest ¶
type CreateRequest struct {
// ClientID is the OAuth client making the request.
ClientID string `json:"client_id"`
DisplayName string `json:"display_name"`
Email string `json:"email,omitempty"`
Managers []ManagerView `json:"managers"`
// Role, in place of Managers, makes the caller the only manager in
// that role.
Role string `json:"role,omitempty"`
}
CreateRequest is the body of POST /.
type Gate ¶
type Gate interface {
ManagedPrincipalsAllowed(ctx context.Context, clientID string) (bool, error)
LoginTokenPolicy(ctx context.Context, clientID string) (credential.LoginTokenPolicy, error)
}
Gate reports whether a client may create and sign in managed principals, and the login token lifetimes that apply to it. *systemauth.Server implements it: ManagedPrincipalsAllowed requires both the deployment setting and the client's flag, and LoginTokenPolicy applies the client's own default lifetime over the deployment's.
type HandlerOption ¶ added in v0.17.0
type HandlerOption func(*handler)
HandlerOption configures NewHandler.
func WithCodeRateLimit ¶ added in v0.17.0
func WithCodeRateLimit(storage ratelimit.Storage, limit ratelimit.Limit) HandlerOption
WithCodeRateLimit limits, per calling principal, the requests that present a code: redeeming a claim and accepting or declining a manager invitation. Over the limit the handler answers 429 with Retry-After.
func WithRequestClient ¶ added in v0.17.0
func WithRequestClient(clientOf func(*http.Request) (string, bool)) HandlerOption
WithRequestClient puts the handler in relying-party mode, for an API that applications in other processes call with a manager's access token. The OAuth client that creation, login-token issue, and invitations are checked against is the one clientOf returns for the request, the client the token was issued to; a body client_id that differs is refused. Creation accepts only the caller as the initial manager, and adding a manager directly is refused: further managers join through invitations and claims, which ask for their consent.
type InviteManagerInput ¶
type InviteManagerInput struct {
// ClientID is the OAuth client making the request; it must allow
// managed principals.
ClientID string
// ManagerID is the human offering to manage: an active human with an
// email that is not itself managed. It is also the actor.
ManagerID uuid.UUID
// Email is the verified email of the human invited to be managed. Only
// a principal whose verified email matches can accept.
Email string
// Role is the manager role the inviter will hold.
Role string
}
InviteManagerInput describes a manager invitation.
type IssueLoginTokenInput ¶
type IssueLoginTokenInput struct {
// ManagedID is the managed principal the token signs in.
ManagedID uuid.UUID
// ClientID is the OAuth client requesting the token; it must allow
// managed principals and supplies the default lifetime.
ClientID string
// IssuedBy is the issuing manager.
IssuedBy uuid.UUID
// ExpiresAt optionally sets the expiry; it is clamped to the ceiling.
ExpiresAt *time.Time
// SingleUse makes the token valid for one sign-in.
SingleUse bool
// Name optionally labels the token, such as the card or device.
Name string
}
IssueLoginTokenInput describes a login token to issue for a managed principal.
type IssueLoginTokenRequest ¶ added in v0.17.0
type IssueLoginTokenRequest struct {
// ClientID is the OAuth client requesting the token. In relying-party
// mode it is the access token's client and may be omitted.
ClientID string `json:"client_id,omitempty"`
// Name optionally labels the token, such as the card or device.
Name string `json:"name,omitempty"`
// SingleUse makes the token valid for one sign-in.
SingleUse bool `json:"single_use,omitempty"`
// ExpiresAt optionally sets the expiry; it is clamped to the ceiling.
ExpiresAt *time.Time `json:"expires_at,omitempty"`
}
IssueLoginTokenRequest is the body of POST /{principalID}/login-tokens.
type IssuedLoginToken ¶ added in v0.17.0
type IssuedLoginToken struct {
ID uuid.UUID `json:"id"`
Token string `json:"token"`
Prefix string `json:"prefix"`
SingleUse bool `json:"single_use"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
}
IssuedLoginToken is the response to POST /{principalID}/login-tokens. The token value is returned only here.
type LoginTokenView ¶
type LoginTokenView struct {
ID uuid.UUID `json:"id"`
Prefix string `json:"prefix"`
Name string `json:"name,omitempty"`
IssuedBy uuid.UUID `json:"issued_by"`
SingleUse bool `json:"single_use"`
CreatedAt time.Time `json:"created_at"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
LastUsedIP string `json:"last_used_ip,omitempty"`
}
LoginTokenView is a login token as a manager sees it; it never carries the token value.
type ManagedSummary ¶ added in v0.17.0
type ManagedSummary struct {
PrincipalID uuid.UUID `json:"principal_id"`
DisplayName string `json:"display_name"`
// Roles are the roles the listing manager holds over the principal.
Roles []string `json:"roles"`
}
ManagedSummary is a managed principal as one of its managers lists it.
type ManagerInput ¶
ManagerInput names a manager and the role it holds.
type ManagerInvite ¶
type ManagerInvite struct {
ID uuid.UUID
Token string
Email string
Role string
ExpiresAt time.Time
}
ManagerInvite is a newly issued manager invitation. Token is available only here; the application delivers it to the invitee.
type ManagerView ¶
ManagerView is a manager relationship as the API returns it.
type Option ¶
type Option func(*Service)
Option configures a Service.
func WithAccessChangeSink ¶
func WithAccessChangeSink(sink accesschange.Sink) Option
WithAccessChangeSink announces session revocations (RevokeSession and RevokeAll) to sink after they commit (ADR-010). The access-change record is written whether or not a sink is set.
func WithAuditEmitter ¶
WithAuditEmitter records every action through the given emitter.
func WithAuthorizer ¶
func WithAuthorizer(a authz.Authorizer) Option
WithAuthorizer sets the authorizer that checks managers' permissions on managed principals, such as a SpiceDB provider or an rbac provider with a platform-administrator check. Defaults to an rbac provider backed by ManagerRoles.
func WithAuthzSyncer ¶
func WithAuthzSyncer(syncer authz.RelationshipSyncer, mode authz.SyncMode) Option
WithAuthzSyncer mirrors manager relationships into the authorization backend as guardian and educator relations on the managed principal. In authz.SyncModeStrict a sync failure is returned, after the database change has committed; otherwise it is logged.
func WithClaimTTL ¶
WithClaimTTL sets how long claim codes stay redeemable. Defaults to DefaultClaimTTL.
func WithDirectManagerAdds ¶ added in v0.17.0
func WithDirectManagerAdds() Option
WithDirectManagerAdds lets Create name managers other than the acting principal and lets AddManager make a human a manager directly, without that human accepting an invitation or redeeming a claim. Without it both return ErrConsentRequired, so nobody is made responsible for an account without agreeing. Use it only for trusted administrator tooling and migrations, never for a path an ordinary manager can reach.
func WithInviteTTL ¶
WithInviteTTL sets how long manager invitations stay acceptable. Defaults to DefaultInviteTTL.
func WithLogger ¶
WithLogger sets the logger used for failures that cannot be returned, such as an audit emission after the change committed.
type Relationship ¶
type Relationship struct {
ManagedPrincipalID uuid.UUID
ManagerPrincipalID uuid.UUID
Role string
Active bool
}
Relationship is an active or inactive manager relationship.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service creates managed principals and maintains their managers.
func NewService ¶
NewService returns a Service. The gate decides whether a client may create managed principals; pass the SystemAuth server.
func (*Service) AcceptManagerInvite ¶
func (s *Service) AcceptManagerInvite(ctx context.Context, token string, principalID uuid.UUID) (uuid.UUID, error)
AcceptManagerInvite is the signed-in principal's consent to be managed: it spends the invitation, makes the inviter a manager with the invited role, and gives up the capabilities that managed principals do not have (profile management, delegation). principalID is the caller's own identity, never a value a manager supplies. It returns the manager's ID.
The principal must be an active human whose verified email is the invited address, must have no manager yet (ErrAlreadyManaged), and must not manage others (ErrManagesOthers). Every failure to match is ErrInvalidInvite.
func (*Service) AddManager ¶
func (s *Service) AddManager(ctx context.Context, managedID uuid.UUID, m ManagerInput, actorID uuid.UUID) error
AddManager makes manager a manager of managed with the given role. It requires WithDirectManagerAdds and returns ErrConsentRequired otherwise; managers ordinarily join by accepting an invitation or redeeming a claim. It is a no-op when the relationship is already active and reactivates it when inactive. The actor needs manage_managers on the managed principal, which guardians hold; the permission is checked under the principal's row lock, in the transaction that writes.
func (*Service) AttachVerifiedEmail ¶
func (s *Service) AttachVerifiedEmail(ctx context.Context, actorID, managedID uuid.UUID, email string, verifiedAt time.Time) error
AttachVerifiedEmail gives a managed principal that has no email an address the caller has verified the principal controls, and makes it the identifier. Management is unchanged (ADR-009): manager relationships and the principal's managed capabilities stay until management ends, and the verified email is what allows it to end. The actor needs manage_account, which guardians hold.
Proving control of the address is the caller's job until SystemAuth's email verification (ADR-003) drives this step. A principal created with an unverified email is verified with VerifyEmail instead.
func (*Service) Create ¶
Create creates a managed principal with its managers in one transaction. It returns the new principal's ID. The actor must be one of the managers, the manager creating the principal, or a platform administrator (ErrForbidden otherwise). Without WithDirectManagerAdds the actor must be the only manager (ErrConsentRequired otherwise); others join later by invitation or claim.
func (*Service) DeclineManagerInvite ¶
func (s *Service) DeclineManagerInvite(ctx context.Context, token string, principalID uuid.UUID) error
DeclineManagerInvite is the invitee's refusal of an invitation to be managed. It marks the invitation declined, so it can be neither accepted nor declined again, and records an audit event, managed_principal. manager_invite_declined, carrying the inviter and the invitation for whoever notifies them. The inviter sees nothing change: no relationship or capability is touched.
principalID is the caller's own identity, never a value a manager supplies. As for AcceptManagerInvite, it must be an active human whose verified email is the invited address, and every failure to match is ErrInvalidInvite. Unlike acceptance it does not depend on the invitee's management state, so an invitee that is already managed may still clear an invitation away.
func (*Service) Delete ¶
Delete deletes a managed principal. The actor needs manage_account, which guardians hold (ADR-005: deletion is a guardian or platform-administrator action; an educator cannot).
Delete is the platform's soft delete, like organization deletion: the principal row stays so that identifiers elsewhere keep resolving, but everything that identifies or authenticates the person is removed in one transaction. The principal is deactivated; its email, names and avatar are cleared and its identifier becomes "deleted:<id>", which frees the address; its credentials, external identities, login sessions, consents and claims are removed; its OAuth and principal token families are revoked; its memberships and manager relationships are deactivated; and invitations to its address are withdrawn. An access change is recorded so tokens already issued stop being honored at once (ADR-010). Data an application keeps about the person, and any personal organization it owns, are the application's to remove.
func (*Service) InviteManager ¶
func (s *Service) InviteManager(ctx context.Context, in InviteManagerInput) (*ManagerInvite, error)
InviteManager offers to manage the self-managed human with the given verified email. Nothing changes for that human until it accepts the invitation while signed in (AcceptManagerInvite): a manager relationship with an existing principal is never created without its consent (ADR-009).
func (*Service) IsManaged ¶
IsManaged reports whether the principal is a human with at least one active manager relationship. An email, verified or not, does not change the answer (ADR-009).
func (*Service) IssueClaim ¶
IssueClaim issues a single-use code with which a human with an email becomes guardian of a managed principal, for example a parent receiving a letter from a teacher. The issuer needs manage_credentials, which guardians and educators hold.
func (*Service) IssueLoginToken ¶
func (s *Service) IssueLoginToken(ctx context.Context, in IssueLoginTokenInput) (*credential.GeneratedLoginToken, error)
IssueLoginToken issues a login token for a managed principal on behalf of one of its active managers holding manage_credentials, which guardians and educators do. The plain token is returned once.
func (*Service) ListLoginTokens ¶
func (s *Service) ListLoginTokens(ctx context.Context, actorID, managedID uuid.UUID) ([]*credential.Credential, error)
ListLoginTokens returns a managed principal's usable login tokens, newest first. It never returns token values.
func (*Service) ListManagedBy ¶ added in v0.17.0
func (s *Service) ListManagedBy(ctx context.Context, managerID, after uuid.UUID, limit int) ([]ManagedSummary, error)
ListManagedBy returns the active principals that managerID actively manages, ordered by principal ID, starting after the given ID (uuid.Nil for the first page). limit is clamped to 1..MaxListLimit. A manager only ever sees its own relationships, so the listing needs no further permission.
func (*Service) ListManagers ¶
ListManagers returns the active manager relationships of a principal.
func (*Service) ListManagersFor ¶
func (s *Service) ListManagersFor(ctx context.Context, actorID, managedID uuid.UUID) ([]Relationship, error)
ListManagersFor returns the active manager relationships of a managed principal to an actor that holds view_profile on it, which guardians and educators do.
func (*Service) ListSessions ¶
func (s *Service) ListSessions(ctx context.Context, actorID, managedID uuid.UUID) ([]Session, error)
ListSessions returns the ways a managed principal is signed in now: browser login sessions, OAuth token families, and principal token families.
func (*Service) ManagerRoles ¶
func (s *Service) ManagerRoles(ctx context.Context, managerID, managedID uuid.UUID) ([]string, error)
ManagerRoles returns the roles managerID actively holds over managedID. It matches authz.ManagerRoleGetter, so it can back the rbac provider: rbac.WithManagerRoleGetter(svc.ManagerRoles).
func (*Service) RedeemClaim ¶
func (s *Service) RedeemClaim(ctx context.Context, code string, claimantID uuid.UUID) (uuid.UUID, error)
RedeemClaim makes claimantID a guardian of the claim's managed principal and spends the claim. The claimant must be an active human with an email who is not managed. It returns the managed principal's ID.
func (*Service) Release ¶
Release ends a principal's management: it removes the principal's remaining managers and restores its default human capabilities in one transaction, after which the principal is self-managed. It requires a verified email (ErrEmailNotVerified otherwise) and the manage_managers permission, which guardians hold and which is checked under the principal's row lock. Release is an explicit, audited event (managed_principal.released); whether the person being released must confirm first is the application's decision.
func (*Service) RemoveManager ¶
func (s *Service) RemoveManager(ctx context.Context, managedID uuid.UUID, m ManagerInput, actorID uuid.UUID) error
RemoveManager deactivates a manager relationship. It refuses with ErrLastManager when this is the last active manager of a principal without a verified email; deactivate or delete the principal instead. Removing the last manager of a principal with a verified email releases it, as Release does. The actor needs manage_managers on the managed principal, which guardians hold; the permission is checked under the principal's row lock, in the transaction that writes, so of two concurrent removals that each withdraw the other's authority exactly one succeeds.
func (*Service) RevokeAll ¶
RevokeAll revokes every login token and ends every session of a managed principal in one transaction.
func (*Service) RevokeLoginToken ¶
func (s *Service) RevokeLoginToken(ctx context.Context, actorID, managedID, credentialID uuid.UUID) error
RevokeLoginToken revokes one of a managed principal's login tokens.
func (*Service) RevokeManagerInvite ¶
RevokeManagerInvite withdraws a pending invitation. Only the inviter may.
func (*Service) RevokeSession ¶
func (s *Service) RevokeSession(ctx context.Context, actorID, managedID uuid.UUID, kind SessionKind, sessionID uuid.UUID) error
RevokeSession ends one session listed by ListSessions. Revoking a token family revokes every token in it, so its refresh token cannot renew it.
func (*Service) VerifyEmail ¶
VerifyEmail records that a managed principal controls the email it was created with, so that its management may end (Release). It is called by the verification flow once the principal has proven control of the address (ADR-003), not by a manager: the platform does not let a manager vouch for someone else's address.
type Session ¶
type Session struct {
// ID identifies the session within its kind: the login session ID, or
// the token family ID (the token ID when it has no family).
ID uuid.UUID `json:"id"`
Kind SessionKind `json:"kind"`
// Provider is how a login session was established, such as
// login_token.
Provider string `json:"provider,omitempty"`
// ClientID is the OAuth client holding an OAuth session.
ClientID string `json:"client_id,omitempty"`
CreatedAt time.Time `json:"created_at"`
ExpiresAt time.Time `json:"expires_at"`
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
ClientIP string `json:"client_ip,omitempty"`
}
Session is one way a managed principal is currently signed in.
type SessionKind ¶
type SessionKind string
SessionKind distinguishes the ways a managed principal can be signed in.
const ( // SessionKindLogin is a SystemAuth browser login session. SessionKindLogin SessionKind = "login" // SessionKindOAuth is an OAuth token family a client holds. SessionKindOAuth SessionKind = "oauth" // SessionKindToken is a principal token family. SessionKindToken SessionKind = "principal_token" )