Help us shape the future of Go! Take the 2026 Go Developer Survey

control

package
v0.1.5 Latest Latest
Warning

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

Go to latest
Published: Oct 29, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

Documentation

Index

Constants

View Source
const (
	APIKeyScopeUser  = "user"
	APIKeyScopeOrg   = "org"
	APIKeyScopeAgent = "agent"
)

API key scopes. Scope names the SURFACE a credential may reach, and the three are mutually exclusive on purpose:

user   a person's own key. Carries no organization, so an org-scoped call
       must name one with X-Org-Id, and it is honoured only where that user
       holds a membership.
org    a tenant's key/secret pair. Already bound to one organization, and
       admitted everywhere EXCEPT the agent surface — so a human or an
       automation cannot present itself as an agent.
agent  the same shape as an org key, admitted ONLY on the agent surface — so
       a compromised hypervisor cannot turn its credential on the rest of
       the tenant's API.

The exclusion runs both ways deliberately. Restricting only the agent would leave an org key able to enrol, heartbeat and report as a fleet agent, which is a way to forge the fleet's view of itself.

View Source
const (
	AccessRuleKindEmail  = "email"
	AccessRuleKindDomain = "domain"
)
View Source
const (
	AccessRulePurposeAuthenticate = "authenticate"
	AccessRulePurposeCreateOrg    = "create_org"

	// AccessRulePurposeJoinOrg gates ACCEPTING an invitation, which is a
	// different question from either of the two above. A domain may be
	// perfectly entitled to authenticate and still be one the platform does not
	// want inside a customer's organization; conversely a contractor domain that
	// may never mint an organization of its own can reasonably be invited into
	// one. Folding this into create_org would tie a contractor's ability to join
	// to their ability to found, which is the conflation the purpose split
	// exists to prevent.
	AccessRulePurposeJoinOrg = "join_org"
)
View Source
const (
	AccessRulePolicyAllow = "allow"
	AccessRulePolicyDeny  = "deny"
)

Variables

View Source
var (
	ErrUnknownFlag = fmt.Errorf("no such feature flag")

	// ErrWouldLockEveryoneOut is raised when disabling a sign-in flag would leave
	// no way into the installation at all.
	//
	// Distinct from the other refusals because it is the only one an operator may
	// legitimately want to override: configuring a provider immediately after is
	// a real sequence, and refusing absolutely would make this system decide
	// something it cannot see. The point is that the decision has to be MADE
	// rather than arrived at by accident.
	ErrWouldLockEveryoneOut = fmt.Errorf(
		"this would leave no way to sign in to this installation")

	// ErrFlagNotTenantScoped is deliberately distinct from "unknown". An operator
	// who names a real flag at the wrong level has made a different mistake from
	// one who mistyped it, and telling them apart is the difference between
	// "check the spelling" and "this one is installation-wide".
	ErrFlagNotTenantScoped = fmt.Errorf("this flag is set for the whole installation, not per organization")
)

ErrUnknownFlag and ErrFlagNotTenantScoped are the two ways a write is refused.

View Source
var ErrConnectionStringNotEncrypted = errors.New("connection_string is not encrypted")

ErrConnectionStringNotEncrypted marks an org whose connection_string was stored in plaintext, before DSNs were sealed (F6). Its IV and tag are empty and ConnectionString holds the DSN itself.

A sentinel rather than a refusal, because callers differ on what it means: connecting a tenant must refuse it and ask for a re-provision, while destroying one still needs to know which database to drop.

Functions

func APIKeyScopeIsValid

func APIKeyScopeIsValid(s string) bool

APIKeyScopeIsValid is the single definition of a usable scope. An unknown scope must never read as "unrestricted": a credential whose surface cannot be determined is refused.

Types

type APIKey

type APIKey struct {
	ID          uuid.UUID  `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id" format:"uuid"`
	OrgID       *uuid.UUID `json:"org_id,omitempty" format:"uuid"`
	Scope       string     `gorm:"not null;default:''" json:"scope"`
	Purpose     string     `json:"purpose"`
	IsEphemeral bool       `json:"is_ephemeral"`
	Name        string     `gorm:"not null;default:''" json:"name"`
	KeyHash     string     `gorm:"uniqueIndex;not null" json:"-"`
	SecretHash  *string    `json:"-"`
	UserID      *uuid.UUID `json:"user_id,omitempty" format:"uuid"`
	ExpiresAt   *time.Time `json:"expires_at,omitempty" format:"date-time"`
	Revoked     bool       `gorm:"not null;default:false" json:"revoked"`
	Prefix      *string    `json:"prefix,omitempty"`
	LastUsedAt  *time.Time `json:"last_used_at,omitempty" format:"date-time"`
	CreatedAt   time.Time  `json:"created_at" gorm:"type:timestamptz;column:created_at;not null;default:now()" format:"date-time"`
	UpdatedAt   time.Time  `json:"updated_at" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()" format:"date-time"`
}

func (*APIKey) RequiresSecret

func (k *APIKey) RequiresSecret() bool

RequiresSecret reports whether this shape carries a secret half. User keys are a single opaque value; org and agent keys are a key/secret pair, so that the identifying half can be shown in a UI and logged while the secret half never leaves the moment it was minted.

func (*APIKey) Usable

func (k *APIKey) Usable(now time.Time) bool

Usable reports whether this key may authenticate at all, before any question of which surface it reaches.

type Account

type Account struct {
	ID            uuid.UUID      `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id" format:"uuid"`
	UserID        uuid.UUID      `gorm:"index;not null" json:"user_id" format:"uuid"`
	User          User           `gorm:"foreignKey:UserID" json:"-"`
	Provider      string         `gorm:"not null" json:"provider"`
	Subject       string         `gorm:"not null" json:"subject"`
	Email         *string        `json:"email,omitempty"`
	EmailVerified bool           `gorm:"not null;default:false" json:"email_verified"`
	Profile       datatypes.JSON `gorm:"type:jsonb;not null;default:'{}'" json:"profile"`
	CreatedAt     time.Time      `gorm:"type:timestamptz;column:created_at;not null;default:now()" json:"created_at" format:"date-time"`
	UpdatedAt     time.Time      `gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()" json:"updated_at" format:"date-time"`
}

type AuthAccessRule

type AuthAccessRule struct {
	ID        uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
	Kind      string    `gorm:"not null;type:varchar(10);uniqueIndex:idx_auth_access_rules_kind_value_purpose,priority:1" json:"kind"`
	Value     string    `gorm:"not null;type:varchar(255);uniqueIndex:idx_auth_access_rules_kind_value_purpose,priority:2" json:"value"`
	Purpose   string    `` /* 222-byte string literal not displayed */
	Policy    string    `gorm:"not null;type:varchar(10);check:chk_auth_access_rules_policy,policy IN ('allow','deny')" json:"policy"`
	Reason    *string   `gorm:"type:text" json:"reason,omitempty"`
	CreatedAt time.Time `gorm:"type:timestamptz;column:created_at;not null;default:now()" json:"created_at"`
	UpdatedAt time.Time `gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()" json:"updated_at"`
}

type AuthAuditEvent

type AuthAuditEvent struct {
	ID             uuid.UUID                          `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
	SubjectID      uuid.UUID                          `gorm:"type:uuid;not null;index" json:"subject_id"`
	SubjectKind    string                             `gorm:"type:varchar(10);not null" json:"subject_kind"`
	OrganizationID uuid.UUID                          `gorm:"type:uuid;not null" json:"organization_id"`
	SessionID      *uuid.UUID                         `gorm:"type:uuid" json:"session_id,omitempty"`
	EventType      string                             `gorm:"type:varchar(30);not null;index" json:"event_type"`
	IPAddress      string                             `gorm:"type:varchar(45)" json:"ip_address,omitempty"`
	UserAgent      string                             `gorm:"type:text" json:"user_agent,omitempty"`
	Metadata       datatypes.JSONType[map[string]any] `gorm:"type:jsonb" json:"metadata,omitempty"`
	CreatedAt      time.Time                          `gorm:"type:timestamptz;not null;default:now()" json:"created_at"`
}

func (AuthAuditEvent) TableName

func (AuthAuditEvent) TableName() string

type AuthChallenge

type AuthChallenge struct {
	ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`

	JTI    uuid.UUID `gorm:"type:uuid;not null;uniqueIndex"`
	UserID uuid.UUID `gorm:"type:uuid;not null;index"`

	Kind string `gorm:"type:varchar(32);not null"`

	Attempts    int        `gorm:"not null;default:0"`
	MaxAttempts int        `gorm:"not null;default:5"`
	ConsumedAt  *time.Time `gorm:"type:timestamptz"`
	ExpiresAt   time.Time  `gorm:"type:timestamptz;not null;index"`
	CreatedAt   time.Time  `gorm:"type:timestamptz;not null;default:now()"`
}

type AuthFailure

type AuthFailure struct {
	ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`

	Identifier string `gorm:"type:varchar(320);not null;uniqueIndex:idx_auth_failures_identifier_kind"`
	Kind       string `gorm:"type:varchar(32);not null;uniqueIndex:idx_auth_failures_identifier_kind"`
	Count      int    `gorm:"not null;default:0"`

	LockedUntil *time.Time `gorm:"type:timestamptz"`
	WindowStart time.Time  `gorm:"type:timestamptz;not null;default:now()"`

	LastFailedAt time.Time `gorm:"type:timestamptz;not null;default:now();index"`
}

type BeeEnrolmentToken

type BeeEnrolmentToken struct {
	ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`

	// The organization the host is admitted INTO, and the only thing this token
	// resolves. It carries no hive: a host enrols into an organization and joins
	// a fleet afterwards, as a task.
	//
	// That is not a simplification for its own sake — it is the same path a
	// MIGRATING host takes. Moving hv5 from fleet1 to fleet2 already means
	// leaving, existing in no fleet, then joining, so "join a fleet" has to work
	// from the fleetless state regardless. Enrolling straight into that state
	// means a new host and a migrating host follow identical code, instead of a
	// second path that is only exercised on day one.
	OrganizationID uuid.UUID    `gorm:"type:uuid;not null;index" json:"organization_id"`
	Organization   Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"-"`

	TokenHash string `gorm:"type:varchar(64);not null;uniqueIndex" json:"-"`

	// Uses is compared against MaxUses inside the enrolment transaction. Zero
	// MaxUses means unlimited within the expiry.
	Uses    int `gorm:"not null;default:0" json:"uses"`
	MaxUses int `gorm:"not null;default:0" json:"max_uses"`

	ExpiresAt time.Time  `gorm:"type:timestamptz;not null;index" json:"expires_at"`
	RevokedAt *time.Time `gorm:"type:timestamptz" json:"revoked_at,omitempty"`

	// NULL when this system minted the token rather than a person.
	//
	// Onboarding a host automatically means a worker needs a token to hand it,
	// and a worker has no user — there is no service principal in this model and
	// inventing one, or attributing the mint to whoever last logged in, would put
	// a name against an action they did not take. An audit column that is
	// sometimes a lie is worse than one that is sometimes empty.
	//
	// Not-null was what stopped this being wired at all: OnboardDeps.EnrolmentToken
	// was declared, nil-checked and never assigned, so every automated onboarding
	// failed at the enrol step with "no enrolment token source configured".
	CreatedByID *uuid.UUID `gorm:"type:uuid" json:"created_by_id,omitempty"`
	CreatedAt   time.Time  `gorm:"type:timestamptz;not null;default:now()" json:"created_at"`
}

BeeEnrolmentToken admits a host into an organization once, within a window.

Multi-use within a short expiry rather than one token per host: a rack is enrolled in a batch, and minting fifty tokens to bring up fifty hypervisors turns a security control into a reason to script around it. The expiry is what bounds the damage — the same trade kubeadm makes.

Only the hash is stored. A leak of this table must not yield a token that can enrol a machine into the fleet.

func (*BeeEnrolmentToken) Open

func (t *BeeEnrolmentToken) Open(now time.Time) bool

Open reports whether this token can still enrol a host.

type DomainCohortFinding

type DomainCohortFinding struct {
	ID                  uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
	RegistrableDomain   string    `gorm:"type:varchar(253);not null;uniqueIndex:idx_domain_cohort_findings_registrable_domain" json:"registrable_domain"`
	OrganizationsTotal  int64     `gorm:"not null;default:0" json:"organizations_total"`
	OrganizationsProven int64     `gorm:"not null;default:0" json:"organizations_proven"`
	ProvenRatio         float64   `gorm:"type:double precision;not null;default:0" json:"proven_ratio"`
	Flagged             bool      `gorm:"not null;default:false;index" json:"flagged"`
	EvaluatedAt         time.Time `gorm:"type:timestamptz;not null;default:now()" json:"evaluated_at"`
}

func (DomainCohortFinding) TableName

func (DomainCohortFinding) TableName() string

type EffectiveFlag

type EffectiveFlag struct {
	Key     string     `json:"key"`
	Value   bool       `json:"value"`
	Source  FlagSource `json:"source"`
	Scope   FlagScope  `json:"scope"`
	Default bool       `json:"default"`
	Why     string     `json:"why,omitempty"`
}

EffectiveFlag is one resolved answer with the reason attached.

func ResolveFlag

func ResolveFlag(f Flag, platform *PlatformFeatureFlag, org *OrgFeatureFlag) EffectiveFlag

ResolveFlag applies the resolution rule: organization row, then platform row, then the compiled default. FIRST HIT WINS, in either direction — that override is what a flag is for and what a policy deliberately cannot do.

Both pointers are nil-safe so a caller with no organization (the platform surface, or an unauthenticated request) passes nil rather than inventing a zero row that would read as "explicitly false".

type EmailChangeRequest

type EmailChangeRequest struct {
	ID        uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
	UserID    uuid.UUID `gorm:"type:uuid;not null;index"`
	NewEmail  string    `gorm:"type:varchar(255);not null"`
	TokenHash string    `gorm:"type:varchar(64);not null;uniqueIndex"`
	ExpiresAt time.Time `gorm:"type:timestamptz;not null;index"`
	UsedAt    *time.Time
	CreatedAt time.Time `gorm:"type:timestamptz;not null;default:now()"`
}

type EmailVerificationToken

type EmailVerificationToken struct {
	ID        uuid.UUID  `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
	UserID    uuid.UUID  `gorm:"type:uuid;not null;index"`
	Email     string     `gorm:"type:varchar(255);not null"`
	TokenHash string     `gorm:"type:varchar(64);not null;uniqueIndex"`
	ExpiresAt time.Time  `gorm:"type:timestamptz;not null;index"`
	UsedAt    *time.Time `gorm:"type:timestamptz"`
	CreatedAt time.Time  `gorm:"type:timestamptz;not null;default:now()"`
}

EmailVerificationToken proves that whoever registered an account can read the mailbox they registered with.

It is deliberately a separate table from PasswordResetToken rather than a "kind" column on a shared one. The two have different lifetimes, different blast radius when leaked, and — most importantly — different invalidation rules: resetting a password revokes every session, while confirming an email revokes nothing. A shared table invites one path's rule to be applied to the other by whoever touches it next.

Email is stored alongside UserID because the two can diverge. A token issued for alice@old.example and confirmed after the address has been changed must not be read as proof of control over the new one; Confirm compares this column against the user's current address and refuses on a mismatch.

type Flag

type Flag struct {
	Key   string
	Scope FlagScope

	// Default is what applies when NEITHER level has a row.
	//
	// An absent row means "not set, fall through" and never "false". If absence
	// meant off, deleting a row would silently disable a feature, and the way to
	// restore a default would be to know what it used to be.
	Default bool

	// Why is shown beside the flag wherever it is listed. A flag nobody can
	// explain is a flag nobody dares delete.
	Why string

	// Public serves this flag to callers with NO CREDENTIAL AT ALL.
	//
	// It exists for one shape of problem: the sign-in page has to know whether
	// to render the password form, and it has to know that before anybody has
	// signed in. There is no authenticated read that can answer a question asked
	// by an anonymous browser.
	//
	// So this is an explicit, per-flag decision to publish a piece of platform
	// configuration to the internet, and it is opt-in for that reason — the zero
	// value keeps a flag private, so a new flag is never public by forgetting.
	//
	// ONLY MEANINGFUL ON A PLATFORM-SCOPED FLAG, and refused on any other by
	// TestOnlyPlatformScopedFlagsMayBePublic. A tenant-scoped flag resolves
	// differently per organization, and an anonymous caller has no organization
	// — so publishing one would either leak another tenant's configuration or
	// answer with a value that applies to nobody.
	//
	// What it publishes is worth being clear-eyed about: an anonymous caller
	// learns which sign-in methods this installation offers. That is already
	// visible to anyone who loads the page, which is why these two are safe and
	// why a flag whose name gives away something unreleased is not.
	Public bool

	// GuardsSignIn marks a flag that can take away a way of signing in.
	//
	// Turning one off is the only flag change that can lock every operator out
	// of the installation — including the ones who would turn it back on, since
	// turning it back on requires signing in. So a write that disables one is
	// checked against whether any other way in survives, and refused if none
	// does unless the caller says explicitly that they know.
	//
	// A property of the flag rather than a key compared in the service, so the
	// next flag that gates authentication is guarded by declaring it rather than
	// by somebody remembering to add it to a list.
	GuardsSignIn bool
}

Flag is one declared feature flag.

THE REGISTRY DECLARES THE FLAG; THE DATABASE SUPPLIES THE TENANT. A key is never per-organization — "acme.proxmox.provisioning" is the pair (acme, proxmox.provisioning) rendered in dotted form, not a key anybody declared. So adding a tenant adds no flags, and adding a flag is one struct literal that applies to every tenant that exists or ever will.

Declared in code rather than free-form because of how the other choice fails. Writing "legacy_lgoin" to a free-form store succeeds, returns 202, and leaves legacy_login exactly as it was — the operator believes they turned it off and nothing anywhere reports a problem. An unknown key here is refused at the write, in the same spirit as every other unknown input in this system.

func AllFlags

func AllFlags() []Flag

AllFlags returns every declared flag, ordered by key so that a listing, a diff and a screen all agree on the order.

func LookupFlag

func LookupFlag(key string) (Flag, bool)

LookupFlag returns the declared flag, or false for an unregistered key.

func PublicFlags

func PublicFlags() []Flag

PublicFlags returns the flags an unauthenticated caller may read.

Built by FILTERING the registry rather than by keeping a second list. Two lists disagree eventually, and the direction this one would drift is a flag being served publicly after its Public bit was removed.

type FlagScope

type FlagScope string

FlagScope says at which levels a flag may carry a stored row.

const (
	// ScopePlatform: one row for the whole installation. A per-organization row
	// is refused — there is no such thing as one tenant's legacy login page.
	ScopePlatform FlagScope = "platform"

	// ScopeTenant: a platform row acts as the default for every organization,
	// and an organization row overrides it for that one.
	ScopeTenant FlagScope = "tenant"
)

type FlagSource

type FlagSource string

FlagSource says which level decided an effective value, so a listing can show "on, because the platform says so" apart from "on, for this tenant only".

const (
	FlagSourceDefault      FlagSource = "default"
	FlagSourcePlatform     FlagSource = "platform"
	FlagSourceOrganization FlagSource = "organization"
)

type ImpersonationGrant

type ImpersonationGrant struct {
	ID             uuid.UUID  `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
	TargetUserID   uuid.UUID  `gorm:"type:uuid;not null;index" json:"target_user_id"`
	OrganizationID uuid.UUID  `gorm:"type:uuid;not null" json:"organization_id"`
	GrantedByID    uuid.UUID  `gorm:"type:uuid;not null" json:"granted_by_id"`
	Reason         string     `gorm:"type:text" json:"reason,omitempty"`
	ExpiresAt      time.Time  `gorm:"type:timestamptz;not null" json:"expires_at"`
	RevokedAt      *time.Time `gorm:"type:timestamptz" json:"revoked_at,omitempty"`
	CreatedAt      time.Time  `gorm:"type:timestamptz;not null;default:now()" json:"created_at"`
}

func (ImpersonationGrant) TableName

func (ImpersonationGrant) TableName() string

type Invitation

type Invitation struct {
	ID             uuid.UUID    `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
	OrganizationID uuid.UUID    `gorm:"type:uuid;not null;index:idx_invitations_org_email,priority:1" json:"organization_id"`
	Organization   Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"-"`

	// InvitedByID is kept even after the inviter leaves the organization: the
	// question "who let this account in" has to remain answerable, and a
	// membership row that outlives its provenance cannot answer it.
	InvitedByID uuid.UUID `gorm:"type:uuid;not null;index" json:"invited_by_id"`

	// Email is lowercased on the way in. It is compared against the account that
	// redeems the token, so an invitation for one address can never be spent by
	// another even if the link is forwarded.
	Email string     `gorm:"type:varchar(255);not null;index:idx_invitations_org_email,priority:2" json:"email"`
	Role  MemberRole `gorm:"type:varchar(20);not null;default:'member'" json:"role"`

	TokenHash  string     `gorm:"type:varchar(64);not null;uniqueIndex" json:"-"`
	ExpiresAt  time.Time  `gorm:"type:timestamptz;not null;index" json:"expires_at"`
	AcceptedAt *time.Time `gorm:"type:timestamptz" json:"accepted_at,omitempty"`
	RevokedAt  *time.Time `gorm:"type:timestamptz" json:"revoked_at,omitempty"`
	CreatedAt  time.Time  `gorm:"type:timestamptz;not null;default:now()" json:"created_at"`
}

Invitation is an offer of membership in an organization, redeemable by whoever can read the mailbox it was sent to.

The token is the whole authorization: accepting is an unauthenticated operation by design, because the invitee frequently has no account yet and cannot be asked to sign in first. That makes possession of the emailed token the only thing standing between a stranger and a membership, which is why only its hash is stored, why it expires, and why Email is bound into the row rather than taken from whoever presents it.

AcceptedAt and RevokedAt are separate nullable stamps rather than a status enum: both are facts with a time, an invitation can only ever move into one of them, and a partial index on "still open" reads naturally from the two.

func (*Invitation) Open

func (i *Invitation) Open(now time.Time) bool

Open reports whether this invitation can still be redeemed.

type MemberRole

type MemberRole string
const (
	MemberRoleOwner  MemberRole = "owner"
	MemberRoleAdmin  MemberRole = "admin"
	MemberRoleMember MemberRole = "member"
)

type Membership

type Membership struct {
	ID             uuid.UUID    `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
	UserID         uuid.UUID    `gorm:"index;not null" json:"user_id"`
	User           User         `gorm:"foreignKey:UserID" json:"-"`
	OrganizationID uuid.UUID    `gorm:"index;not null" json:"org_id"`
	Organization   Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"-"`
	Role           MemberRole   `gorm:"not null;default:'member'" json:"role"`
	SupportEnabled bool         `gorm:"default:false" json:"support_enabled"`
	CreatedAt      time.Time    `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()"`
	UpdatedAt      time.Time    `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()"`
}

type OAuthState

type OAuthState struct {
	ID         uuid.UUID  `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
	StateHash  string     `gorm:"type:varchar(64);not null;uniqueIndex"`
	Provider   string     `gorm:"type:varchar(32);not null"`
	Mode       string     `gorm:"type:varchar(32);not null;default:''"`
	Origin     string     `gorm:"type:text;not null;default:''"`
	CookieHash string     `gorm:"type:varchar(64);not null"`
	ExpiresAt  time.Time  `gorm:"type:timestamptz;not null;index"`
	ConsumedAt *time.Time `gorm:"type:timestamptz"`
	CreatedAt  time.Time  `gorm:"type:timestamptz;not null;default:now()"`
}

func (OAuthState) TableName

func (OAuthState) TableName() string

type OrgFeatureFlag

type OrgFeatureFlag struct {
	ID             uuid.UUID    `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
	OrganizationID uuid.UUID    `gorm:"type:uuid;not null;uniqueIndex:idx_org_feature_flags_org_key" json:"organization_id"`
	Organization   Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"-"`
	Key            string       `gorm:"not null;type:varchar(120);uniqueIndex:idx_org_feature_flags_org_key" json:"key"`
	Value          bool         `gorm:"not null" json:"value"`
	UpdatedBy      uuid.UUID    `gorm:"type:uuid;not null" json:"updated_by"`
	CreatedAt      time.Time    `gorm:"type:timestamptz;not null;default:now()" json:"created_at"`
	UpdatedAt      time.Time    `gorm:"type:timestamptz;autoUpdateTime;not null;default:now()" json:"updated_at"`
}

OrgFeatureFlag is one organization's override of a tenant-scoped flag.

type OrgPolicy

type OrgPolicy struct {
	ID             uuid.UUID                        `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
	OrganizationID uuid.UUID                        `gorm:"type:uuid;not null;uniqueIndex:idx_org_policies_org_kind" json:"organization_id"`
	Organization   Organization                     `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"-"`
	Kind           PolicyKind                       `gorm:"not null;type:varchar(50);uniqueIndex:idx_org_policies_org_kind" json:"kind"`
	Enabled        bool                             `gorm:"not null;default:false" json:"enabled"`
	Config         datatypes.JSONType[PolicyConfig] `gorm:"type:jsonb;not null;default:'{}'" json:"config"`
	CreatedBy      uuid.UUID                        `gorm:"type:uuid;not null" json:"created_by"`
	CreatedAt      time.Time                        `gorm:"type:timestamptz;column:created_at;not null;default:now()" json:"created_at"`
	UpdatedAt      time.Time                        `gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()" json:"updated_at"`
}

type Organization

type Organization struct {
	ID                  uuid.UUID  `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
	Name                string     `gorm:"not null" json:"name"`
	Domain              *string    `gorm:"index" json:"domain"`
	DomainVerifiedAt    *time.Time `gorm:"column:domain_verified_at" json:"domain_verified_at,omitempty"`
	Status              string     `gorm:"not null;default:'pending'" json:"status"`
	ConnectionString    string     `gorm:"not null;default:''" json:"-"`
	EncryptedTenantKey  string     `gorm:"not null;default:''" json:"-"`
	TenantKeyIV         string     `gorm:"not null;default:''" json:"-"`
	TenantKeyTag        string     `gorm:"not null;default:''" json:"-"`
	ConnectionStringIV  string     `gorm:"not null;default:''" json:"-"`
	ConnectionStringTag string     `gorm:"not null;default:''" json:"-"`
	RegistrableDomain   *string    `gorm:"type:varchar(253);column:registrable_domain;index:idx_organizations_registrable_domain_proven,priority:1" json:"-"`
	ProvenAt            *time.Time `gorm:"type:timestamptz;column:proven_at;index:idx_organizations_registrable_domain_proven,priority:2" json:"-"`
	ProvingDaysSeen     int        `gorm:"column:proving_days_seen;not null;default:0" json:"-"`
	ProvingLastDay      *time.Time `gorm:"type:date;column:proving_last_day" json:"-"`

	// DeletedAt soft-deletes the organization, and soft is the only safe kind
	// here.
	//
	// This row holds EncryptedTenantKey and the sealed ConnectionString, which
	// are the ONLY things that can decrypt the tenant database. Deleting the row
	// destroys the customer's data whether or not the database is dropped: what
	// survives is ciphertext under a key that no longer exists anywhere. A hard
	// delete is therefore not "remove the record", it is "destroy everything,
	// irreversibly, from one API call".
	//
	// So deletion marks the row and stops it resolving. Purging the key material
	// and the database is a separate, deliberate operator step, which is what
	// gives a retention window and a way back from a mistaken deletion.
	DeletedAt *time.Time `gorm:"column:deleted_at;index" json:"deleted_at,omitempty"`

	CreatedAt time.Time `gorm:"column:created_at;not null;default:now()" json:"created_at"`
	UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at;not null;default:now()" json:"updated_at"`
}

func (*Organization) DecryptConnectionString added in v0.1.5

func (o *Organization) DecryptConnectionString(dsnKey []byte) (string, error)

DecryptConnectionString opens the org's sealed tenant DSN with the tenant-DSN key. The org id is the AAD, so a sealed DSN copied onto another org's row does not open.

The one implementation of the rule, so every caller treats a legacy plaintext DSN the same way: ErrConnectionStringNotEncrypted, never a base64 error from decoding a postgres:// URL.

type PasswordResetToken

type PasswordResetToken struct {
	ID        uuid.UUID  `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
	UserID    uuid.UUID  `gorm:"type:uuid;not null;index"`
	TokenHash string     `gorm:"type:varchar(64);not null;uniqueIndex"`
	ExpiresAt time.Time  `gorm:"type:timestamptz;not null"`
	UsedAt    *time.Time `gorm:"type:timestamptz"`
	CreatedAt time.Time  `gorm:"type:timestamptz;not null;default:now()"`
}

type PlatformFeatureFlag

type PlatformFeatureFlag struct {
	ID        uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
	Key       string    `gorm:"not null;type:varchar(120);uniqueIndex" json:"key"`
	Value     bool      `gorm:"not null" json:"value"`
	UpdatedBy uuid.UUID `gorm:"type:uuid;not null" json:"updated_by"`
	CreatedAt time.Time `gorm:"type:timestamptz;not null;default:now()" json:"created_at"`
	UpdatedAt time.Time `gorm:"type:timestamptz;autoUpdateTime;not null;default:now()" json:"updated_at"`
}

PlatformFeatureFlag is the installation-wide value for one key.

Separate table rather than a nullable organization_id on one, mirroring PlatformPolicy and OrgPolicy. A nullable column would need a unique index over (key, organization_id) where Postgres treats NULLs as distinct — so two platform rows for the same key would both be accepted — and the partial indexes that fix it cannot be expressed in GORM tags, which means Atlas would diff them away on the next generated migration.

type PlatformPolicy

type PlatformPolicy struct {
	ID        uuid.UUID                        `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
	Kind      PolicyKind                       `gorm:"not null;type:varchar(50);uniqueIndex" json:"kind"`
	Enabled   bool                             `gorm:"not null;default:false" json:"enabled"`
	Config    datatypes.JSONType[PolicyConfig] `gorm:"type:jsonb;not null;default:'{}'" json:"config"`
	CreatedBy uuid.UUID                        `gorm:"type:uuid;not null" json:"created_by"`
	CreatedAt time.Time                        `gorm:"type:timestamptz;column:created_at;not null;default:now()" json:"created_at"`
	UpdatedAt time.Time                        `gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()" json:"updated_at"`
}

type PolicyConfig

type PolicyConfig struct {
	MinLength          int  `json:"min_length,omitempty"`
	RequireUppercase   bool `json:"require_uppercase,omitempty"`
	RequireLowercase   bool `json:"require_lowercase,omitempty"`
	RequireDigit       bool `json:"require_digit,omitempty"`
	RequireSpecial     bool `json:"require_special,omitempty"`
	MaxLifetimeMinutes int  `json:"max_lifetime_minutes,omitempty"`
}

type PolicyKind

type PolicyKind string
const (
	PolicyKindRequireMFA         PolicyKind = "require_mfa"
	PolicyKindRequirePasskey     PolicyKind = "require_passkey"
	PolicyKindPasswordMinLength  PolicyKind = "password_min_length"
	PolicyKindPasswordComplexity PolicyKind = "password_complexity"
	PolicyKindSessionMaxLifetime PolicyKind = "session_max_lifetime"
)

func (PolicyKind) IsCheckable

func (k PolicyKind) IsCheckable() bool

func (PolicyKind) IsValid

func (k PolicyKind) IsValid() bool

type TokenSession

type TokenSession struct {
	ID               uuid.UUID  `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
	SubjectID        uuid.UUID  `gorm:"type:uuid;not null;index:idx_token_sessions_subject,composite:subject" json:"subject_id"`
	SubjectKind      string     `gorm:"type:varchar(10);not null;index:idx_token_sessions_subject,composite:subject" json:"subject_kind"`
	OrganizationID   uuid.UUID  `gorm:"type:uuid;not null;index" json:"organization_id"`
	FamilyID         uuid.UUID  `gorm:"type:uuid;not null;index" json:"family_id"`
	AccessTokenJTI   string     `gorm:"type:varchar(36);uniqueIndex;not null" json:"-"`
	RefreshTokenJTI  string     `gorm:"type:varchar(36);uniqueIndex;not null" json:"-"`
	AccessTokenHash  string     `gorm:"type:varchar(64);not null" json:"-"`
	RefreshTokenHash string     `gorm:"type:varchar(64);not null" json:"-"`
	ActorID          *uuid.UUID `gorm:"type:uuid" json:"actor_id,omitempty"`
	AMR              string     `gorm:"type:varchar(100)" json:"amr,omitempty"`
	IPAddress        string     `gorm:"type:varchar(45)" json:"ip_address,omitempty"`
	UserAgent        string     `gorm:"type:text" json:"user_agent,omitempty"`
	AccessExpiresAt  time.Time  `gorm:"type:timestamptz;not null" json:"access_expires_at"`
	RefreshExpiresAt time.Time  `gorm:"type:timestamptz;not null" json:"refresh_expires_at"`
	RevokedAt        *time.Time `gorm:"type:timestamptz" json:"revoked_at,omitempty"`
	CreatedAt        time.Time  `gorm:"type:timestamptz;not null;default:now()" json:"created_at"`
}

func (TokenSession) TableName

func (TokenSession) TableName() string

type User

type User struct {
	ID         uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
	Email      string    `gorm:"uniqueIndex;not null;type:varchar(255)" json:"email"`
	Password   string    `gorm:"not null;type:varchar(255)" json:"-"`
	FirstName  string    `gorm:"not null;type:varchar(100)" json:"first_name"`
	LastName   string    `gorm:"not null;type:varchar(100)" json:"last_name"`
	Role       UserRole  `gorm:"not null;default:'platform_customer';type:varchar(20)" json:"role"`
	MFAEnabled bool      `gorm:"default:false" json:"mfa_enabled"`
	// EmailVerifiedAt records when this user proved control of Email. It is a
	// timestamp rather than a bool to match ProvenAt and DomainVerifiedAt, and
	// because "when" is what an audit of a disputed registration needs.
	//
	// It lives on User, not on Account: Account is the social-identity table and
	// a password registration has no row there at all, so Account.EmailVerified
	// can only ever speak for an address an IdP asserted.
	EmailVerifiedAt *time.Time `gorm:"type:timestamptz;column:email_verified_at" json:"email_verified_at,omitempty"`
	IsActive        bool       `gorm:"default:true" json:"is_active"`

	// DeletedAt soft-deletes the account.
	//
	// The row is kept because other records point at it and must stay readable:
	// audit events name the actor, fleet tasks name who created them, and
	// memberships record who invited whom. Removing the row would either break
	// those references or cascade them away — and an audit trail that disappears
	// when the account does is an audit trail that can be erased by the person
	// being audited.
	//
	// Distinct from IsActive, which is a suspension: reversible, and the account
	// still exists. This is the account being gone.
	DeletedAt       *time.Time `gorm:"column:deleted_at;index" json:"deleted_at,omitempty"`
	TokensRevokedAt *time.Time `gorm:"type:timestamptz" json:"-"`
	LastLoginAt     *time.Time `gorm:"autoUpdateTime;column:last_login_at;not null;default:now()" json:"last_login_at,omitempty"`
	CreatedAt       time.Time  `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()"`
	UpdatedAt       time.Time  `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()"`
}

func (*User) CheckPassword

func (u *User) CheckPassword(password string) bool

func (*User) EmailVerified

func (u *User) EmailVerified() bool

EmailVerified reports whether this user has proven control of their address.

Callers ask through this method rather than comparing the pointer themselves, so that the meaning of "verified" has one definition. A nil check written out at each call site is how one path ends up treating a legacy NULL as verified.

func (*User) FullName

func (u *User) FullName() string

func (*User) SetPassword

func (u *User) SetPassword(password string) error

type UserPasskey

type UserPasskey struct {
	ID              uuid.UUID                    `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
	UserID          uuid.UUID                    `gorm:"type:uuid;index;not null" json:"user_id"`
	CredentialID    string                       `gorm:"uniqueIndex;not null;type:text" json:"credential_id"`
	PublicKey       []byte                       `gorm:"not null;type:bytea" json:"-"`
	AttestationType string                       `gorm:"not null;type:varchar(64)" json:"attestation_type"`
	AAGUID          string                       `gorm:"type:varchar(36)" json:"aaguid"`
	SignCount       uint32                       `gorm:"not null;default:0" json:"sign_count"`
	BackupEligible  bool                         `gorm:"not null;default:false" json:"backup_eligible"`
	BackupState     bool                         `gorm:"not null;default:false" json:"backup_state"`
	Name            string                       `gorm:"not null;type:varchar(255)" json:"name"`
	Transports      datatypes.JSONType[[]string] `gorm:"type:jsonb" json:"transports"`
	LastUsedAt      *time.Time                   `json:"last_used_at,omitempty"`
	CreatedAt       time.Time                    `json:"created_at" gorm:"type:timestamptz;not null;default:now()"`
}

type UserRole

type UserRole string
const (
	// RolePlatformAdmin operates the platform. Everything, including policy that
	// binds every tenant.
	RolePlatformAdmin UserRole = "platform_admin"

	// RolePlatformSupport may impersonate a member who has consented, and that
	// is its whole standing capability — it reaches a tenant's resources AS that
	// member, never by holding a grant over them.
	RolePlatformSupport UserRole = "platform_support"

	// RolePlatformSales carries nothing today.
	RolePlatformSales UserRole = "platform_sales"

	// RolePlatformCustomer is everyone else: tenant-bound operations only,
	// authorized entirely through membership.
	RolePlatformCustomer UserRole = "platform_customer"
)

PLATFORM roles, held on the user record. This is one of two independent ladders and they are not interchangeable:

platform (here)          who you are to the platform
tenant (MemberRole)      what you may do inside one organization

Both used to contain a bare "admin", in code and in the database, which made the two indistinguishable at a glance and invited exactly the mistake of answering "may they drain this hypervisor" with the wrong ladder. The prefix is on the VALUE as well as the identifier so that a row, a log line and an API payload each say which ladder they mean.

type UserTOTP

type UserTOTP struct {
	ID           uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
	UserID       uuid.UUID `gorm:"type:uuid;uniqueIndex;not null" json:"user_id"`
	Secret       string    `gorm:"not null;type:text" json:"-"`
	Verified     bool      `gorm:"default:false" json:"verified"`
	LastUsedStep int64     `gorm:"not null;default:0" json:"-"`
	CreatedAt    time.Time `json:"created_at" gorm:"type:timestamptz;not null;default:now()"`
	UpdatedAt    time.Time `json:"updated_at" gorm:"type:timestamptz;autoUpdateTime;not null;default:now()"`
}

Jump to

Keyboard shortcuts

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