Documentation
¶
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func IsNotFound ¶
IsNotFound reports whether err means "the row does not exist", as opposed to "the query failed". Use it to keep those two apart:
org, err := p.StorageProvider.GetOrganizationByID(ctx, id)
switch {
case storage.IsNotFound(err):
return nil, NotFound("organization not found") // 404, permanent
case err != nil:
return nil, Internal("storage unavailable") // 500, retryable
}
Why this matters: without it every caller collapses to `if err != nil` and reports a database outage as though the caller's input were wrong. A user clicking a perfectly valid verification link during a brief outage was told "invalid verification token" — a permanent, non-retryable answer to a transient condition, and in auth paths that ambiguity is a security concern as much as a UX one.
It is a predicate rather than a single shared sentinel for two reasons. Each backend reports absence with its own driver value (gorm.ErrRecordNotFound, mongo.ErrNoDocuments, gocql.ErrNotFound, gocb.ErrDocumentNotFound), and errors.Is cannot match those against a foreign sentinel. And a sentinel declared here could not be wrapped by the backends anyway — this package imports all of them, so the dependency only runs one way. The shape follows k8s.io/apimachinery's apierrors.IsNotFound for the same reasons.
Backends that have no canonical driver sentinel (ArangoDB, DynamoDB) declare their own and wrap it; see their errors.go.
A nil error is never "not found".
Types ¶
type Dependencies ¶
Dependencies carries shared resources for constructing a storage Provider.
type Provider ¶
type Provider interface {
// AddUser to save user information in database
AddUser(ctx context.Context, user *schemas.User) (*schemas.User, error)
// UpdateUser to update user information in database
UpdateUser(ctx context.Context, user *schemas.User) (*schemas.User, error)
// DeleteUser to delete user information from database
DeleteUser(ctx context.Context, user *schemas.User) error
// ListUsers to get list of users from database. query is an optional
// case-insensitive substring filter matched against email, given_name,
// family_name and nickname; empty means no filter.
ListUsers(ctx context.Context, pagination *model.Pagination, query string) ([]*schemas.User, *model.Pagination, error)
// GetUserByEmail to get user information from database using email address
GetUserByEmail(ctx context.Context, email string) (*schemas.User, error)
// GetUserByPhoneNumber to get user information from database using phone number
GetUserByPhoneNumber(ctx context.Context, phoneNumber string) (*schemas.User, error)
// GetUserByID to get user information from database using user ID
GetUserByID(ctx context.Context, id string) (*schemas.User, error)
// GetUserByExternalID fetches an IdP-provisioned user by its org-namespaced
// external id. The lookup key is composed as "<orgID>:<externalID>" so one
// org's SCIM/SSO connection can never resolve another org's user by external
// id (design §4.4 H6). Provisioning stores User.ExternalID in the same
// namespaced form.
GetUserByExternalID(ctx context.Context, orgID, externalID string) (*schemas.User, error)
// UpdateUsers to update multiple users, identified by the ids slice.
// If ids is nil / empty NO update is performed: global updates are disabled,
// so implementations return an error rather than silently updating every
// user (SQL: gorm.ErrMissingWhereClause; all other backends:
// schemas.ErrUpdateUsersEmptyIDs).
UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error
// AddVerificationRequest to save verification request in database
AddVerificationRequest(ctx context.Context, verificationRequest *schemas.VerificationRequest) (*schemas.VerificationRequest, error)
// GetVerificationRequestByToken to get verification request from database using token
GetVerificationRequestByToken(ctx context.Context, token string) (*schemas.VerificationRequest, error)
// GetVerificationRequestByEmail to get verification request by email from database
GetVerificationRequestByEmail(ctx context.Context, email string, identifier string) (*schemas.VerificationRequest, error)
// ListVerificationRequests to get list of verification requests from database
ListVerificationRequests(ctx context.Context, pagination *model.Pagination) ([]*schemas.VerificationRequest, *model.Pagination, error)
// DeleteVerificationRequest to delete verification request from database
DeleteVerificationRequest(ctx context.Context, verificationRequest *schemas.VerificationRequest) error
// AddSession to save session information in database
AddSession(ctx context.Context, session *schemas.Session) error
// DeleteSession to delete session information from database
DeleteSession(ctx context.Context, userId string) error
// AddWebhook to add webhook
AddWebhook(ctx context.Context, webhook *schemas.Webhook) (*schemas.Webhook, error)
// UpdateWebhook to update webhook
UpdateWebhook(ctx context.Context, webhook *schemas.Webhook) (*schemas.Webhook, error)
// ListWebhook to list webhook
ListWebhook(ctx context.Context, pagination *model.Pagination) ([]*schemas.Webhook, *model.Pagination, error)
// GetWebhookByID to get webhook by id
GetWebhookByID(ctx context.Context, webhookID string) (*schemas.Webhook, error)
// GetWebhookByEventName to get webhook by event_name
GetWebhookByEventName(ctx context.Context, eventName string) ([]*schemas.Webhook, error)
// DeleteWebhook to delete webhook
DeleteWebhook(ctx context.Context, webhook *schemas.Webhook) error
// AddWebhookLog to add webhook log
AddWebhookLog(ctx context.Context, webhookLog *schemas.WebhookLog) (*schemas.WebhookLog, error)
// ListWebhookLogs to list webhook logs
ListWebhookLogs(ctx context.Context, pagination *model.Pagination, webhookID string) ([]*schemas.WebhookLog, *model.Pagination, error)
// AddEmailTemplate to add EmailTemplate
AddEmailTemplate(ctx context.Context, emailTemplate *schemas.EmailTemplate) (*schemas.EmailTemplate, error)
// UpdateEmailTemplate to update EmailTemplate
UpdateEmailTemplate(ctx context.Context, emailTemplate *schemas.EmailTemplate) (*schemas.EmailTemplate, error)
// ListEmailTemplate to list EmailTemplate
ListEmailTemplate(ctx context.Context, pagination *model.Pagination) ([]*schemas.EmailTemplate, *model.Pagination, error)
// GetEmailTemplateByID to get EmailTemplate by id
GetEmailTemplateByID(ctx context.Context, emailTemplateID string) (*schemas.EmailTemplate, error)
// GetEmailTemplateByEventName to get EmailTemplate by event_name
GetEmailTemplateByEventName(ctx context.Context, eventName string) (*schemas.EmailTemplate, error)
// DeleteEmailTemplate to delete EmailTemplate
DeleteEmailTemplate(ctx context.Context, emailTemplate *schemas.EmailTemplate) error
// UpsertOTP to add or update otp
UpsertOTP(ctx context.Context, otp *schemas.OTP) (*schemas.OTP, error)
// GetOTPByEmail to get otp for a given email address
GetOTPByEmail(ctx context.Context, emailAddress string) (*schemas.OTP, error)
// GetOTPByPhoneNumber to get otp for a given phone number
GetOTPByPhoneNumber(ctx context.Context, phoneNumber string) (*schemas.OTP, error)
// DeleteOTP to delete otp
DeleteOTP(ctx context.Context, otp *schemas.OTP) error
// AddAuthenticator adds a new authenticator document to the database.
// If the authenticator doesn't have an ID, a new one is generated.
// The created document is returned, or an error if the operation fails.
AddAuthenticator(ctx context.Context, totp *schemas.Authenticator) (*schemas.Authenticator, error)
// UpdateAuthenticator updates an existing authenticator document in the database.
// The updated document is returned, or an error if the operation fails.
UpdateAuthenticator(ctx context.Context, totp *schemas.Authenticator) (*schemas.Authenticator, error)
// UpdateAuthenticatorSecretAndVerifiedAt writes ONLY the secret,
// verified_at and updated_at columns of an authenticator row. It exists so
// that marking an authenticator verified — or re-encrypting a legacy
// plaintext secret in place — cannot touch recovery_codes.
//
// UpdateAuthenticator writes the whole row from the caller's struct, so a
// caller that read the row, spent time validating a passcode, and then
// wrote it back would restore the recovery-code blob as it was at read
// time — resurrecting a code that a concurrent redemption had consumed in
// between, and silently undoing the single-use guarantee
// ConsumeAuthenticatorRecoveryCode exists to provide.
//
// With this method the two writers to an authenticator row touch DISJOINT
// columns: this one owns secret/verified_at, ConsumeAuthenticatorRecoveryCode
// owns recovery_codes. They therefore commute — neither ordering loses the
// other's write — which is the property that makes the row safe without a
// transaction. Keep them disjoint; widening either one re-opens the race.
//
// A row that no longer exists is a no-op, not an error: this write is
// bookkeeping that follows an already-successful validation, and it must
// never turn a completed login into a failure. Implementations MUST NOT
// upsert — backends whose UPDATE creates a row when none matched need an
// explicit existence condition, or an admin MFA reset landing mid-flight
// leaves a resurrected ghost authenticator behind.
UpdateAuthenticatorSecretAndVerifiedAt(ctx context.Context, id, secret string, verifiedAt int64) error
// ConsumeAuthenticatorRecoveryCode replaces the recovery-code blob of the
// authenticator row with the given ID, but only while the row still holds
// oldCodes. The bool reports whether THIS call performed the write, and it
// MUST be decided by a single atomic database operation — never by a
// separate read followed by an unconditional update.
//
// This is the single-use primitive behind TOTP recovery codes. The caller
// reads the blob, marks one code consumed, and offers the before/after pair
// here; if anything changed the blob in between, the write is refused and
// the caller re-reads. A read-then-write implementation lets two concurrent
// redemptions of the SAME recovery code both observe it unconsumed and both
// succeed, so one code authenticates any number of times — which is the
// whole property a recovery code is supposed to have.
//
// oldCodes MUST be the exact string read from the row, byte for byte, never
// a re-marshalled map: re-encoding can reorder keys or change spacing and
// then the comparison matches nothing and every redemption fails.
//
// A refused write is not an error — it returns (false, nil), and that
// includes the case where the row no longer exists.
//
// FAULT TOLERANCE: on error the bool is always false. Callers MUST check the
// error first and treat it as "claim outcome unknown", never as "not
// consumed" — reporting a database outage as an invalid recovery code
// burns the user's credential for nothing.
ConsumeAuthenticatorRecoveryCode(ctx context.Context, id, oldCodes, newCodes string) (bool, error)
// GetAuthenticatorDetailsByUserId retrieves details of an authenticator document based on user ID and authenticator type.
// If found, the authenticator document is returned, or an error if not found or an error occurs during the retrieval.
GetAuthenticatorDetailsByUserId(ctx context.Context, userId string, authenticatorType string) (*schemas.Authenticator, error)
// DeleteAuthenticatorsByUserID removes every authenticator row (TOTP,
// email OTP, SMS OTP) for a user. Used by admin MFA reset.
DeleteAuthenticatorsByUserID(ctx context.Context, userID string) error
// Session Token methods (for database-backed memory store)
// AddSessionToken adds a session token to the database
AddSessionToken(ctx context.Context, token *schemas.SessionToken) error
// GetSessionTokenByUserIDAndKey retrieves a session token by user ID and key
GetSessionTokenByUserIDAndKey(ctx context.Context, userId, key string) (*schemas.SessionToken, error)
// DeleteSessionToken deletes a session token by ID
DeleteSessionToken(ctx context.Context, id string) error
// DeleteSessionTokenByUserIDAndKey deletes a session token by user ID and
// key. The bool reports whether THIS call removed the row, and it MUST be
// decided by a single atomic database operation — never by a separate
// existence check followed by a delete.
//
// This is the single-use primitive behind refresh-token rotation (OAuth 2.1
// §6.1): the token endpoint claims the presented refresh token before
// issuing its replacement, so under concurrent redemption of the same token
// exactly one caller may observe true. A read-then-delete implementation
// lets every racer mint an independent token family, which defeats rotation
// and its reuse detection. Deleting an absent key is not an error — it
// returns (false, nil).
//
// FAULT TOLERANCE: on error the bool is always false, even where a multi-row
// implementation already removed one row before failing. Fail-safe direction —
// callers MUST check the error first and treat an error as "claim outcome
// unknown", never as "not claimed" (TokenHandler answers 503, not
// invalid_grant, for exactly that reason).
DeleteSessionTokenByUserIDAndKey(ctx context.Context, userId, key string) (bool, error)
// DeleteAllSessionTokensByUserID deletes all session tokens for a user ID
DeleteAllSessionTokensByUserID(ctx context.Context, userId string) error
// DeleteSessionTokensByNamespace deletes all session tokens for a namespace (e.g., "auth_provider")
DeleteSessionTokensByNamespace(ctx context.Context, namespace string) error
// CleanExpiredSessionTokens removes expired session tokens from the database
CleanExpiredSessionTokens(ctx context.Context) error
// GetAllSessionTokens retrieves all session tokens (for testing)
GetAllSessionTokens(ctx context.Context) ([]*schemas.SessionToken, error)
// MFA Session methods (for database-backed memory store)
// AddMFASession adds an MFA session to the database
AddMFASession(ctx context.Context, session *schemas.MFASession) error
// GetMFASessionByUserIDAndKey retrieves an MFA session by user ID and key
GetMFASessionByUserIDAndKey(ctx context.Context, userId, key string) (*schemas.MFASession, error)
// DeleteMFASession deletes an MFA session by ID
DeleteMFASession(ctx context.Context, id string) error
// DeleteMFASessionByUserIDAndKey deletes an MFA session by user ID and key
DeleteMFASessionByUserIDAndKey(ctx context.Context, userId, key string) error
// GetAllMFASessionsByUserID retrieves all MFA sessions for a user ID
GetAllMFASessionsByUserID(ctx context.Context, userId string) ([]*schemas.MFASession, error)
// CleanExpiredMFASessions removes expired MFA sessions from the database
CleanExpiredMFASessions(ctx context.Context) error
// GetAllMFASessions retrieves all MFA sessions (for testing)
GetAllMFASessions(ctx context.Context) ([]*schemas.MFASession, error)
// OAuth State methods (for database-backed memory store)
// AddOAuthState adds an OAuth state to the database
AddOAuthState(ctx context.Context, state *schemas.OAuthState) error
// GetOAuthStateByKey retrieves an OAuth state by key
GetOAuthStateByKey(ctx context.Context, key string) (*schemas.OAuthState, error)
// DeleteOAuthStateByKey deletes an OAuth state by key. The bool reports
// whether THIS call removed the row, and it MUST be decided by a single
// atomic database operation (row-level DELETE, LWT, or conditional write) —
// never by a separate existence check followed by a delete.
//
// This is the single-use primitive behind authorization codes (RFC 6749
// §4.1.2) and the SSO broker's `state`: under concurrent redemption of the
// same code exactly one caller may observe true. A read-then-delete
// implementation hands the same code to every racer, which is an
// authorization-code replay. Deleting an absent key is not an error — it
// returns (false, nil).
//
// FAULT TOLERANCE: on error the bool is always false, even where a multi-row
// implementation already removed one row before failing. That is the
// fail-safe direction — a caller that ignored the error would decline to
// proceed rather than proceed twice. Callers MUST check the error before
// trusting the bool, and MUST treat an error as "claim outcome unknown", not
// as "not claimed".
DeleteOAuthStateByKey(ctx context.Context, key string) (bool, error)
// GetAllOAuthStates retrieves all OAuth states (for testing)
GetAllOAuthStates(ctx context.Context) ([]*schemas.OAuthState, error)
// Audit Log methods
// AddAuditLog adds an audit log entry
AddAuditLog(ctx context.Context, log *schemas.AuditLog) error
// ListAuditLogs queries audit logs with filters and pagination
ListAuditLogs(ctx context.Context, pagination *model.Pagination, filter map[string]interface{}) ([]*schemas.AuditLog, *model.Pagination, error)
// DeleteAuditLogsBefore removes logs older than a timestamp (retention)
DeleteAuditLogsBefore(ctx context.Context, before int64) error
// HealthCheck verifies that the storage backend is reachable and responsive.
HealthCheck(ctx context.Context) error
// Close releases resources held by the provider (e.g. database connection pools).
Close() error
// AddClient creates a new service account record.
AddClient(ctx context.Context, sa *schemas.Client) (*schemas.Client, error)
// UpdateClient updates name, description, allowed_scopes, or is_active.
UpdateClient(ctx context.Context, sa *schemas.Client) (*schemas.Client, error)
// DeleteClient removes a service account. Callers must delete
// associated TrustedIssuers before or within the same logical operation.
DeleteClient(ctx context.Context, sa *schemas.Client) error
// GetClientByID fetches a client by its surrogate primary key.
GetClientByID(ctx context.Context, id string) (*schemas.Client, error)
// GetClientByClientID fetches a client by its public, unique client_id
// (distinct from the surrogate ID). This is the lookup the token/authorize
// endpoints and the boot-time reserved-client seed use.
//
// Contract: a genuinely absent client_id MUST return (nil, nil), never a
// wrapped driver "not found" error (gorm.ErrRecordNotFound,
// mongo.ErrNoDocuments, gocql.ErrNotFound, etc.) — callers (notably
// clientauth.ResolveClient, the token endpoint's client_credentials/
// refresh_token/authorization_code auth) distinguish "no such client" from
// "storage temporarily unavailable" solely by whether err is nil. A real
// storage error (a dropped connection, a busy/locked database) must come
// back as (nil, err) so the caller reports a retryable failure instead of
// misreporting the client's credentials as permanently wrong.
GetClientByClientID(ctx context.Context, clientID string) (*schemas.Client, error)
// ListClients returns a paginated list of all service accounts.
ListClients(ctx context.Context, pagination *model.Pagination) ([]*schemas.Client, *model.Pagination, error)
// AddTrustedIssuer creates a new trusted issuer record.
AddTrustedIssuer(ctx context.Context, issuer *schemas.TrustedIssuer) (*schemas.TrustedIssuer, error)
// UpdateTrustedIssuer updates mutable fields: jwks_url, expected_aud,
// is_active, spiffe_refresh_hint_seconds, enable_token_review,
// kubernetes_api_server_url, trusted_proxy_header, trusted_proxy_cidrs.
UpdateTrustedIssuer(ctx context.Context, issuer *schemas.TrustedIssuer) (*schemas.TrustedIssuer, error)
// DeleteTrustedIssuer removes a trusted issuer.
DeleteTrustedIssuer(ctx context.Context, issuer *schemas.TrustedIssuer) error
// GetTrustedIssuerByID fetches a trusted issuer by primary key.
GetTrustedIssuerByID(ctx context.Context, id string) (*schemas.TrustedIssuer, error)
// GetTrustedIssuerByIssuerURL fetches by issuer URL (unique index).
// This is called on every client_assertion validation — keep it fast.
//
// SECURITY (CR1): issuer_url is globally unique, so at most one row exists per
// URL. The client_assertion resolver additionally rejects any row whose
// EffectiveKind is not client_assertion_trust (or whose OrgID is non-empty), so
// an sso_oidc row registered at the same URL can never authenticate a client.
GetTrustedIssuerByIssuerURL(ctx context.Context, issuerURL string) (*schemas.TrustedIssuer, error)
// GetTrustedIssuerByOrgIDAndKind fetches the single trusted issuer for an
// organization of a given kind — used to resolve an org's sso_oidc connection.
// Returns an error when no matching row exists.
GetTrustedIssuerByOrgIDAndKind(ctx context.Context, orgID, kind string) (*schemas.TrustedIssuer, error)
// ListTrustedIssuers returns trusted issuers filtered by serviceAccountID.
// Pass an empty serviceAccountID to list all issuers.
ListTrustedIssuers(ctx context.Context, serviceAccountID string, pagination *model.Pagination) ([]*schemas.TrustedIssuer, *model.Pagination, error)
// AddSAMLServiceProvider registers a new downstream SP.
AddSAMLServiceProvider(ctx context.Context, sp *schemas.SAMLServiceProvider) (*schemas.SAMLServiceProvider, error)
// UpdateSAMLServiceProvider writes back a fully-loaded record. Callers MUST
// load the existing record and mutate it before calling (Save semantics).
UpdateSAMLServiceProvider(ctx context.Context, sp *schemas.SAMLServiceProvider) (*schemas.SAMLServiceProvider, error)
// DeleteSAMLServiceProvider removes a registered SP.
DeleteSAMLServiceProvider(ctx context.Context, sp *schemas.SAMLServiceProvider) error
// GetSAMLServiceProviderByID fetches a registered SP by primary key.
GetSAMLServiceProviderByID(ctx context.Context, id string) (*schemas.SAMLServiceProvider, error)
// GetSAMLServiceProviderByOrgAndEntityID resolves the single registered SP for
// an (orgID, entityID) pair — the lookup that binds an incoming AuthnRequest's
// Issuer to a trusted ACS URL. Returns an error when no matching row exists.
GetSAMLServiceProviderByOrgAndEntityID(ctx context.Context, orgID, entityID string) (*schemas.SAMLServiceProvider, error)
// ListSAMLServiceProviders returns the registered SPs for an org (paginated).
ListSAMLServiceProviders(ctx context.Context, orgID string, pagination *model.Pagination) ([]*schemas.SAMLServiceProvider, *model.Pagination, error)
// AddSAMLIDPKey persists a newly-generated signing keypair.
AddSAMLIDPKey(ctx context.Context, key *schemas.SAMLIDPKey) (*schemas.SAMLIDPKey, error)
// UpdateSAMLIDPKey writes back a fully-loaded record (used to flip rotation
// status). Callers MUST load the existing record before calling.
UpdateSAMLIDPKey(ctx context.Context, key *schemas.SAMLIDPKey) (*schemas.SAMLIDPKey, error)
// DeleteSAMLIDPKey removes a signing key.
DeleteSAMLIDPKey(ctx context.Context, key *schemas.SAMLIDPKey) error
// GetSAMLIDPKeyByID fetches a signing key by primary key.
GetSAMLIDPKeyByID(ctx context.Context, id string) (*schemas.SAMLIDPKey, error)
// ListSAMLIDPKeys returns every signing key for an org (typically 1–3). The
// caller filters by Status: "current" is the signing key, "current"+"active"
// are published in metadata.
ListSAMLIDPKeys(ctx context.Context, orgID string) ([]*schemas.SAMLIDPKey, error)
// AddWebauthnCredential persists a newly registered passkey.
AddWebauthnCredential(ctx context.Context, cred *schemas.WebauthnCredential) (*schemas.WebauthnCredential, error)
// UpdateWebauthnCredential writes back mutable fields (sign_count, flags,
// last_used_at, name). Caller must load the full record first.
UpdateWebauthnCredential(ctx context.Context, cred *schemas.WebauthnCredential) (*schemas.WebauthnCredential, error)
// DeleteWebauthnCredential removes a passkey.
DeleteWebauthnCredential(ctx context.Context, cred *schemas.WebauthnCredential) error
// GetWebauthnCredentialByID fetches a passkey by primary key.
GetWebauthnCredentialByID(ctx context.Context, id string) (*schemas.WebauthnCredential, error)
// GetWebauthnCredentialByCredentialID resolves a passkey by its unique
// WebAuthn credential id — the usernameless-login lookup.
GetWebauthnCredentialByCredentialID(ctx context.Context, credentialID string) (*schemas.WebauthnCredential, error)
// ListWebauthnCredentialsByUserID returns all of a user's passkeys. The list
// is inherently small (one per device), so it is not paginated.
ListWebauthnCredentialsByUserID(ctx context.Context, userID string) ([]*schemas.WebauthnCredential, error)
// AddOrganization creates a new organization record.
AddOrganization(ctx context.Context, org *schemas.Organization) (*schemas.Organization, error)
// GetOrganizationByID fetches an organization by its primary key.
GetOrganizationByID(ctx context.Context, id string) (*schemas.Organization, error)
// GetOrganizationByName fetches an organization by its unique name slug.
GetOrganizationByName(ctx context.Context, name string) (*schemas.Organization, error)
// UpdateOrganization updates name, display_name, or enabled.
UpdateOrganization(ctx context.Context, org *schemas.Organization) (*schemas.Organization, error)
// DeleteOrganization removes an organization and cascade-deletes its
// memberships. Mirrors the DeleteClient cascade pattern.
DeleteOrganization(ctx context.Context, org *schemas.Organization) error
// ListOrganizations returns a paginated list of all organizations.
ListOrganizations(ctx context.Context, pagination *model.Pagination) ([]*schemas.Organization, *model.Pagination, error)
// AddOrgMembership creates a new membership. The (org_id, user_id) pair is
// unique — adding a duplicate returns an error.
AddOrgMembership(ctx context.Context, membership *schemas.OrgMembership) (*schemas.OrgMembership, error)
// GetOrgMembership fetches the membership for a (orgID, userID) pair.
GetOrgMembership(ctx context.Context, orgID, userID string) (*schemas.OrgMembership, error)
// UpdateOrgMembership updates the roles of an existing membership.
UpdateOrgMembership(ctx context.Context, membership *schemas.OrgMembership) (*schemas.OrgMembership, error)
// DeleteOrgMembership removes a membership.
DeleteOrgMembership(ctx context.Context, membership *schemas.OrgMembership) error
// ListOrgMembershipsByOrg returns paginated memberships of an organization.
ListOrgMembershipsByOrg(ctx context.Context, orgID string, pagination *model.Pagination) ([]*schemas.OrgMembership, *model.Pagination, error)
// ListOrgMembershipsByUser returns paginated memberships held by a user.
ListOrgMembershipsByUser(ctx context.Context, userID string, pagination *model.Pagination) ([]*schemas.OrgMembership, *model.Pagination, error)
// AddFederatedIdentity records a JIT-provisioned upstream identity. The
// (org_id, issuer, subject) triple is unique — adding a duplicate returns an
// error.
AddFederatedIdentity(ctx context.Context, identity *schemas.FederatedIdentity) (*schemas.FederatedIdentity, error)
// GetFederatedIdentity fetches the identity for a (orgID, issuer, subject)
// triple. Returns an error when no matching row exists.
GetFederatedIdentity(ctx context.Context, orgID, issuer, subject string) (*schemas.FederatedIdentity, error)
// AddScimEndpoint creates a new SCIM endpoint. OrgID is unique — one
// endpoint per org.
AddScimEndpoint(ctx context.Context, endpoint *schemas.ScimEndpoint) (*schemas.ScimEndpoint, error)
// GetScimEndpointByID fetches an endpoint by primary key (the id embedded in
// the presented bearer token).
GetScimEndpointByID(ctx context.Context, id string) (*schemas.ScimEndpoint, error)
// GetScimEndpointByOrgID fetches an org's endpoint (admin surface, uniqueness
// pre-check).
GetScimEndpointByOrgID(ctx context.Context, orgID string) (*schemas.ScimEndpoint, error)
// UpdateScimEndpoint updates an existing endpoint (token rotation, enable).
// Callers MUST load-then-mutate — Save writes every column.
UpdateScimEndpoint(ctx context.Context, endpoint *schemas.ScimEndpoint) (*schemas.ScimEndpoint, error)
// DeleteScimEndpoint removes an endpoint.
DeleteScimEndpoint(ctx context.Context, endpoint *schemas.ScimEndpoint) error
// AddScimGroup creates a new SCIM group. DisplayName uniqueness within an org
// is enforced by the caller (service layer), not the DB.
AddScimGroup(ctx context.Context, group *schemas.ScimGroup) (*schemas.ScimGroup, error)
// GetScimGroupByID fetches a group by primary key.
GetScimGroupByID(ctx context.Context, id string) (*schemas.ScimGroup, error)
// GetScimGroupByOrgAndDisplayName resolves the single group with the given
// displayName in an org — the SCIM `displayName eq` filter and the create
// dedup probe. Returns an error when no matching row exists.
GetScimGroupByOrgAndDisplayName(ctx context.Context, orgID, displayName string) (*schemas.ScimGroup, error)
// GetScimGroupByOrgAndExternalID resolves the single group with the given
// externalId in an org — the IdP correlation key that lets a renamed group
// (same externalId, new displayName) update in place instead of duplicating.
// externalID is the raw IdP value; the store namespaces it as "<orgID>:<raw>"
// exactly like GetUserByExternalID. Returns an error when no matching row exists.
GetScimGroupByOrgAndExternalID(ctx context.Context, orgID, externalID string) (*schemas.ScimGroup, error)
// UpdateScimGroup writes back a fully-loaded record (PUT displayName change).
// Callers MUST load-then-mutate — Save writes every column.
UpdateScimGroup(ctx context.Context, group *schemas.ScimGroup) (*schemas.ScimGroup, error)
// DeleteScimGroup removes a group.
DeleteScimGroup(ctx context.Context, group *schemas.ScimGroup) error
// AddOrgDomain atomically inserts a verified domain row, keyed by the
// normalized domain (the primary/partition key). First-writer-wins:
// - domain unclaimed → inserts and returns the new row.
// - domain already held by the SAME org → returns the existing row (idempotent).
// - domain already held by a DIFFERENT org → returns schemas.ErrOrgDomainConflict.
// ID and Domain MUST both be set to the normalized domain by the caller.
AddOrgDomain(ctx context.Context, domain *schemas.OrgDomain) (*schemas.OrgDomain, error)
// GetOrgDomainByDomain fetches the verified row for a normalized domain
// (the home-realm-discovery reverse lookup — a primary-key GET).
GetOrgDomainByDomain(ctx context.Context, domain string) (*schemas.OrgDomain, error)
// ListOrgDomainsByOrg returns an org's verified domains, paginated.
ListOrgDomainsByOrg(ctx context.Context, orgID string, pagination *model.Pagination) ([]*schemas.OrgDomain, *model.Pagination, error)
// DeleteOrgDomain removes a verified domain mapping by normalized domain.
DeleteOrgDomain(ctx context.Context, domain string) error
// DeleteOrgDomainsByOrg removes all of an org's verified domains (cascade on
// org delete — otherwise the domain becomes permanently unclaimable).
DeleteOrgDomainsByOrg(ctx context.Context, orgID string) error
}
Provider is the interface which defines the methods for the database provider.
Delete methods are idempotent: deleting a non-existent id returns nil, not an error. Callers that rely on delete-confirms-existence must check existence separately first.
Not-found convention ¶
Every backend must agree on how "the row does not exist" is reported, because callers branch on it. Two rules, and one explicitly documented exception:
SINGLE-ENTITY GETTERS RETURN AN ERROR when the row is absent — the driver's own not-found value (gorm.ErrRecordNotFound, mongo.ErrNoDocuments, gocql.ErrNotFound, a bare errors.New for key-value backends). This is the default and covers all but one of them. Callers therefore treat `err != nil` as "absent or unavailable" and are entitled to dereference the returned pointer once err is nil.
Returning (nil, nil) from one of these is a PARITY BUG, not a style choice: callers written against the majority contract dereference the nil row and panic on that backend alone. Three DynamoDB methods did exactly this (authenticator and verification-request lookups) and crashed the TOTP and email-verification paths on that backend only.
LIST METHODS RETURN (nil, nil) FOR AN EMPTY RESULT. An empty collection is not an error, and callers range over the slice — a nil slice ranges zero times, so no guard is needed.
EXCEPTION — GetClientByClientID MUST return (nil, nil) for an absent client_id. Its callers distinguish "no such client" from "storage unavailable" solely by whether err is nil, so an absent row must not be reported as an error. See the method's own comment for the reasoning. Callers of THIS method must nil-check the returned pointer.
When adding a method, follow rule 1 unless there is a documented reason not to, and implement the same behaviour in all backends (internal/storage/db/{sql,mongodb,arangodb,cassandradb,dynamodb,couchbase}).
Directories
¶
| Path | Synopsis |
|---|---|
|
db
|
|
|
cassandradb
Package cassandradb implements the storage provider backed by Cassandra/ScyllaDB.
|
Package cassandradb implements the storage provider backed by Cassandra/ScyllaDB. |
|
sql/sqlitedialect
Package sqlitedialect is a pure-Go GORM SQLite dialector.
|
Package sqlitedialect is a pure-Go GORM SQLite dialector. |