identity

package
v1.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 2, 2026 License: AGPL-3.0 Imports: 22 Imported by: 0

Documentation

Overview

Package identity owns the humans a deployment knows: users, the teams they form, and the memberships that tie them together. A user arrives through an acquisition path — OAuth or enterprise SSO — that resolves an external subject to the one user model here; this package holds the models and their durable repositories. The routes that authenticate a person live with the identity grant, not here.

Identity is optional. A deployment with no identity configured has no rows here, and every account works exactly as it does without users.

Index

Constants

View Source
const StorageSchemaVersion = 1

StorageSchemaVersion identifies the only supported identity record schema.

Variables

View Source
var (
	// ErrNoProvidersConfigured reports an acquisition config with nothing in
	// it. The caller should not have built an acquisition path at all.
	ErrNoProvidersConfigured = errors.New("no identity providers configured")
	// ErrCallbackBaseRequired reports a config that names providers but no
	// address for them to send the browser back to.
	ErrCallbackBaseRequired = errors.New("identity providers need a callback base URL")
	// ErrUnknownProvider reports a provider name no configured acquisition
	// path serves.
	ErrUnknownProvider = errors.New("unknown identity provider")
	// ErrClaimInvalid reports an identity claim that is unknown, spent, or
	// expired. One message for all three: a claim is redeemed once, seconds
	// after it is issued, and which way it failed tells a caller nothing
	// actionable.
	ErrClaimInvalid = errors.New("identity claim is invalid or expired")
)
View Source
var (
	// ErrMissingID reports an empty or oversized identifier.
	ErrMissingID = errors.New("missing id")
	// ErrMissingSubject reports a user without an external subject.
	ErrMissingSubject = errors.New("missing external identity subject")
	// ErrInvalidName reports an empty or oversized human-readable name.
	ErrInvalidName = errors.New("invalid name: must be 1-255 characters")
	// ErrInvalidTimestamps reports an update that precedes creation.
	ErrInvalidTimestamps = errors.New("updated_at must not be before created_at")

	// ErrUserNotFound reports a missing user.
	ErrUserNotFound = errors.New("user not found")
	// ErrUserConflict reports an existing user, a taken subject, or a
	// stale revision.
	ErrUserConflict = errors.New("user revision conflict")
	// ErrCorruptUser reports invalid durable user data.
	ErrCorruptUser = errors.New("user record is invalid")

	// ErrTeamNotFound reports a missing team.
	ErrTeamNotFound = errors.New("team not found")
	// ErrTeamConflict reports an existing team or a stale revision.
	ErrTeamConflict = errors.New("team revision conflict")
	// ErrCorruptTeam reports invalid durable team data.
	ErrCorruptTeam = errors.New("team record is invalid")

	// ErrMembershipNotFound reports a membership that does not exist.
	ErrMembershipNotFound = errors.New("membership not found")
	// ErrMembershipConflict reports a membership that already exists.
	ErrMembershipConflict = errors.New("membership already exists")

	// ErrGranteeRequired reports an account grant that names no grantee or
	// both kinds at once: a grant gives an account to exactly one user or
	// exactly one team.
	ErrGranteeRequired = errors.New("an account grant names one user or one team")
	// ErrAccountGrantNotFound reports an account grant that does not exist.
	ErrAccountGrantNotFound = errors.New("account grant not found")
	// ErrAccountGrantConflict reports an account grant that already exists.
	ErrAccountGrantConflict = errors.New("account grant already exists")
)
View Source
var (
	// ErrIncompleteWorkOS reports a WorkOS config missing its credential
	// halves.
	ErrIncompleteWorkOS = errors.New("WorkOS needs an API key and a client ID")
	// ErrWorkOSDestinationRequired reports a WorkOS config that names no
	// organization or connection, so WorkOS would not know which enterprise's
	// people to ask for.
	ErrWorkOSDestinationRequired = errors.New("WorkOS needs an organization or a connection")
)
View Source
var (
	// ErrIncompleteOAuthProvider reports a provider missing its client ID or
	// secret.
	ErrIncompleteOAuthProvider = errors.New("OAuth provider needs a client ID and a client secret")
)
View Source
var ErrRepositoryRequired = errors.New("identity storage is required")

ErrRepositoryRequired reports a missing relational store.

Functions

func CallbackPath added in v1.1.0

func CallbackPath(provider string) string

CallbackPath returns the route a provider redirects back to. It lives here so the URL an operator registers and the route the server mounts cannot drift apart.

Types

type AccountGrant added in v1.1.0

type AccountGrant struct {
	AccountID string    `json:"account_id"`
	UserID    string    `json:"user_id,omitempty"`
	TeamID    string    `json:"team_id,omitempty"`
	CreatedAt time.Time `json:"created_at"`
}

AccountGrant maps who may use which account: it gives one account to one user or one team. Exactly one grantee side is set. Granting to a team reaches every member through their memberships, so an operator manages the team, not each person. The account itself lives in internal/account; this row only names it, the way a shared credential's grant list names accounts without owning them.

func (AccountGrant) Validate added in v1.1.0

func (g AccountGrant) Validate() error

Validate checks the account-grant invariants: a real account on one side, exactly one valid grantee on the other.

type AccountGrantRepository added in v1.1.0

type AccountGrantRepository interface {
	Add(context.Context, AccountGrant) (AccountGrant, error)
	// Remove deletes the exact grant its argument states; only the grantee
	// fields and the account matter, not the timestamp.
	Remove(context.Context, AccountGrant) error
	ListByAccount(context.Context, string) ([]AccountGrant, error)
	ListByUser(context.Context, string) ([]AccountGrant, error)
	ListByTeam(context.Context, string) ([]AccountGrant, error)
	// ReachableAccounts reports every account a user's grants reach: the
	// ones granted to the user directly and the ones granted to any team
	// the user belongs to, deduplicated and ordered.
	ReachableAccounts(ctx context.Context, userID string) ([]string, error)
}

AccountGrantRepository is the durable account-grant contract. A grant is a link row like a membership: added and removed, never edited.

type AccountResolver added in v1.1.0

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

AccountResolver resolves the accounts an identity session reaches. A session carries the provider-qualified subject an acquisition path resolved; this turns that subject into account IDs through the user's own grants and the grants of every team the user is on. It is the object the composition root hands the session gate, satisfying the gate's contract structurally the way the Authenticator satisfies its identity slot.

func NewAccountResolver added in v1.1.0

func NewAccountResolver(users UserRepository, grants AccountGrantRepository) (*AccountResolver, error)

NewAccountResolver builds the resolver over the two repositories it reads.

func (*AccountResolver) ReachableAccounts added in v1.1.0

func (r *AccountResolver) ReachableAccounts(ctx context.Context, subject string) ([]string, error)

ReachableAccounts reports every account the subject's grants reach. A subject with no user — one whose user was removed while a session still lived — reaches nothing, which is a normal answer here, not a failure: the session stays valid and simply has no accounts behind it.

type AcquisitionConfig added in v1.1.0

type AcquisitionConfig struct {
	CallbackBaseURL string
	OAuthProviders  []OAuthProvider
	WorkOS          WorkOSConfig
}

AcquisitionConfig is what an operator supplies to open the identity seam. CallbackBaseURL is the address every provider sends the browser back to, scheme and host only; the per-provider callback path is appended so the operator registers exactly the URL this gateway serves. Each acquisition half is on when it names anything at all, and a half-configured one is refused with a named error instead of silently dropped.

type Authenticator added in v1.1.0

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

Authenticator is the identity seam's one value: it dispatches each request to the acquisition path serving that provider, resolves every completed dance to the one user model, and answers the identity grant's Authenticate with the subject a redeemed claim names.

It implements localauth.IdentityProvider structurally: this package stays below the grant seam in the import graph, and the composition root hands the value across.

func NewAuthenticator added in v1.1.0

func NewAuthenticator(cfg AcquisitionConfig, users UserRepository) (*Authenticator, error)

NewAuthenticator opens the identity seam over the operator's configured acquisition paths: gothic for the OAuth providers, WorkOS for enterprise SSO, either or both.

func (*Authenticator) Authenticate added in v1.1.0

func (a *Authenticator) Authenticate(claim string) (string, error)

Authenticate redeems a claim for the subject it names. It is the localauth.IdentityProvider contract: the grant calls it once per claim, and a second redemption is a replay.

func (*Authenticator) Begin added in v1.1.0

func (a *Authenticator) Begin(w http.ResponseWriter, r *http.Request, provider string) error

Begin redirects the browser to the named provider's consent page.

func (*Authenticator) Complete added in v1.1.0

func (a *Authenticator) Complete(w http.ResponseWriter, r *http.Request, provider string) (string, error)

Complete finishes the provider's callback: the acquisition path verifies what came back, the seam resolves the person to the one user model, and the caller receives a one-time claim the identity grant will redeem. The claim is the seam: a path proved who the person is, and only the grant may turn that proof into a session.

func (*Authenticator) Providers added in v1.1.0

func (a *Authenticator) Providers() []string

Providers reports the configured provider names, sorted. The console reads this to know which buttons to draw.

type Membership added in v1.1.0

type Membership struct {
	UserID    string    `json:"user_id"`
	TeamID    string    `json:"team_id"`
	CreatedAt time.Time `json:"created_at"`
}

Membership ties one user to one team. It carries no state of its own beyond when it was made: what a membership grants comes from what the team is granted.

func (Membership) Validate added in v1.1.0

func (m Membership) Validate() error

Validate checks the membership invariants.

type MembershipRepository added in v1.1.0

type MembershipRepository interface {
	Add(context.Context, Membership) (Membership, error)
	Remove(ctx context.Context, userID, teamID string) error
	ListByUser(context.Context, string) ([]Membership, error)
	ListByTeam(context.Context, string) ([]Membership, error)
}

MembershipRepository is the durable membership contract. A membership is a link row: it is added and removed, never edited, so it carries no revision.

type OAuthProvider added in v1.1.0

type OAuthProvider struct {
	Name         string
	ClientID     string
	ClientSecret string
}

OAuthProvider is one operator-configured OAuth application. The name is the provider this gateway supports — see supportedOAuthProviders — and the pair is the application the operator registered with that provider.

type Repositories added in v1.1.0

type Repositories struct {
	Users         UserRepository
	Teams         TeamRepository
	Memberships   MembershipRepository
	AccountGrants AccountGrantRepository
}

Repositories bundles the identity repositories one store opens.

func Open

func Open(db *sqlstore.DB) (Repositories, error)

Open returns sqlstore-backed identity repositories. The caller has already migrated the store; this constructor only refuses a nil one.

type Team added in v1.1.0

type Team struct {
	ID   string `json:"id"`
	Name string `json:"name"`
	// Budget bounds the team's nano-USD spend inside one fixed UTC interval,
	// across every key attributed to the team. Nil leaves the team unmetered.
	Budget    *limits.TeamBudget `json:"budget,omitempty"`
	CreatedAt time.Time          `json:"created_at"`
	UpdatedAt time.Time          `json:"updated_at"`
}

Team is a named group of users. Access granted to a team reaches every member, so a team is the unit an operator manages instead of people.

func (Team) Validate added in v1.1.0

func (t Team) Validate() error

Validate checks the team invariants.

type TeamRecord added in v1.1.0

type TeamRecord struct {
	Revision uint64
	Team     Team
}

TeamRecord is one versioned team repository value.

type TeamRepository added in v1.1.0

type TeamRepository interface {
	Create(context.Context, Team) (TeamRecord, error)
	GetByID(context.Context, string) (TeamRecord, error)
	List(context.Context, int, int) ([]TeamRecord, error)
	Update(context.Context, Team, uint64) (TeamRecord, error)
	Delete(context.Context, string, uint64) error
}

TeamRepository is the durable team contract.

type User added in v1.1.0

type User struct {
	ID string `json:"id"`
	// Subject is the provider-qualified external identity subject, such as
	// "google:114380...". It never changes for the life of the user.
	Subject     string    `json:"subject"`
	Email       string    `json:"email,omitempty"`
	DisplayName string    `json:"display_name,omitempty"`
	CreatedAt   time.Time `json:"created_at"`
	UpdatedAt   time.Time `json:"updated_at"`
}

User is one human the deployment knows. The subject is the external identity an acquisition path resolved — the provider-qualified subject an OAuth or SSO callback names — and it is unique: the same subject returning is the same user.

func (User) Validate added in v1.1.0

func (u User) Validate() error

Validate checks the user invariants.

type UserRecord added in v1.1.0

type UserRecord struct {
	Revision uint64
	User     User
}

UserRecord is one versioned user repository value.

type UserRepository added in v1.1.0

type UserRepository interface {
	Create(context.Context, User) (UserRecord, error)
	GetByID(context.Context, string) (UserRecord, error)
	// GetBySubject resolves the external identity subject an acquisition
	// path hands back to the one user it names.
	GetBySubject(context.Context, string) (UserRecord, error)
	List(context.Context, int, int) ([]UserRecord, error)
	Update(context.Context, User, uint64) (UserRecord, error)
	Delete(context.Context, string, uint64) error
}

UserRepository is the durable user contract. Users are relational — a subject lookup, memberships, and later grants all join on them — so the repository rides sqlstore.

type WorkOSConfig added in v1.1.0

type WorkOSConfig struct {
	APIKey       string
	ClientID     string
	Organization string
	Connection   string
	// Endpoint overrides the WorkOS API base URL. Tests point it at a stub;
	// production leaves it empty.
	Endpoint string
}

WorkOSConfig is what an operator supplies to open the WorkOS acquisition path. APIKey and ClientID come from the WorkOS dashboard; Organization or Connection names which enterprise directory the dance goes to, and at least one is required.

Jump to

Keyboard shortcuts

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