Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,25 @@ const FIELD: Record<ClassName, string> = { masked: 'role', gated: 'must_change_p
const SUBJECT = { email: 'ald.subject.kq@example.com', name: 'Ald Subject', role: 'aldrolekq' };
const VALUE: Record<ClassName, string | null> = { masked: SUBJECT.role, gated: null, withheld: SUBJECT.email };

/** The explicit rows' events, and the decision keys each may carry. */
/**
* The explicit rows' events, and the decision keys each may carry: `required` is
* written on every row of the event, `optional` only on some (the producer's
* `AdminAuditDecisions` type, `plugin-auth/src/admin-user-endpoints.ts`).
*
* The password set (#23287, ruling C′ item 2, with the revocation ON by default):
* `revokeSessionsAndKeys` is the administrator's switch, written on every forced
* reset. `revokedSessions`, `revokedApiKeys` and `revocationComplete` are what the
* revocation landed, counts only, written only when the switch was on. This
* fixture's reset leaves the switch at its default, so its row carries all three.
*/
const CREATE_EVENT = 'user.admin_created';
const PASSWORD_SET_EVENT = 'user.admin_password_set';
const DECISIONS: Record<string, { required: string[]; optional: string[] }> = {
[CREATE_EVENT]: { required: ['event', 'membershipCreated', 'passwordGenerated', 'placeholderEmail'], optional: ['organizationId'] },
[PASSWORD_SET_EVENT]: { required: ['event', 'passwordGenerated'], optional: [] },
[PASSWORD_SET_EVENT]: {
required: ['event', 'passwordGenerated', 'revokeSessionsAndKeys'],
optional: ['revokedSessions', 'revokedApiKeys', 'revocationComplete'],
},
};

const Anchor = ObjectSchema.create({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
* ## The defect this pins
*
* An admin-provisioned user (`mustChangePassword` defaults to true) is gated out
* of every protected route with `403 PASSWORD_EXPIRED` until they rotate their
* password. On the COOKIE lane the escape hatch worked. On the BEARER lane —
* of every protected route with `403 PASSWORD_RESET_REQUIRED` until they rotate
* their password. (#23287, ruling C′ item 2: the administrator-forced change has
* its own code, scope `credential`; it reused `PASSWORD_EXPIRED`, which stays
* natural expiry under `passwordExpiryDays`, until then.) On the COOKIE lane the
* escape hatch worked. On the BEARER lane —
* the documented API/agent/CLI lane — `/auth/change-password` answered **200**,
* the password really rotated, and nothing else happened: `must_change_password`
* stayed `true`, `password_changed_at` stayed `null`, and the caller stayed
Expand Down Expand Up @@ -225,7 +228,7 @@ describe('#8049: /auth/change-password clears the force-change flag and enforces
const email = `bearer.lane.8049.${lane.name.replace(/[^a-z]/gi, '').toLowerCase()}@example.com`;
let credential = '';

it('an admin-provisioned user is gated out with 403 PASSWORD_EXPIRED', async () => {
it('an admin-provisioned user is gated out with 403 PASSWORD_RESET_REQUIRED', async () => {
const created = await stack.apiAs(adminToken, 'POST', '/auth/admin/create-user', {
email,
name: `Bearer Lane ${lane.name}`,
Expand All @@ -241,9 +244,12 @@ describe('#8049: /auth/change-password clears the force-change flag and enforces

const read = await stack.api('/data/showcase_task?$top=1', { headers: lane.headers(credential) });
// The gate refusal — asserted as code AND status, and distinctly from
// the reuse refusal below (different envelope, different outcome).
// the reuse refusal below (different envelope, different outcome). The
// flag is the administrator's forced change, not expiry: no
// `passwordExpiryDays` is armed here, so the one ruled answer is
// `PASSWORD_RESET_REQUIRED` (ruling C′ item 2).
expect(read.status).toBe(403);
expect((await read.json())?.error?.code).toBe('PASSWORD_EXPIRED');
expect((await read.json())?.error?.code).toBe('PASSWORD_RESET_REQUIRED');
}, 120_000);

it('POST /auth/change-password rotates the password AND clears the force-change flag', async () => {
Expand Down
Loading