Documentation
¶
Index ¶
- Constants
- Variables
- func APIKeyScopeIsValid(s string) bool
- type APIKey
- type Account
- type AuthAccessRule
- type AuthAuditEvent
- type AuthChallenge
- type AuthFailure
- type BeeEnrolmentToken
- type DomainCohortFinding
- type EffectiveFlag
- type EmailChangeRequest
- type EmailVerificationToken
- type Flag
- type FlagScope
- type FlagSource
- type ImpersonationGrant
- type Invitation
- type MemberRole
- type Membership
- type OAuthState
- type OrgFeatureFlag
- type OrgPolicy
- type Organization
- type PasswordResetToken
- type PlatformFeatureFlag
- type PlatformPolicy
- type PolicyConfig
- type PolicyKind
- type TokenSession
- type User
- type UserPasskey
- type UserRole
- type UserTOTP
Constants ¶
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.
const ( AccessRuleKindEmail = "email" AccessRuleKindDomain = "domain" )
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" )
const ( AccessRulePolicyAllow = "allow" AccessRulePolicyDeny = "deny" )
Variables ¶
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.
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 ¶
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 ¶
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.
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.
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 ¶
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.
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 (*User) EmailVerified ¶
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) SetPassword ¶
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()"`
}
Source Files
¶
- account.go
- api_key.go
- auth_access_rule.go
- auth_audit_event.go
- auth_challenge.go
- domain_cohort_finding.go
- email_change_request.go
- email_verification_token.go
- feature_flag.go
- fleet.go
- impersonation_grant.go
- invitation.go
- membership.go
- oauth_state.go
- organization.go
- organization_dsn.go
- password_reset_token.go
- policy.go
- token_session.go
- user.go
- user_passkey.go
- user_totp.go