DEC-014 — Credential Lifecycle & Recovery Contract (V1 Freeze)
| Item |
Value |
| Decision |
DEC-014 |
| Tranche |
T0-D / T0-U07 |
| Status |
FROZEN for V1 |
| Mode |
Server development /home/scola/odoo |
| Related |
CTRL-006, E2E-GAP-12, STAB-007, QG-03/06/08/09/11/16 |
| Inventory |
T0-D-credential-surface-inventory.md |
This document freezes the chosen Scola V1 credential contract and records rejected alternatives. Implementation must not invent surfaces marked excluded.
Chosen V1 contract (summary)
- Activation / provisioning: provisioned users receive a random high-entropy temporary password with
scola_must_change_password=True, or a recovery token with purpose activation / admin_reset. No shared school defaults (including no GBS/GTS gbs123/gts123).
- Public forgot/reset: anti-enumeration — identical success shape for existing, missing, and disabled accounts.
- Session revoke on successful public reset, authenticated password change (other sessions), admin reset, and deactivate.
- Break-glass / impersonation:
EXCLUDED_AND_DENIED on the customer surface.
- Password expiry: not invented;
EXCLUDED.
- Lockout product: not invented;
EXCLUDED (disabled account is a separate state).
- Credential audit: system-only ACL; no secrets in audit payloads.
A. Initial activation
Chosen
- Invited/provisioned users must not receive a reusable shared/default password.
- V1 activation paths:
- Provisioned user + random temporary password +
must_change_password, then forced change via /api/auth/change-password; or
- Recovery token with purpose
activation or admin_reset (hash-at-rest, single-use, TTL, purpose+user bound).
- Token properties when used: expires; single use; stored as hash only; never logged raw; invalidated after success; cannot activate another account; generic denial on invalid/expired/replayed.
- After credential establishment, normal login/session contract (T0-B) applies.
Rejected
| Alternative |
Why rejected |
Shared school temps (gbs123/gts123 or similar) |
Predictable, cross-user reusable, violates least surprise and QG secret hygiene |
| Permanent known default left on account |
Stale credential authority |
| Emailing long-lived plaintext without force-change |
Credential remains reusable without bound lifecycle |
| Treating SPMB OTP as password-reset activation |
Different purpose/channel; must stay separate |
B. Authenticated password change
Chosen
- Authenticated user proves current password via Odoo
change_password.
- On success:
- credential updates atomically;
scola_must_change_password cleared;
- other sessions revoked; current session sid kept;
- active-role context remains valid only for the retained session after recompute on next auth payload;
- audit
password_change.
- Min length 8; new ≠ old.
Rejected
| Alternative |
Why rejected |
| Change without current-credential proof |
Hijack via stolen session alone without re-auth proof |
| Leave other sessions alive |
Stale credential authority after compromise/reset |
Capability-free write on res.users for all roles |
Privilege expansion |
C. Forgotten-password recovery
Chosen
- Public endpoints:
POST /api/auth/password-reset/request
POST /api/auth/password-reset/confirm
- FE:
/forgot-password, /reset-password.
- Anti-enumeration: response for existing active, nonexistent, and disabled accounts uses the same success shape (generic message +
correlation_id). Existence must not leak via body/code/message.
- Recovery token: high entropy; single use; TTL (
scola.credential.reset_ttl_seconds, default 3600); account + purpose password_reset bound; superseded on re-issue; hash-at-rest; generic failure on invalid/expired/replay.
- Successful confirm: set password, clear must_change, revoke all sessions, audit.
Rejected
| Alternative |
Why rejected |
| Distinct “user not found” / “disabled” messages |
User enumeration |
| Predictable tokens (uid/timestamp) |
Forgeable |
| Storing raw tokens |
Secret leakage risk |
| Allowing confirm to mutate role/company |
Privilege escalation via recovery |
D. Administrative reset
Chosen
- Retained:
POST /api/v1/users/<id>/reset-password (capability/admin scoped).
- Admin never retrieves the user’s existing password.
- Operation: set server-generated random temporary secret +
must_change_password=True + revoke all sessions + audit admin_reset.
- Caller-chosen / admin-selected passwords are rejected.
- One-time reveal of generated temp in response only; never log password.
- Prefer random temp + force change; recovery-token purpose
admin_reset remains available in the token service for future channeling without inventing a second admin UX in V1.
Rejected
| Alternative |
Why rejected |
| Reset to a permanent known school default |
Shared secret regression |
| Reset without must_change |
User can keep attacker-chosen or admin-chosen password without acknowledgement |
| Reset without session revoke |
Old sessions retain authority |
| Unscoped cross-tenant admin reset |
Tenant isolation violation |
E. Locked / disabled account
Chosen
- Do not equate: disabled user ↔ credential failure ↔ expired token ↔ revoked role ↔ revoked session.
- Disabled (
active=False): soft-deactivate path revokes sessions; public reset request is a noop that still returns the generic success shape.
- Account lockout (failed-attempt lock + customer unlock) is not a V1 feature — EXCLUDED. Do not invent unlock workflows merely to fill a matrix.
Rejected
| Alternative |
Why rejected |
| Invent customer lockout/unlock for V1 completeness |
Scope creep; false security theater without approved policy |
| Same error text for disable vs wrong password on login |
May be desirable later; not required here — keep distinct from public reset anti-enumeration contract |
F. Password expiry
Chosen
- No periodic password expiry in V1.
- Status: EXCLUDED / NOT_IMPLEMENTED.
- Do not invent cron-based expiry or “password expired” customer journeys.
Rejected
| Alternative |
Why rejected |
| Add expiry to “look complete” vs DEC backlog |
No approved business/security requirement; increases support load and false failures |
G. Break-glass / impersonation
Chosen
- Customer-facing generic impersonation / login-as / credential break-glass:
EXCLUDED_AND_DENIED.
- Existing
_is_break_glass_admin for school-unit platform mutate is INTERNAL_OPERATIONAL RBAC, not a customer recovery or impersonation surface.
- If a future provider-only emergency-access mechanism is approved, it must be separate from school roles, with elevated authority, reason, bounded lifetime, audit, tenant scope, identity attribution, and explicit termination. None is created in V1 by this freeze.
Rejected
| Alternative |
Why rejected |
| Silent login-as for school admins |
Unattributed privilege; violates DEC intent |
| Reusing unit break-glass RBAC as credential recovery |
Confuses ops elevation with account takeover |
Cross-cutting rules (normative)
| Topic |
V1 rule |
| Shared defaults |
Forbidden on supported paths |
| Public reset anti-enumeration |
Required |
| Session revoke |
On public reset, change-password (others), admin reset, deactivate |
| Break-glass / impersonation (customer) |
EXCLUDED_AND_DENIED |
| Password expiry |
EXCLUDED (not invented) |
| Lockout product |
EXCLUDED (not invented) |
| Audit ACL |
scola.credential.audit.event and recovery tokens: system-only |
| Audit contents |
No passwords, raw tokens, session cookies, or full secrets |
| SPMB OTP |
Separate from password-reset tokens |
| Foundation dead reset route |
Not resurrected; use canonical admin reset |
Rejected global alternatives
| Proposal |
Disposition |
| Rely on Odoo native signup/reset UI as supported Scola lifecycle |
Rejected — not assumed canonical |
| Multiple parallel forgot-password APIs |
Rejected — one public pair only |
Foundation /api/v1/foundation/hr/users/reset-password without controller |
Leave dead / remove from product exposure |
| Superadmin as normal E2E actor |
Rejected (Golden Tenant policy) |
Approval record
| Approver |
Date |
Scope |
Source |
| Requesting user (conversation approval) |
2026-08-10 |
DEC-014 V1 contract: server-generated temporary credential + must_change; canonical Scola recovery/change flows; native Odoo lifecycle denied; expiry and customer lockout excluded |
Current conversation |
The requesting user explicitly replaced the previously required formal multi-role
sign-off with this approval record. No additional names or signatures are inferred.
Legacy sign-off fields (superseded by the approval record above)
| Role |
Name |
Date |
Signature |
| Product |
|
|
|
| Identity/RBAC |
|
|
|
| Security |
|
|
|
| Operations/Support |
|
|
|
| DPO (recovery data) |
|
|
|
Freeze applies to engineering immediately; the approval record above replaces the
previous formal-signature requirement in this decision workflow.