From 00ecb9349f7c4e5cc2c94fa42d10121355102b8e Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Wed, 22 Jul 2026 11:43:39 +0530 Subject: [PATCH 1/2] chore(storage): bring provider_template to full Provider parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Template only stubbed 60 of 122 storage.Provider methods — files for client, organization, org_membership, org_domain, saml_idp, scim_endpoint, scim_group, trusted_issuer, webauthn_credential, federated_identity, audit_log, and health_check were never added after those features landed on the real providers (#580, #686, #691, #694). make generate-db-template handed contributors a struct that didn't compile against the interface. - add the 12 missing feature files as stubs, matching the existing file-per-feature layout of sql/mongodb - add GetUserByExternalID stub to user.go - add var _ storage.Provider = (*provider)(nil) to provider.go so future interface drift fails the build immediately instead of silently shipping an incomplete template Verified: make generate-db-template dbname=x now produces a package that builds and satisfies storage.Provider out of the box. --- CONTRIBUTING.md | 20 ++++- .../storage/db/provider_template/audit_log.go | 28 +++++++ .../storage/db/provider_template/client.go | 48 +++++++++++ .../provider_template/federated_identity.go | 27 +++++++ .../db/provider_template/health_check.go | 10 +++ .../db/provider_template/org_domain.go | 44 +++++++++++ .../db/provider_template/org_membership.go | 48 +++++++++++ .../db/provider_template/organization.go | 47 +++++++++++ .../storage/db/provider_template/provider.go | 22 +++--- .../storage/db/provider_template/saml_idp.go | 79 +++++++++++++++++++ .../db/provider_template/scim_endpoint.go | 42 ++++++++++ .../db/provider_template/scim_group.go | 50 ++++++++++++ .../db/provider_template/trusted_issuer.go | 54 +++++++++++++ internal/storage/db/provider_template/user.go | 7 ++ .../provider_template/webauthn_credential.go | 48 +++++++++++ 15 files changed, 558 insertions(+), 16 deletions(-) create mode 100644 internal/storage/db/provider_template/audit_log.go create mode 100644 internal/storage/db/provider_template/client.go create mode 100644 internal/storage/db/provider_template/federated_identity.go create mode 100644 internal/storage/db/provider_template/health_check.go create mode 100644 internal/storage/db/provider_template/org_domain.go create mode 100644 internal/storage/db/provider_template/org_membership.go create mode 100644 internal/storage/db/provider_template/organization.go create mode 100644 internal/storage/db/provider_template/saml_idp.go create mode 100644 internal/storage/db/provider_template/scim_endpoint.go create mode 100644 internal/storage/db/provider_template/scim_group.go create mode 100644 internal/storage/db/provider_template/trusted_issuer.go create mode 100644 internal/storage/db/provider_template/webauthn_credential.go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e600ea697..ee29a21f8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -61,12 +61,24 @@ Please ask as many questions as you need, either directly in the issue or on [Di ### Adding support for new database -- Run `make generate-db-template dbname=NEW_DB_NAME` - - e.g. `make generate-db-template dbname=dynamodb` +1. Run `make generate-db-template dbname=NEW_DB_NAME` + - e.g. `make generate-db-template dbname=dynamodb` -This generates a folder in `internal/storage/db/` with the specified name. Implement the methods in that folder. + This copies `internal/storage/db/provider_template/` to `internal/storage/db/NEW_DB_NAME/` and renames the package. The template already stubs every method of `storage.Provider` (`internal/storage/provider.go`) across all feature areas — users, sessions, webhooks, email templates, OTP, authenticators, memory-store (session/MFA/OAuth-state), audit logs, clients, trusted issuers, SAML (SP + IDP keys), SCIM (endpoints + groups), WebAuthn credentials, organizations, org memberships, org domains, and federated identities — plus a compile-time `var _ storage.Provider = (*provider)(nil)` assertion in `provider.go`. If that assertion ever fails to compile, the template has drifted from the interface — fix the template, not just your new provider. -> Note: Database connection and schema changes are in `internal/storage/db/DB_NAME/provider.go`; `NewProvider` is called for the configured database type. +2. Change the `provider` struct and `NewProvider` in `NEW_DB_NAME/provider.go` to hold and construct your actual database client (the template ships with a placeholder `*gorm.DB` field — replace it). + +3. Implement each stubbed method for real, one feature file at a time. Use an existing provider as a reference for the query patterns of a similar backend: + - SQL-like/GORM backend → `internal/storage/db/sql/` + - Document store → `internal/storage/db/mongodb/` or `internal/storage/db/arangodb/` + - Wide-column store → `internal/storage/db/cassandradb/` + - Key-value store → `internal/storage/db/dynamodb/` or `internal/storage/db/couchbase/` + +4. Wire the new provider into `storage.New()` (`internal/storage/provider.go`) behind its config-selected database type. + +5. Add the new provider to the storage test matrix (`TEST_DBS`) and a `make test-NEW_DB_NAME` / `test-cleanup-NEW_DB_NAME` Docker target in the `Makefile`, following the pattern of the existing `test-postgres` / `test-mongodb` targets. + +> Note: `go build ./internal/storage/db/NEW_DB_NAME/...` will fail with a `does not implement storage.Provider (missing method ...)` error until every method is implemented — that's the compile-time assertion in `provider.go` doing its job. ### Testing diff --git a/internal/storage/db/provider_template/audit_log.go b/internal/storage/db/provider_template/audit_log.go new file mode 100644 index 000000000..894143737 --- /dev/null +++ b/internal/storage/db/provider_template/audit_log.go @@ -0,0 +1,28 @@ +package provider_template + +import ( + "context" + + "github.com/google/uuid" + + "github.com/authorizerdev/authorizer/internal/graph/model" + "github.com/authorizerdev/authorizer/internal/storage/schemas" +) + +// AddAuditLog adds an audit log entry +func (p *provider) AddAuditLog(ctx context.Context, log *schemas.AuditLog) error { + if log.ID == "" { + log.ID = uuid.New().String() + } + return nil +} + +// ListAuditLogs queries audit logs with filters and pagination +func (p *provider) ListAuditLogs(ctx context.Context, pagination *model.Pagination, filter map[string]interface{}) ([]*schemas.AuditLog, *model.Pagination, error) { + return nil, nil, nil +} + +// DeleteAuditLogsBefore removes logs older than a timestamp (retention) +func (p *provider) DeleteAuditLogsBefore(ctx context.Context, before int64) error { + return nil +} diff --git a/internal/storage/db/provider_template/client.go b/internal/storage/db/provider_template/client.go new file mode 100644 index 000000000..6d2b3bf8d --- /dev/null +++ b/internal/storage/db/provider_template/client.go @@ -0,0 +1,48 @@ +package provider_template + +import ( + "context" + "time" + + "github.com/google/uuid" + + "github.com/authorizerdev/authorizer/internal/graph/model" + "github.com/authorizerdev/authorizer/internal/storage/schemas" +) + +// AddClient creates a new service account record. +func (p *provider) AddClient(ctx context.Context, sa *schemas.Client) (*schemas.Client, error) { + if sa.ID == "" { + sa.ID = uuid.New().String() + } + sa.CreatedAt = time.Now().Unix() + sa.UpdatedAt = time.Now().Unix() + return sa, nil +} + +// UpdateClient updates name, description, allowed_scopes, or is_active. +func (p *provider) UpdateClient(ctx context.Context, sa *schemas.Client) (*schemas.Client, error) { + sa.UpdatedAt = time.Now().Unix() + return sa, nil +} + +// DeleteClient removes a client. Callers must delete associated TrustedIssuers +// before or within the same logical operation. +func (p *provider) DeleteClient(ctx context.Context, sa *schemas.Client) error { + return nil +} + +// GetClientByID fetches a client by its surrogate primary key. +func (p *provider) GetClientByID(ctx context.Context, id string) (*schemas.Client, error) { + return nil, nil +} + +// GetClientByClientID fetches a client by its public, unique client_id. +func (p *provider) GetClientByClientID(ctx context.Context, clientID string) (*schemas.Client, error) { + return nil, nil +} + +// ListClients returns a paginated list of all clients. +func (p *provider) ListClients(ctx context.Context, pagination *model.Pagination) ([]*schemas.Client, *model.Pagination, error) { + return nil, nil, nil +} diff --git a/internal/storage/db/provider_template/federated_identity.go b/internal/storage/db/provider_template/federated_identity.go new file mode 100644 index 000000000..81fde4e7b --- /dev/null +++ b/internal/storage/db/provider_template/federated_identity.go @@ -0,0 +1,27 @@ +package provider_template + +import ( + "context" + "time" + + "github.com/google/uuid" + + "github.com/authorizerdev/authorizer/internal/storage/schemas" +) + +// AddFederatedIdentity records a JIT-provisioned upstream identity. The +// (org_id, issuer, subject) triple is unique — adding a duplicate returns an +// error. +func (p *provider) AddFederatedIdentity(ctx context.Context, identity *schemas.FederatedIdentity) (*schemas.FederatedIdentity, error) { + if identity.ID == "" { + identity.ID = uuid.New().String() + } + identity.CreatedAt = time.Now().Unix() + identity.UpdatedAt = time.Now().Unix() + return identity, nil +} + +// GetFederatedIdentity fetches the identity for a (orgID, issuer, subject) triple. +func (p *provider) GetFederatedIdentity(ctx context.Context, orgID, issuer, subject string) (*schemas.FederatedIdentity, error) { + return nil, nil +} diff --git a/internal/storage/db/provider_template/health_check.go b/internal/storage/db/provider_template/health_check.go new file mode 100644 index 000000000..3e1ec219c --- /dev/null +++ b/internal/storage/db/provider_template/health_check.go @@ -0,0 +1,10 @@ +package provider_template + +import ( + "context" +) + +// HealthCheck verifies that the storage backend is reachable and responsive. +func (p *provider) HealthCheck(ctx context.Context) error { + return nil +} diff --git a/internal/storage/db/provider_template/org_domain.go b/internal/storage/db/provider_template/org_domain.go new file mode 100644 index 000000000..8a94e4351 --- /dev/null +++ b/internal/storage/db/provider_template/org_domain.go @@ -0,0 +1,44 @@ +package provider_template + +import ( + "context" + "time" + + "github.com/authorizerdev/authorizer/internal/graph/model" + "github.com/authorizerdev/authorizer/internal/storage/schemas" +) + +// AddOrgDomain atomically inserts a verified domain row, keyed by the +// normalized domain. Caller MUST set ID and Domain to the normalized domain +// before calling. First-writer-wins: same org already holding the domain is +// idempotent success, a different org owning it returns schemas.ErrOrgDomainConflict. +func (p *provider) AddOrgDomain(ctx context.Context, domain *schemas.OrgDomain) (*schemas.OrgDomain, error) { + now := time.Now().Unix() + domain.CreatedAt = now + domain.UpdatedAt = now + if domain.VerifiedAt == 0 { + domain.VerifiedAt = now + } + return domain, nil +} + +// GetOrgDomainByDomain fetches the verified row for a normalized domain. +func (p *provider) GetOrgDomainByDomain(ctx context.Context, domain string) (*schemas.OrgDomain, error) { + return nil, nil +} + +// ListOrgDomainsByOrg returns an org's verified domains, paginated. +func (p *provider) ListOrgDomainsByOrg(ctx context.Context, orgID string, pagination *model.Pagination) ([]*schemas.OrgDomain, *model.Pagination, error) { + return nil, nil, nil +} + +// DeleteOrgDomain removes a verified domain mapping by normalized domain. +func (p *provider) DeleteOrgDomain(ctx context.Context, domain string) error { + return nil +} + +// DeleteOrgDomainsByOrg removes all of an org's verified domains (cascade on +// org delete — otherwise the domain becomes permanently unclaimable). +func (p *provider) DeleteOrgDomainsByOrg(ctx context.Context, orgID string) error { + return nil +} diff --git a/internal/storage/db/provider_template/org_membership.go b/internal/storage/db/provider_template/org_membership.go new file mode 100644 index 000000000..32e3688cc --- /dev/null +++ b/internal/storage/db/provider_template/org_membership.go @@ -0,0 +1,48 @@ +package provider_template + +import ( + "context" + "time" + + "github.com/google/uuid" + + "github.com/authorizerdev/authorizer/internal/graph/model" + "github.com/authorizerdev/authorizer/internal/storage/schemas" +) + +// AddOrgMembership creates a new membership. The (org_id, user_id) pair is +// unique — adding a duplicate returns an error. +func (p *provider) AddOrgMembership(ctx context.Context, membership *schemas.OrgMembership) (*schemas.OrgMembership, error) { + if membership.ID == "" { + membership.ID = uuid.New().String() + } + membership.CreatedAt = time.Now().Unix() + membership.UpdatedAt = time.Now().Unix() + return membership, nil +} + +// GetOrgMembership fetches the membership for a (orgID, userID) pair. +func (p *provider) GetOrgMembership(ctx context.Context, orgID, userID string) (*schemas.OrgMembership, error) { + return nil, nil +} + +// UpdateOrgMembership updates the roles of an existing membership. +func (p *provider) UpdateOrgMembership(ctx context.Context, membership *schemas.OrgMembership) (*schemas.OrgMembership, error) { + membership.UpdatedAt = time.Now().Unix() + return membership, nil +} + +// DeleteOrgMembership removes a membership. +func (p *provider) DeleteOrgMembership(ctx context.Context, membership *schemas.OrgMembership) error { + return nil +} + +// ListOrgMembershipsByOrg returns paginated memberships of an organization. +func (p *provider) ListOrgMembershipsByOrg(ctx context.Context, orgID string, pagination *model.Pagination) ([]*schemas.OrgMembership, *model.Pagination, error) { + return nil, nil, nil +} + +// ListOrgMembershipsByUser returns paginated memberships held by a user. +func (p *provider) ListOrgMembershipsByUser(ctx context.Context, userID string, pagination *model.Pagination) ([]*schemas.OrgMembership, *model.Pagination, error) { + return nil, nil, nil +} diff --git a/internal/storage/db/provider_template/organization.go b/internal/storage/db/provider_template/organization.go new file mode 100644 index 000000000..67ebd856b --- /dev/null +++ b/internal/storage/db/provider_template/organization.go @@ -0,0 +1,47 @@ +package provider_template + +import ( + "context" + "time" + + "github.com/google/uuid" + + "github.com/authorizerdev/authorizer/internal/graph/model" + "github.com/authorizerdev/authorizer/internal/storage/schemas" +) + +// AddOrganization creates a new organization record. +func (p *provider) AddOrganization(ctx context.Context, org *schemas.Organization) (*schemas.Organization, error) { + if org.ID == "" { + org.ID = uuid.New().String() + } + org.CreatedAt = time.Now().Unix() + org.UpdatedAt = time.Now().Unix() + return org, nil +} + +// GetOrganizationByID fetches an organization by its primary key. +func (p *provider) GetOrganizationByID(ctx context.Context, id string) (*schemas.Organization, error) { + return nil, nil +} + +// GetOrganizationByName fetches an organization by its unique name slug. +func (p *provider) GetOrganizationByName(ctx context.Context, name string) (*schemas.Organization, error) { + return nil, nil +} + +// UpdateOrganization updates name, display_name, or enabled. +func (p *provider) UpdateOrganization(ctx context.Context, org *schemas.Organization) (*schemas.Organization, error) { + org.UpdatedAt = time.Now().Unix() + return org, nil +} + +// DeleteOrganization removes an organization and cascade-deletes its memberships. +func (p *provider) DeleteOrganization(ctx context.Context, org *schemas.Organization) error { + return nil +} + +// ListOrganizations returns a paginated list of all organizations. +func (p *provider) ListOrganizations(ctx context.Context, pagination *model.Pagination) ([]*schemas.Organization, *model.Pagination, error) { + return nil, nil, nil +} diff --git a/internal/storage/db/provider_template/provider.go b/internal/storage/db/provider_template/provider.go index b224bed91..05d22039e 100644 --- a/internal/storage/db/provider_template/provider.go +++ b/internal/storage/db/provider_template/provider.go @@ -5,6 +5,7 @@ import ( "gorm.io/gorm" "github.com/authorizerdev/authorizer/internal/config" + "github.com/authorizerdev/authorizer/internal/storage" ) // Dependencies struct the TODO(replace with new db name) data store provider @@ -19,21 +20,18 @@ type provider struct { db *gorm.DB } +// Compile-time check: provider must implement every method of storage.Provider. +// Deleting or renaming a method here without updating the interface (or vice +// versa) fails the build immediately instead of silently drifting. +var _ storage.Provider = (*provider)(nil) + // NewProvider returns a new provider for your database type. // TODO: change provider struct and NewProvider to use your database client. // -// This provider must implement all methods from storage.Provider, including: -// - User, VerificationRequest, Session, Webhook, EmailTemplate, OTP, Authenticator -// - Memory store methods (when Redis is not configured): -// - SessionToken: AddSessionToken, GetSessionTokenByUserIDAndKey, DeleteSessionToken, -// DeleteSessionTokenByUserIDAndKey, DeleteAllSessionTokensByUserID, -// DeleteSessionTokensByNamespace, CleanExpiredSessionTokens, GetAllSessionTokens -// - MFASession: AddMFASession, GetMFASessionByUserIDAndKey, DeleteMFASession, -// DeleteMFASessionByUserIDAndKey, GetAllMFASessionsByUserID, -// CleanExpiredMFASessions, GetAllMFASessions -// - OAuthState: AddOAuthState, GetOAuthStateByKey, DeleteOAuthStateByKey, GetAllOAuthStates -// -// Use schemas.Collections for table/collection names (e.g., schemas.Collections.SessionToken). +// This provider must implement every method of storage.Provider — see that +// interface (internal/storage/provider.go) for the authoritative, documented +// list. Use schemas.Collections for table/collection names (e.g., +// schemas.Collections.SessionToken). func NewProvider( config *config.Config, deps *Dependencies, diff --git a/internal/storage/db/provider_template/saml_idp.go b/internal/storage/db/provider_template/saml_idp.go new file mode 100644 index 000000000..6e7c0459e --- /dev/null +++ b/internal/storage/db/provider_template/saml_idp.go @@ -0,0 +1,79 @@ +package provider_template + +import ( + "context" + "time" + + "github.com/google/uuid" + + "github.com/authorizerdev/authorizer/internal/graph/model" + "github.com/authorizerdev/authorizer/internal/storage/schemas" +) + +// AddSAMLServiceProvider registers a new downstream SP. +func (p *provider) AddSAMLServiceProvider(ctx context.Context, sp *schemas.SAMLServiceProvider) (*schemas.SAMLServiceProvider, error) { + if sp.ID == "" { + sp.ID = uuid.New().String() + } + sp.CreatedAt = time.Now().Unix() + sp.UpdatedAt = time.Now().Unix() + return sp, nil +} + +// UpdateSAMLServiceProvider writes back a fully-loaded record. +func (p *provider) UpdateSAMLServiceProvider(ctx context.Context, sp *schemas.SAMLServiceProvider) (*schemas.SAMLServiceProvider, error) { + sp.UpdatedAt = time.Now().Unix() + return sp, nil +} + +// DeleteSAMLServiceProvider removes a registered SP. +func (p *provider) DeleteSAMLServiceProvider(ctx context.Context, sp *schemas.SAMLServiceProvider) error { + return nil +} + +// GetSAMLServiceProviderByID fetches a registered SP by primary key. +func (p *provider) GetSAMLServiceProviderByID(ctx context.Context, id string) (*schemas.SAMLServiceProvider, error) { + return nil, nil +} + +// GetSAMLServiceProviderByOrgAndEntityID resolves the single registered SP for +// an (orgID, entityID) pair. +func (p *provider) GetSAMLServiceProviderByOrgAndEntityID(ctx context.Context, orgID, entityID string) (*schemas.SAMLServiceProvider, error) { + return nil, nil +} + +// ListSAMLServiceProviders returns the registered SPs for an org (paginated). +func (p *provider) ListSAMLServiceProviders(ctx context.Context, orgID string, pagination *model.Pagination) ([]*schemas.SAMLServiceProvider, *model.Pagination, error) { + return nil, nil, nil +} + +// AddSAMLIDPKey persists a newly-generated signing keypair. +func (p *provider) AddSAMLIDPKey(ctx context.Context, key *schemas.SAMLIDPKey) (*schemas.SAMLIDPKey, error) { + if key.ID == "" { + key.ID = uuid.New().String() + } + key.CreatedAt = time.Now().Unix() + key.UpdatedAt = time.Now().Unix() + return key, nil +} + +// UpdateSAMLIDPKey writes back a fully-loaded record (used to flip rotation status). +func (p *provider) UpdateSAMLIDPKey(ctx context.Context, key *schemas.SAMLIDPKey) (*schemas.SAMLIDPKey, error) { + key.UpdatedAt = time.Now().Unix() + return key, nil +} + +// DeleteSAMLIDPKey removes a signing key. +func (p *provider) DeleteSAMLIDPKey(ctx context.Context, key *schemas.SAMLIDPKey) error { + return nil +} + +// GetSAMLIDPKeyByID fetches a signing key by primary key. +func (p *provider) GetSAMLIDPKeyByID(ctx context.Context, id string) (*schemas.SAMLIDPKey, error) { + return nil, nil +} + +// ListSAMLIDPKeys returns every signing key for an org. +func (p *provider) ListSAMLIDPKeys(ctx context.Context, orgID string) ([]*schemas.SAMLIDPKey, error) { + return nil, nil +} diff --git a/internal/storage/db/provider_template/scim_endpoint.go b/internal/storage/db/provider_template/scim_endpoint.go new file mode 100644 index 000000000..46de0c070 --- /dev/null +++ b/internal/storage/db/provider_template/scim_endpoint.go @@ -0,0 +1,42 @@ +package provider_template + +import ( + "context" + "time" + + "github.com/google/uuid" + + "github.com/authorizerdev/authorizer/internal/storage/schemas" +) + +// AddScimEndpoint creates a new SCIM endpoint. OrgID is unique — one endpoint per org. +func (p *provider) AddScimEndpoint(ctx context.Context, endpoint *schemas.ScimEndpoint) (*schemas.ScimEndpoint, error) { + if endpoint.ID == "" { + endpoint.ID = uuid.New().String() + } + endpoint.CreatedAt = time.Now().Unix() + endpoint.UpdatedAt = time.Now().Unix() + return endpoint, nil +} + +// GetScimEndpointByID fetches an endpoint by primary key. +func (p *provider) GetScimEndpointByID(ctx context.Context, id string) (*schemas.ScimEndpoint, error) { + return nil, nil +} + +// GetScimEndpointByOrgID fetches an org's endpoint. +func (p *provider) GetScimEndpointByOrgID(ctx context.Context, orgID string) (*schemas.ScimEndpoint, error) { + return nil, nil +} + +// UpdateScimEndpoint updates an existing endpoint (token rotation, enable). +// Callers MUST load-then-mutate — Save writes every column. +func (p *provider) UpdateScimEndpoint(ctx context.Context, endpoint *schemas.ScimEndpoint) (*schemas.ScimEndpoint, error) { + endpoint.UpdatedAt = time.Now().Unix() + return endpoint, nil +} + +// DeleteScimEndpoint removes an endpoint. +func (p *provider) DeleteScimEndpoint(ctx context.Context, endpoint *schemas.ScimEndpoint) error { + return nil +} diff --git a/internal/storage/db/provider_template/scim_group.go b/internal/storage/db/provider_template/scim_group.go new file mode 100644 index 000000000..2070ec6f2 --- /dev/null +++ b/internal/storage/db/provider_template/scim_group.go @@ -0,0 +1,50 @@ +package provider_template + +import ( + "context" + "time" + + "github.com/google/uuid" + + "github.com/authorizerdev/authorizer/internal/storage/schemas" +) + +// AddScimGroup creates a new SCIM group. DisplayName uniqueness within an org +// is enforced by the caller (service layer), not the DB. +func (p *provider) AddScimGroup(ctx context.Context, group *schemas.ScimGroup) (*schemas.ScimGroup, error) { + if group.ID == "" { + group.ID = uuid.New().String() + } + group.CreatedAt = time.Now().Unix() + group.UpdatedAt = time.Now().Unix() + return group, nil +} + +// GetScimGroupByID fetches a group by primary key. +func (p *provider) GetScimGroupByID(ctx context.Context, id string) (*schemas.ScimGroup, error) { + return nil, nil +} + +// GetScimGroupByOrgAndDisplayName resolves the single group with the given +// displayName in an org. +func (p *provider) GetScimGroupByOrgAndDisplayName(ctx context.Context, orgID, displayName string) (*schemas.ScimGroup, error) { + return nil, nil +} + +// GetScimGroupByOrgAndExternalID resolves the single group with the given +// externalId in an org. +func (p *provider) GetScimGroupByOrgAndExternalID(ctx context.Context, orgID, externalID string) (*schemas.ScimGroup, error) { + return nil, nil +} + +// UpdateScimGroup writes back a fully-loaded record (PUT displayName change). +// Callers MUST load-then-mutate — Save writes every column. +func (p *provider) UpdateScimGroup(ctx context.Context, group *schemas.ScimGroup) (*schemas.ScimGroup, error) { + group.UpdatedAt = time.Now().Unix() + return group, nil +} + +// DeleteScimGroup removes a group. +func (p *provider) DeleteScimGroup(ctx context.Context, group *schemas.ScimGroup) error { + return nil +} diff --git a/internal/storage/db/provider_template/trusted_issuer.go b/internal/storage/db/provider_template/trusted_issuer.go new file mode 100644 index 000000000..9114d1bba --- /dev/null +++ b/internal/storage/db/provider_template/trusted_issuer.go @@ -0,0 +1,54 @@ +package provider_template + +import ( + "context" + "time" + + "github.com/google/uuid" + + "github.com/authorizerdev/authorizer/internal/graph/model" + "github.com/authorizerdev/authorizer/internal/storage/schemas" +) + +// AddTrustedIssuer creates a new trusted issuer record. +func (p *provider) AddTrustedIssuer(ctx context.Context, issuer *schemas.TrustedIssuer) (*schemas.TrustedIssuer, error) { + if issuer.ID == "" { + issuer.ID = uuid.New().String() + } + issuer.CreatedAt = time.Now().Unix() + issuer.UpdatedAt = time.Now().Unix() + return issuer, nil +} + +// UpdateTrustedIssuer updates mutable fields. +func (p *provider) UpdateTrustedIssuer(ctx context.Context, issuer *schemas.TrustedIssuer) (*schemas.TrustedIssuer, error) { + issuer.UpdatedAt = time.Now().Unix() + return issuer, nil +} + +// DeleteTrustedIssuer removes a trusted issuer. +func (p *provider) DeleteTrustedIssuer(ctx context.Context, issuer *schemas.TrustedIssuer) error { + return nil +} + +// GetTrustedIssuerByID fetches a trusted issuer by primary key. +func (p *provider) GetTrustedIssuerByID(ctx context.Context, id string) (*schemas.TrustedIssuer, error) { + return nil, nil +} + +// GetTrustedIssuerByIssuerURL fetches by issuer URL (unique index). +func (p *provider) GetTrustedIssuerByIssuerURL(ctx context.Context, issuerURL string) (*schemas.TrustedIssuer, error) { + return nil, nil +} + +// GetTrustedIssuerByOrgIDAndKind fetches the single trusted issuer for an +// organization of a given kind. +func (p *provider) GetTrustedIssuerByOrgIDAndKind(ctx context.Context, orgID, kind string) (*schemas.TrustedIssuer, error) { + return nil, nil +} + +// ListTrustedIssuers returns trusted issuers filtered by serviceAccountID. +// Pass an empty serviceAccountID to list all issuers. +func (p *provider) ListTrustedIssuers(ctx context.Context, serviceAccountID string, pagination *model.Pagination) ([]*schemas.TrustedIssuer, *model.Pagination, error) { + return nil, nil, nil +} diff --git a/internal/storage/db/provider_template/user.go b/internal/storage/db/provider_template/user.go index d51984a20..74f622733 100644 --- a/internal/storage/db/provider_template/user.go +++ b/internal/storage/db/provider_template/user.go @@ -63,6 +63,13 @@ func (p *provider) GetUserByID(ctx context.Context, id string) (*schemas.User, e return user, nil } +// GetUserByExternalID fetches an IdP-provisioned user by its org-namespaced +// external id. +func (p *provider) GetUserByExternalID(ctx context.Context, orgID, externalID string) (*schemas.User, error) { + var user *schemas.User + return user, nil +} + // UpdateUsers to update multiple users, with parameters of user IDs slice // If ids set to nil / empty all the users will be updated func (p *provider) UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error { diff --git a/internal/storage/db/provider_template/webauthn_credential.go b/internal/storage/db/provider_template/webauthn_credential.go new file mode 100644 index 000000000..bd244c4ce --- /dev/null +++ b/internal/storage/db/provider_template/webauthn_credential.go @@ -0,0 +1,48 @@ +package provider_template + +import ( + "context" + "time" + + "github.com/google/uuid" + + "github.com/authorizerdev/authorizer/internal/storage/schemas" +) + +// AddWebauthnCredential persists a newly registered passkey. +func (p *provider) AddWebauthnCredential(ctx context.Context, cred *schemas.WebauthnCredential) (*schemas.WebauthnCredential, error) { + if cred.ID == "" { + cred.ID = uuid.New().String() + } + cred.CreatedAt = time.Now().Unix() + cred.UpdatedAt = time.Now().Unix() + return cred, nil +} + +// UpdateWebauthnCredential writes back mutable fields (sign_count, flags, +// last_used_at, name). Caller must load the full record first. +func (p *provider) UpdateWebauthnCredential(ctx context.Context, cred *schemas.WebauthnCredential) (*schemas.WebauthnCredential, error) { + cred.UpdatedAt = time.Now().Unix() + return cred, nil +} + +// DeleteWebauthnCredential removes a passkey. +func (p *provider) DeleteWebauthnCredential(ctx context.Context, cred *schemas.WebauthnCredential) error { + return nil +} + +// GetWebauthnCredentialByID fetches a passkey by primary key. +func (p *provider) GetWebauthnCredentialByID(ctx context.Context, id string) (*schemas.WebauthnCredential, error) { + return nil, nil +} + +// GetWebauthnCredentialByCredentialID resolves a passkey by its unique +// WebAuthn credential id — the usernameless-login lookup. +func (p *provider) GetWebauthnCredentialByCredentialID(ctx context.Context, credentialID string) (*schemas.WebauthnCredential, error) { + return nil, nil +} + +// ListWebauthnCredentialsByUserID returns all of a user's passkeys. +func (p *provider) ListWebauthnCredentialsByUserID(ctx context.Context, userID string) ([]*schemas.WebauthnCredential, error) { + return nil, nil +} From ba8ba2aa0a0d62d2a8f19d8f0bba2688ce266728 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Wed, 22 Jul 2026 11:48:08 +0530 Subject: [PATCH 2/2] fix(storage): move Provider assertion out of provider_template.go var _ storage.Provider = (*provider)(nil) in provider.go created an import cycle the moment a generated provider gets wired into storage.New() per CONTRIBUTING.md step 4: internal/storage would import the new db package, which imported internal/storage right back. Caught by simulating the full generate -> wire -> build flow, not just building the template in isolation. Move the check into interface_test.go as an external _test package (provider_template_test), which can import internal/storage without being part of the same build graph storage.New() pulls in. Verified: - go build ./... clean with a provider actually wired into storage.New() - go test ./internal/storage/db/provider_template/... fails to compile (not just fails at runtime) when a method is removed --- CONTRIBUTING.md | 4 +-- .../db/provider_template/interface_test.go | 25 +++++++++++++++++++ .../storage/db/provider_template/provider.go | 15 ++++++----- 3 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 internal/storage/db/provider_template/interface_test.go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ee29a21f8..7d21c7a2b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,7 +64,7 @@ Please ask as many questions as you need, either directly in the issue or on [Di 1. Run `make generate-db-template dbname=NEW_DB_NAME` - e.g. `make generate-db-template dbname=dynamodb` - This copies `internal/storage/db/provider_template/` to `internal/storage/db/NEW_DB_NAME/` and renames the package. The template already stubs every method of `storage.Provider` (`internal/storage/provider.go`) across all feature areas — users, sessions, webhooks, email templates, OTP, authenticators, memory-store (session/MFA/OAuth-state), audit logs, clients, trusted issuers, SAML (SP + IDP keys), SCIM (endpoints + groups), WebAuthn credentials, organizations, org memberships, org domains, and federated identities — plus a compile-time `var _ storage.Provider = (*provider)(nil)` assertion in `provider.go`. If that assertion ever fails to compile, the template has drifted from the interface — fix the template, not just your new provider. + This copies `internal/storage/db/provider_template/` to `internal/storage/db/NEW_DB_NAME/` and renames the package. The template already stubs every method of `storage.Provider` (`internal/storage/provider.go`) across all feature areas — users, sessions, webhooks, email templates, OTP, authenticators, memory-store (session/MFA/OAuth-state), audit logs, clients, trusted issuers, SAML (SP + IDP keys), SCIM (endpoints + groups), WebAuthn credentials, organizations, org memberships, org domains, and federated identities. Run `go test ./internal/storage/db/NEW_DB_NAME/...` any time to confirm it still satisfies `storage.Provider` in full — `interface_test.go` fails to compile the instant a method goes missing. 2. Change the `provider` struct and `NewProvider` in `NEW_DB_NAME/provider.go` to hold and construct your actual database client (the template ships with a placeholder `*gorm.DB` field — replace it). @@ -78,7 +78,7 @@ Please ask as many questions as you need, either directly in the issue or on [Di 5. Add the new provider to the storage test matrix (`TEST_DBS`) and a `make test-NEW_DB_NAME` / `test-cleanup-NEW_DB_NAME` Docker target in the `Makefile`, following the pattern of the existing `test-postgres` / `test-mongodb` targets. -> Note: `go build ./internal/storage/db/NEW_DB_NAME/...` will fail with a `does not implement storage.Provider (missing method ...)` error until every method is implemented — that's the compile-time assertion in `provider.go` doing its job. +> Note: `go test ./internal/storage/db/NEW_DB_NAME/...` will fail to compile with a `does not implement storage.Provider (missing method ...)` error until every method is implemented. This check lives in a `_test` file rather than in `provider.go` itself — `internal/storage` imports every concrete provider (including yours, once step 4 is done), so a same-package assertion would create an import cycle. ### Testing diff --git a/internal/storage/db/provider_template/interface_test.go b/internal/storage/db/provider_template/interface_test.go new file mode 100644 index 000000000..b0dd5785a --- /dev/null +++ b/internal/storage/db/provider_template/interface_test.go @@ -0,0 +1,25 @@ +package provider_template_test + +import ( + "testing" + + "github.com/rs/zerolog" + + "github.com/authorizerdev/authorizer/internal/config" + "github.com/authorizerdev/authorizer/internal/storage" + "github.com/authorizerdev/authorizer/internal/storage/db/provider_template" +) + +// TestImplementsStorageProvider fails to compile — not just to run — the +// moment provider stops satisfying storage.Provider. It lives in the +// _test external package so it can import internal/storage without an +// import cycle (internal/storage will import this package's non-test code +// once it's wired into storage.New(); see provider.go). +func TestImplementsStorageProvider(t *testing.T) { + log := zerolog.Nop() + p, err := provider_template.NewProvider(&config.Config{}, &provider_template.Dependencies{Log: &log}) + if err != nil { + t.Fatalf("NewProvider() error = %v", err) + } + var _ storage.Provider = p +} diff --git a/internal/storage/db/provider_template/provider.go b/internal/storage/db/provider_template/provider.go index 05d22039e..3975edf21 100644 --- a/internal/storage/db/provider_template/provider.go +++ b/internal/storage/db/provider_template/provider.go @@ -5,7 +5,6 @@ import ( "gorm.io/gorm" "github.com/authorizerdev/authorizer/internal/config" - "github.com/authorizerdev/authorizer/internal/storage" ) // Dependencies struct the TODO(replace with new db name) data store provider @@ -20,12 +19,16 @@ type provider struct { db *gorm.DB } -// Compile-time check: provider must implement every method of storage.Provider. -// Deleting or renaming a method here without updating the interface (or vice -// versa) fails the build immediately instead of silently drifting. -var _ storage.Provider = (*provider)(nil) - // NewProvider returns a new provider for your database type. +// +// The parent internal/storage package cannot be imported here to add a +// `var _ storage.Provider = (*provider)(nil)` assertion: internal/storage +// imports every concrete provider package (including this one, once you wire +// it into storage.New()), so importing it back would create an import cycle. +// See interface_test.go for the equivalent check done from an external test +// package instead — run `go test ./internal/storage/db/provider_template/...` +// (or `go build ./...` after wiring into storage.New()) to verify parity with +// storage.Provider. // TODO: change provider struct and NewProvider to use your database client. // // This provider must implement every method of storage.Provider — see that