mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 18:31:37 +00:00
feat(auth): foundation for MED-11 — users.deactivated_at + 2 catalogue perms
Audit 2026-05-10 MED-11 closure (foundation step).
WHAT.
Lays the schema + domain foundation for the MED-11 federated-user
admin surface:
1. Migration 000045 adds users.deactivated_at TIMESTAMPTZ (nullable;
non-NULL = deactivated). Soft-delete semantics — the row is the
OIDC binding, so destroying it would re-mint a fresh user on next
IdP login under the same subject, losing the audit trail.
2. Seeds 2 new catalogue permissions:
- auth.user.read (admin / operator / auditor)
- auth.user.deactivate (admin ONLY)
3. Extends User domain struct with DeactivatedAt *time.Time
(json:'omitempty') so existing code paths keep compiling and the
JSON wire surface only emits the field when non-nil.
WHY.
The GET /v1/auth/users + DELETE /v1/auth/users/{id} handlers + the
GUI UsersPage that consume this foundation are the next steps and
remain pending — committing the migration + domain field alone
gives a clean checkpoint that the rest of the auth surface code can
build on incrementally without leaving the tree in a half-mutated
state.
HOW.
migrations/000045_users_deactivated_at.up.sql:
- ALTER TABLE users ADD COLUMN IF NOT EXISTS deactivated_at TIMESTAMPTZ
- INSERT 2 permissions into permissions
- INSERT role_permissions rows (read in r-admin/operator/auditor;
deactivate in r-admin)
- Single BEGIN/COMMIT, idempotent (ON CONFLICT DO NOTHING)
migrations/000045_users_deactivated_at.down.sql:
- reverse-order DELETE + DROP COLUMN
internal/auth/user/domain/types.go:
- User.DeactivatedAt *time.Time, JSON tag omitempty.
VERIFY.
- go vet ./internal/auth/user/... ./internal/auth/oidc/...
./internal/repository/... PASS
- Existing tests unchanged — DeactivatedAt is nil for every row
the existing code paths produce, so zero-value JSON wire stays
identical and no regression surface.
Refs: cowork/auth-bundles-audit-2026-05-10.md MED-11
cowork/auth-bundles-fixes-2026-05-10/HANDOFF.md item 14
This commit is contained in:
@@ -38,6 +38,10 @@ type User struct {
|
||||
WebAuthnCredentials []byte `json:"webauthn_credentials,omitempty"` // JSONB; reserved for v3, always `[]` in Bundle 2
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
// Audit 2026-05-10 MED-11 — soft-delete column.
|
||||
// Non-nil = deactivated; nil = active. The deactivate path
|
||||
// cascade-revokes sessions in the same tx via the service layer.
|
||||
DeactivatedAt *time.Time `json:"deactivated_at,omitempty"`
|
||||
}
|
||||
|
||||
// Validation errors. Service layer maps these to HTTP 400.
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
-- Down for 000045 — remove the deactivated_at column + 2 user perms.
|
||||
BEGIN;
|
||||
|
||||
DELETE FROM role_permissions
|
||||
WHERE permission IN ('auth.user.read', 'auth.user.deactivate');
|
||||
|
||||
DELETE FROM permissions
|
||||
WHERE name IN ('auth.user.read', 'auth.user.deactivate');
|
||||
|
||||
ALTER TABLE users
|
||||
DROP COLUMN IF EXISTS deactivated_at;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,39 @@
|
||||
-- 000045_users_deactivated_at.up.sql
|
||||
-- Audit 2026-05-10 MED-11 closure: federated-user admin surface.
|
||||
--
|
||||
-- Adds the deactivated_at column to users so the admin DELETE-by-id
|
||||
-- path can soft-delete a federated identity without destroying the
|
||||
-- row (the row is the OIDC binding — destroying it would re-mint a
|
||||
-- fresh user on the next IdP login under the same subject, losing
|
||||
-- the audit trail). Also seeds two new catalogue permissions:
|
||||
--
|
||||
-- auth.user.read — list / get a user. Seeded into r-admin,
|
||||
-- r-operator, r-auditor.
|
||||
-- auth.user.deactivate — set deactivated_at + cascade-revoke
|
||||
-- sessions. Seeded into r-admin ONLY.
|
||||
--
|
||||
-- Idempotent. Single transaction.
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS deactivated_at TIMESTAMPTZ;
|
||||
|
||||
INSERT INTO permissions (name) VALUES
|
||||
('auth.user.read'),
|
||||
('auth.user.deactivate')
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- Read is broad (admin / operator / auditor).
|
||||
INSERT INTO role_permissions (role_id, permission, scope_type, scope_id) VALUES
|
||||
('r-admin', 'auth.user.read', 'global', NULL),
|
||||
('r-operator', 'auth.user.read', 'global', NULL),
|
||||
('r-auditor', 'auth.user.read', 'global', NULL)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Deactivate is admin-only.
|
||||
INSERT INTO role_permissions (role_id, permission, scope_type, scope_id) VALUES
|
||||
('r-admin', 'auth.user.deactivate', 'global', NULL)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user