Documentation
¶
Index ¶
- type PGRoleRepo
- func (r *PGRoleRepo) AssignToUser(ctx context.Context, t tenant.TenantID, userID, roleID string) (bool, error)
- func (r *PGRoleRepo) CountByRole(ctx context.Context, t tenant.TenantID, roleID string) (int, error)
- func (r *PGRoleRepo) CountEffectiveAdmins(ctx context.Context, t tenant.TenantID) (int, error)
- func (r *PGRoleRepo) Create(ctx context.Context, t tenant.TenantID, role *domain.Role) error
- func (r *PGRoleRepo) EffectiveAdminExists(ctx context.Context, t tenant.TenantID) (bool, error)
- func (r *PGRoleRepo) GetByID(ctx context.Context, t tenant.TenantID, id string) (*domain.Role, error)
- func (r *PGRoleRepo) GetByUserID(ctx context.Context, t tenant.TenantID, userID string) ([]*domain.Role, error)
- func (r *PGRoleRepo) ListByUserID(ctx context.Context, t tenant.TenantID, userID string, params query.ListParams) ([]*domain.Role, error)
- func (r *PGRoleRepo) RemoveFromUser(ctx context.Context, t tenant.TenantID, userID, roleID string) error
- func (r *PGRoleRepo) RemoveFromUserIfNotLast(ctx context.Context, t tenant.TenantID, userID, roleID string) (bool, error)
- type PGSetupLock
- type PGUserRepo
- func (r *PGUserRepo) BumpAuthzEpoch(ctx context.Context, t tenant.TenantID, userID string, ...) (int64, error)
- func (r *PGUserRepo) Create(ctx context.Context, t tenant.TenantID, user *domain.User) error
- func (r *PGUserRepo) Delete(ctx context.Context, t tenant.TenantID, id string) error
- func (r *PGUserRepo) GetByIDForUpdate(ctx context.Context, t tenant.TenantID, id string) (*domain.User, error)
- func (r *PGUserRepo) GetByIDInTenant(ctx context.Context, t tenant.TenantID, id string) (*domain.User, error)
- func (r *PGUserRepo) GetByUsername(ctx context.Context, t tenant.TenantID, username string) (*domain.User, error)
- func (r *PGUserRepo) GetByUsernameForUpdate(ctx context.Context, t tenant.TenantID, username string) (*domain.User, error)
- func (r *PGUserRepo) UpdateLockState(ctx context.Context, t tenant.TenantID, userID string, ...) error
- func (r *PGUserRepo) UpdateLockoutFields(ctx context.Context, t tenant.TenantID, user *domain.User) error
- func (r *PGUserRepo) UpdatePassword(ctx context.Context, t tenant.TenantID, userID string, newHash string, ...) (int64, error)
- func (r *PGUserRepo) UpdatePasswordResetFlag(ctx context.Context, t tenant.TenantID, userID string, required bool, ...) error
- func (r *PGUserRepo) UpdateProfile(ctx context.Context, t tenant.TenantID, userID string, ...) (*domain.User, error)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type PGRoleRepo ¶
type PGRoleRepo struct {
// contains filtered or unexported fields
}
PGRoleRepo is the cell-private PostgreSQL implementation of ports.RoleRepository. It reads/writes the `roles` and `role_assignments` tables (migration 019).
func NewPGRoleRepo ¶
func NewPGRoleRepo( pool *pgxpool.Pool, txRunner persistence.TxRunner, clk clock.Clock, ) (*PGRoleRepo, error)
NewPGRoleRepo constructs a PGRoleRepo. Fails fast on nil dependencies.
func (*PGRoleRepo) AssignToUser ¶
func (r *PGRoleRepo) AssignToUser(ctx context.Context, t tenant.TenantID, userID, roleID string) (bool, error)
AssignToUser assigns a role to a user within a tenant. Idempotent: returns changed=false when the assignment already existed. Returns ErrAuthRoleNotFound when the role does not exist (FK on role_id). Returns ErrAuthUserNotFound when the user does not exist (FK on user_id). Fallback for unknown FK violations returns ErrAuthRoleNotFound.
func (*PGRoleRepo) CountByRole ¶
func (r *PGRoleRepo) CountByRole(ctx context.Context, t tenant.TenantID, roleID string) (int, error)
CountByRole returns the total count of role_assignments for roleID regardless of user status. Used for bootstrap idempotency (adminprovision); MUST NOT be used as the last-admin invariant counter — see CountEffectiveAdmins.
func (*PGRoleRepo) CountEffectiveAdmins ¶
CountEffectiveAdmins returns the number of users that are simultaneously status='active' AND hold the admin role. Satisfies the domain. EffectiveAdminCounter sealed interface (S4.0 invariant counter).
Acquires advisory xact lock 'gocell.accesscore.last_admin' inside the CTE so concurrent guard paths (this read, removeIfNotLastSQL CTE, and the migration-024 trigger on users) serialize.
CONTRACT (runtime-enforced): Must be called within an open write transaction. The advisory lock is transaction-scoped (pg_advisory_xact_lock) and releases on commit/rollback; outside-transaction callers would acquire and immediately release the lock, defeating the invariant guarantee. The function fail-fasts with ErrInternal when no pgx.Tx is present under kernel/persistence.TxCtxKey — same shape as PGSetupLock.Acquire / OutboxWriter.Write. If a lock-free read is ever needed for diagnostics or observability, add a dedicated variant without the advisory-lock CTE rather than relaxing this contract.
func (*PGRoleRepo) Create ¶
Create upserts a role (seed/bootstrap semantics: existing role is overwritten). The SQL uses ON CONFLICT (tenant_id, id) DO UPDATE — the composite PK on `roles`. A unique-violation (SQLSTATE 23505) is therefore structurally impossible here; any error is a genuine infra failure, classified as ErrInternal.
func (*PGRoleRepo) EffectiveAdminExists ¶
EffectiveAdminExists implements ports.RoleRepository — see the port godoc for fast-path semantics. Pool-driven (no tx required, no advisory lock).
func (*PGRoleRepo) GetByID ¶
func (r *PGRoleRepo) GetByID(ctx context.Context, t tenant.TenantID, id string) (*domain.Role, error)
GetByID fetches a role by composite (tenant_id, id) key. Returns ErrAuthRoleNotFound when absent.
func (*PGRoleRepo) GetByUserID ¶
func (r *PGRoleRepo) GetByUserID(ctx context.Context, t tenant.TenantID, userID string) ([]*domain.Role, error)
GetByUserID returns all roles assigned to the user in the given tenant. Returns an empty slice when the user has no roles (mirrors mem behavior).
func (*PGRoleRepo) ListByUserID ¶
func (r *PGRoleRepo) ListByUserID(ctx context.Context, t tenant.TenantID, userID string, params query.ListParams) ([]*domain.Role, error)
ListByUserID returns a paginated, sorted list of roles assigned to userID. Mirrors the mem implementation: loads all roles for the user, then applies query.Sort and query.ApplyCursor in Go.
func (*PGRoleRepo) RemoveFromUser ¶
func (r *PGRoleRepo) RemoveFromUser(ctx context.Context, t tenant.TenantID, userID, roleID string) error
RemoveFromUser removes a role assignment. Idempotent — no error when the assignment did not exist.
SAFETY: callers needing the at-least-one-effective-admin invariant (revoke admin role, demote sole admin) must use RemoveFromUserIfNotLast, which combines the application-layer CTE check with the migration-024 trigger safety net. RemoveFromUser issues a plain DELETE and relies SOLELY on the trigger to enforce the invariant — when the trigger blocks the DELETE, isLastAdminProtected translates SQLSTATE P0001 into ErrAuthLastAdminProtected (HTTP 403). The single legitimate caller passing roleID == auth.RoleAdmin is adminprovision.Compensate, which runs after a setup failure: if the just-provisioned admin is the only effective admin, the trigger correctly blocks the cleanup and leaves the operator with a usable account rather than an unusable system.
func (*PGRoleRepo) RemoveFromUserIfNotLast ¶
func (r *PGRoleRepo) RemoveFromUserIfNotLast(ctx context.Context, t tenant.TenantID, userID, roleID string) (bool, error)
RemoveFromUserIfNotLast removes a role assignment with admin-scoped last-effective-admin protection (ADR-admin-invariant §3.2, S4.0). For roleID == auth.RoleAdmin the CTE acquires an advisory xact lock plus FOR UPDATE OF u on the other active-admin users, atomically serializing concurrent revocations / locks / deletes. The DELETE only fires when at least one OTHER effective admin (status='active' AND admin) remains. For any other roleID the operation is a plain idempotent DELETE (matches the migration-024 trigger scope: `IF OLD.role_id <> 'admin' THEN RETURN OLD;`).
CONTRACT (runtime-enforced for admin path): the admin-role branch must be called within an open write transaction so the CTE's pg_advisory_xact_lock scopes to the caller's tx, not a one-shot pool connection. The function fail-fasts with ErrInternal when roleID equals auth.RoleAdmin and no pgx.Tx is present under kernel/persistence.TxCtxKey. The non-admin path stays pool-driven (no lock involved); calling it outside a tx is fine.
Returns:
- (true, nil) — role was held and successfully removed.
- (false, nil) — role was not held (idempotent no-op).
- (false, ErrAuthLastAdminProtected) — admin path only; removal would leave zero effective admins. Both the app-level CTE detect path and the DB trigger safety-net path return the same errcode so client handlers match a single business invariant.
type PGSetupLock ¶
type PGSetupLock struct {
// contains filtered or unexported fields
}
PGSetupLock serializes the admin provisioning path across concurrent processes via PostgreSQL advisory locks. It uses pg_advisory_xact_lock so the lock is automatically released at transaction commit or rollback — no explicit Release is required or possible.
Multi-pod deployments that both hit POST /setup/admin before any admin exists now block on this lock inside the same transaction that writes the admin user and emits the outbox event. Only the first committer persists; the second reads zero-count at fast-path and returns OutcomeAlreadyExists / 410 Gone.
The advisory lock key is derived from the well-known sentinel string "accesscore.admin.setup" via hashtextextended(..., 0). This is stable across all PG versions and does not collide with table OIDs.
func NewPGSetupLock ¶
func NewPGSetupLock(txRunner persistence.TxRunner) (*PGSetupLock, error)
NewPGSetupLock constructs a PGSetupLock. Returns an error when txRunner is nil so composition roots fail at construction time rather than at the first Acquire.
func (*PGSetupLock) Acquire ¶
func (l *PGSetupLock) Acquire(ctx context.Context) error
Acquire blocks until the advisory lock is granted within the ambient transaction. ctx must carry a live pgx.Tx injected by txRunner.RunInTx (stored under kernel/persistence.TxCtxKey). If no transaction is present, Acquire returns ErrInternal because the xact-scoped lock semantics require an enclosing transaction.
type PGUserRepo ¶
type PGUserRepo struct {
// contains filtered or unexported fields
}
PGUserRepo is the cell-private PostgreSQL implementation of ports.UserRepository. It reads/writes the `users` table (migration 017).
Transaction contract — dual-signal pattern (S3+S5 PR #449 round-3 clarification). The txRunner field is a *construction-time policy declaration*: it fail-fasts at NewPGUserRepo when the L2 caller has not wired a real TxRunner (single source of truth for "this repo is intended for L2-atomic call sites"). The repo methods themselves do NOT invoke txRunner.RunInTx directly because all current write paths are single-statement (Create / Update / Delete). Instead, methods extract any ambient pgx.Tx from ctx via kernel/persistence.TxCtxKey (the value stored by adapters/postgres.TxManager.RunInTx); when no tx is in ctx the methods fall through to the pool. The setup service wraps Create + outbox.Write in a single TxManager.RunInTx call so both writes share the tx that the package-local typed executor picks up here.
Compare runtime/auth/refresh adapters/postgres/refresh_store.go where txRunner IS invoked directly because its multi-statement methods (Peek, Rotate) need an explicit boundary. PGUserRepo's pattern is the "single-statement repo" variant of the same dual-signal contract.
func NewPGUserRepo ¶
func NewPGUserRepo( pool *pgxpool.Pool, txRunner persistence.TxRunner, clk clock.Clock, ) (*PGUserRepo, error)
NewPGUserRepo constructs a PGUserRepo. Fails fast on nil dependencies.
func (*PGUserRepo) BumpAuthzEpoch ¶
func (r *PGUserRepo) BumpAuthzEpoch(ctx context.Context, t tenant.TenantID, userID string, tok credentialfence.FenceToken) (int64, error)
BumpAuthzEpoch atomically increments users.authz_epoch by 1 and returns the new value. It must be called inside an ambient transaction — the credential-invalidation funnel entry point guarantees this. Returns ErrAuthUserNotFound (KindNotFound) when no row matches userID.
fail-fast enforced: calling without an ambient transaction returns an error (errcode.ErrInternal); without a transaction the row update is auto-committed before the caller's surrounding atomic sequence completes.
func (*PGUserRepo) Create ¶
Create inserts a new user row. Returns ErrAuthUserDuplicate on unique constraint violation (username or email already taken within the tenant).
func (*PGUserRepo) Delete ¶
Delete removes a user row. Returns ErrAuthUserNotFound when no row matched. Returns ErrAuthLastAdminProtected (403) when the migration-024 trigger on `users` rejects the delete because the row is the sole effective admin — same errcode + message as PGUserRepo.Update / domain.LastAdminGuard so client handlers match a single business invariant regardless of which layer caught the violation.
func (*PGUserRepo) GetByIDForUpdate ¶
func (r *PGUserRepo) GetByIDForUpdate(ctx context.Context, t tenant.TenantID, id string) (*domain.User, error)
GetByIDForUpdate (S4d) — see ports.UserRepository godoc. Acquires a row lock via SELECT ... FOR UPDATE. The lookup is tenant-scoped (WHERE tenant_id=$1 AND id=$2) to prevent a cross-tenant read leak on the FOR UPDATE path. Tenant validation is performed inside getForUpdateBy (the shared convergence point for both lookup kinds); no redundant Validate call here.
func (*PGUserRepo) GetByIDInTenant ¶
func (r *PGUserRepo) GetByIDInTenant(ctx context.Context, t tenant.TenantID, id string) (*domain.User, error)
GetByIDInTenant fetches a user by primary key within tenant t. Returns ErrAuthUserNotFound when the row is absent OR belongs to a different tenant — both cases produce pgx.ErrNoRows from `WHERE id=$1 AND tenant_id=$2`.
func (*PGUserRepo) GetByUsername ¶
func (r *PGUserRepo) GetByUsername(ctx context.Context, t tenant.TenantID, username string) (*domain.User, error)
GetByUsername fetches a user by (tenant_id, username). Returns ErrAuthUserNotFound when absent.
func (*PGUserRepo) GetByUsernameForUpdate ¶
func (r *PGUserRepo) GetByUsernameForUpdate(ctx context.Context, t tenant.TenantID, username string) (*domain.User, error)
GetByUsernameForUpdate (S4d) — see ports.UserRepository godoc. Tenant validation is performed inside getForUpdateBy; no redundant Validate call here.
func (*PGUserRepo) UpdateLockState ¶
func (r *PGUserRepo) UpdateLockState( ctx context.Context, t tenant.TenantID, userID string, status domain.UserStatus, now time.Time, ) error
UpdateLockState writes status + updated_at, atomically resetting the three auto-lockout columns when status == StatusActive (SQL CASE clause). Returns ErrAuthLastAdminProtected when the migration-024 trigger blocks the change.
func (*PGUserRepo) UpdateLockoutFields ¶
func (r *PGUserRepo) UpdateLockoutFields(ctx context.Context, t tenant.TenantID, user *domain.User) error
UpdateLockoutFields persists the auto-lockout state (failed_login_count, last_failed_at, locked_until, updated_at) for an existing user. Called exclusively from cells/accesscore/internal/accountlockout inside the sessionlogin tx. Returns ErrAuthUserNotFound when no row matched.
Schema note: this method touches only the four lockout-bookkeeping columns plus updated_at. The status / authz_epoch / password_hash columns remain owned by authzmutate.Mutator.ApplyInTx (Update / BumpAuthzEpoch) and the password-change path (UpdatePassword); they are NOT mutated here.
func (*PGUserRepo) UpdatePassword ¶
func (r *PGUserRepo) UpdatePassword( ctx context.Context, t tenant.TenantID, userID string, newHash string, resetRequired bool, expectedPV int64, ) (int64, error)
UpdatePassword applies a CAS-guarded password write.
It executes updatePasswordSQL (WHERE id=$4 AND password_version=$5). If 0 rows were affected the method distinguishes "user absent" from "version mismatch" via a follow-up GetByID probe — callers receive ErrAuthUserNotFound or ErrVersionConflict respectively. On success the new password_version is returned.
func (*PGUserRepo) UpdatePasswordResetFlag ¶
func (r *PGUserRepo) UpdatePasswordResetFlag( ctx context.Context, t tenant.TenantID, userID string, required bool, now time.Time, ) error
UpdatePasswordResetFlag writes password_reset_required + updated_at only.
func (*PGUserRepo) UpdateProfile ¶
func (r *PGUserRepo) UpdateProfile( ctx context.Context, t tenant.TenantID, userID string, name, email *domain.NonEmpty, now time.Time, ) (*domain.User, error)
UpdateProfile writes username / email / updated_at. Nil name or email leaves that column unchanged (SQL COALESCE). Returns the post-write *domain.User reconstituted from the RETURNING row (explicit user column list); caller MUST use it as the new system-of-record aggregate.