@jwilleke/ngdpbase-types 5.27.0 → 5.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/packages/ngdpbase-types/index.d.ts +4 -1
- package/lib/src/context/ActorContext.d.ts +15 -2
- package/lib/src/context/ActorContext.js +20 -0
- package/lib/src/context/ApiContext.d.ts +17 -4
- package/lib/src/context/ApiContext.js +22 -6
- package/lib/src/ice/runImport.d.ts +26 -0
- package/lib/src/ice/types.d.ts +29 -0
- package/lib/src/managers/AddonsManager.d.ts +7 -3
- package/lib/src/managers/AssetManager.d.ts +7 -3
- package/lib/src/managers/ExportManager.js +5 -0
- package/lib/src/managers/ImportManager.d.ts +57 -8
- package/lib/src/managers/OrganizationManager.d.ts +5 -3
- package/lib/src/managers/PageManager.d.ts +32 -0
- package/lib/src/managers/PersonManager.d.ts +48 -14
- package/lib/src/managers/RoleManager.d.ts +79 -19
- package/lib/src/managers/UserManager.d.ts +66 -2
- package/lib/src/security/PolicyDecisionPoint.d.ts +18 -9
- package/lib/src/security/PolicyInformationPoint.d.ts +25 -10
- package/lib/src/types/Consent.js +22 -0
- package/lib/src/types/User.d.ts +1 -1
- package/lib/src/utils/auditEventNames.generated.d.ts +10 -10
- package/lib/src/utils/auditEventNames.generated.js +10 -10
- package/lib/src/utils/contactAccess.d.ts +30 -0
- package/lib/src/utils/keyedSerial.d.ts +31 -0
- package/lib/src/utils/privateStoreAccess.js +1 -3
- package/lib/src/utils/roleChangeError.d.ts +25 -0
- package/lib/src/utils/subjectMayDo.d.ts +2 -2
- package/package.json +1 -1
|
@@ -12,6 +12,9 @@ declare class RoleManager extends BaseManager {
|
|
|
12
12
|
private storageDir;
|
|
13
13
|
private memberCache;
|
|
14
14
|
private byOrgPositionCache;
|
|
15
|
+
private cacheGeneration;
|
|
16
|
+
/** #2015: one membership change at a time per role record, read included. */
|
|
17
|
+
private readonly membership;
|
|
15
18
|
private metricsRef;
|
|
16
19
|
constructor(engine: WikiEngine);
|
|
17
20
|
initialize(config?: Record<string, unknown>): Promise<void>;
|
|
@@ -42,7 +45,8 @@ declare class RoleManager extends BaseManager {
|
|
|
42
45
|
backupFolders(): BackupFolder[];
|
|
43
46
|
/** Provider accessor, mainly for tests. */
|
|
44
47
|
getProvider(): RoleProvider | null;
|
|
45
|
-
|
|
48
|
+
/** One role record by `@id`: its members are identity records, so `contact-read`, or core's own work (#2018). */
|
|
49
|
+
getById(ctx: ActorContext, id: string): Promise<Role | null>;
|
|
46
50
|
getByOrgAndPosition(organizationId: string, namedPosition: string): Promise<Role | null>;
|
|
47
51
|
listByMember(personId: string): Promise<Role[]>;
|
|
48
52
|
list(): Promise<Role[]>;
|
|
@@ -63,10 +67,16 @@ declare class RoleManager extends BaseManager {
|
|
|
63
67
|
* The pseudo-roles `'Authenticated'` and `'All'` are NOT added here — the
|
|
64
68
|
* caller adds those when constructing `userContext.roles`.
|
|
65
69
|
*
|
|
66
|
-
* Returns `[]` when PersonManager is unavailable,
|
|
67
|
-
*
|
|
70
|
+
* Returns `[]` when PersonManager is unavailable, there is no account by
|
|
71
|
+
* that username, no record lists them, or the lookup throws. An account
|
|
72
|
+
* missing its identity record gets it here (#2037).
|
|
73
|
+
*
|
|
74
|
+
* `ctx` is who is asking (#2018), decided by PersonManager's door: a user
|
|
75
|
+
* about themselves under `profile-manage`, about anyone else under
|
|
76
|
+
* `contact-read`; core's own work passes the system context. A refusal is
|
|
77
|
+
* thrown as `ContactRefusedError`, never answered as "no roles".
|
|
68
78
|
*/
|
|
69
|
-
resolveUserRoles(username: string): Promise<string[]>;
|
|
79
|
+
resolveUserRoles(ctx: ActorContext, username: string): Promise<string[]>;
|
|
70
80
|
/**
|
|
71
81
|
* The `required-aal` levels the OPERATOR set in their own config (#448,
|
|
72
82
|
* #1523), as opposed to the shipped defaults. An operator's level that no
|
|
@@ -86,15 +96,26 @@ declare class RoleManager extends BaseManager {
|
|
|
86
96
|
* `anonymous`) requires nothing beyond being signed in.
|
|
87
97
|
*/
|
|
88
98
|
roleRequiredAal(): Record<string, 1 | 2 | 3>;
|
|
89
|
-
|
|
90
|
-
|
|
99
|
+
/** Whether `username` holds `roleName` now, asked by `ctx`; decided as {@link resolveUserRoles} (#2018). */
|
|
100
|
+
hasRole(ctx: ActorContext, username: string, roleName: string): Promise<boolean>;
|
|
101
|
+
/**
|
|
102
|
+
* Assign one role to an existing account, and record it (#1204). A role
|
|
103
|
+
* that cannot be given is refused (#2015): the error is thrown, admins are
|
|
104
|
+
* notified, and the attempt is recorded as a failed user-edit.
|
|
105
|
+
*/
|
|
91
106
|
assignRole(username: string, roleName: string, ctx: ActorContext): Promise<boolean>;
|
|
92
|
-
/** Remove one role from an existing account, and record it (#1204). */
|
|
107
|
+
/** Remove one role from an existing account, and record it (#1204); refused like `assignRole` when it cannot be done. */
|
|
93
108
|
removeRole(username: string, roleName: string, ctx: ActorContext): Promise<boolean>;
|
|
94
109
|
/**
|
|
95
110
|
* Make the user's memberships go from `oldRoles` to `newRoles`. Used by
|
|
96
111
|
* UserManager when an account is created or edited; the account write
|
|
97
112
|
* records its own audit event, so this records none.
|
|
113
|
+
*
|
|
114
|
+
* All or nothing (#2015): when any change cannot be made, the ones already
|
|
115
|
+
* made are undone and a `RoleChangeError` is thrown, after it is logged as
|
|
116
|
+
* an error and admins are notified. That includes a change that cannot even
|
|
117
|
+
* be attempted: no PersonManager, no Person record for the username, or no
|
|
118
|
+
* anchor Organization.
|
|
98
119
|
*/
|
|
99
120
|
applyRoleDiff(username: string, oldRoles: string[], newRoles: string[]): Promise<void>;
|
|
100
121
|
/**
|
|
@@ -108,28 +129,67 @@ declare class RoleManager extends BaseManager {
|
|
|
108
129
|
* a role change and is recorded as one, under the system context `ctx`,
|
|
109
130
|
* deferred until the audit sink is up when this runs at boot.
|
|
110
131
|
*
|
|
132
|
+
* #2015: each account gets the role or not. One that does not is recorded as
|
|
133
|
+
* a failed user-edit, and every miss is an error in the log and one error
|
|
134
|
+
* notification for admins naming the accounts to fix by hand. The boot goes
|
|
135
|
+
* on: nobody asked, so there is nobody to refuse.
|
|
136
|
+
*
|
|
111
137
|
* @returns how many accounts were given the role (0 once it has been given)
|
|
112
138
|
*/
|
|
113
139
|
grantToEveryAccountOnce(roleName: string, usernames: readonly string[], ctx: JobContext): Promise<number>;
|
|
114
140
|
/**
|
|
115
141
|
* Move every membership held by one Person `@id` to another (#1930): when
|
|
116
142
|
* PersonManager joins an account to an existing person, the roles the
|
|
117
|
-
* account's person held go with the account.
|
|
118
|
-
*
|
|
143
|
+
* account's person held go with the account. Returns how many records
|
|
144
|
+
* changed. PersonManager's join records the change. All or nothing (#2015).
|
|
119
145
|
*/
|
|
120
146
|
moveMemberships(fromPersonId: string, toPersonId: string): Promise<number>;
|
|
121
|
-
/** Remove a deleted account's Person from every role it held. */
|
|
122
|
-
removeAllMemberships(username: string): Promise<void>;
|
|
123
147
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* role
|
|
129
|
-
|
|
148
|
+
* Remove a deleted account's Person from every role it held. All or nothing
|
|
149
|
+
* (#2015): when any removal fails the others are put back and it throws, so
|
|
150
|
+
* the account delete is refused. Returns the undo, for a delete that fails
|
|
151
|
+
* after this: it gives every role back. An account with no Person holds no
|
|
152
|
+
* role, so there is nothing to remove.
|
|
153
|
+
*/
|
|
154
|
+
removeAllMemberships(username: string): Promise<() => Promise<void>>;
|
|
155
|
+
/** The roles `username` gains and loses, all or nothing; `notify` false lets a batch send one notice. */
|
|
156
|
+
private changeRoles;
|
|
157
|
+
/**
|
|
158
|
+
* The Person `@id` and anchor Organization a username's role changes need
|
|
159
|
+
* (#1027). Either missing refuses the change (#2015): an account must not
|
|
160
|
+
* be left without the role it was given, nor keep one it was meant to lose.
|
|
161
|
+
*/
|
|
162
|
+
private membershipTarget;
|
|
163
|
+
/** One member change and its inverse. */
|
|
164
|
+
private memberChange;
|
|
165
|
+
/**
|
|
166
|
+
* Make each change in turn (#2015). When one fails, undo the ones made, in
|
|
167
|
+
* reverse, and throw. Returns the changes made, for a later undo.
|
|
168
|
+
*/
|
|
169
|
+
private allOrNothing;
|
|
170
|
+
/** Undo `done`, newest first; returns the labels that could not be undone. */
|
|
171
|
+
private undoAll;
|
|
172
|
+
/**
|
|
173
|
+
* A role change that did not happen is always an error (operator,
|
|
174
|
+
* 2026-10-23): logged at error level, and an error notification for admins
|
|
175
|
+
* unless the caller sends one for a whole batch. Returns the error to throw.
|
|
176
|
+
*/
|
|
177
|
+
private roleFailure;
|
|
178
|
+
/** An error notification for admins; a notification that cannot be sent is logged, never thrown. */
|
|
179
|
+
private reportToAdmins;
|
|
180
|
+
/** #2015: the membership queue's key for one role record. */
|
|
181
|
+
private membershipKey;
|
|
182
|
+
/**
|
|
183
|
+
* Make `personId` a member of the role record, or not, one change at a time
|
|
184
|
+
* per record (#2015). The record is read inside the queue, so a change never
|
|
185
|
+
* works from a member list another change has since replaced. A missing
|
|
186
|
+
* record is created in `createIn` for an add; with no `createIn` an add to a
|
|
187
|
+
* missing record throws, and a removal from one changes nothing. False,
|
|
188
|
+
* writing nothing, when the person already is (or is not) a member.
|
|
130
189
|
*/
|
|
131
|
-
private
|
|
132
|
-
|
|
190
|
+
private setMember;
|
|
191
|
+
/** assignRole / removeRole: the change, recorded as a user-edit whether it happened or not (#1204, #2015). */
|
|
192
|
+
private recordedRoleChange;
|
|
133
193
|
/**
|
|
134
194
|
* The role record for (installOrg, namedPosition), created when missing.
|
|
135
195
|
*
|
|
@@ -153,7 +153,7 @@ interface UserCreateInput {
|
|
|
153
153
|
/**
|
|
154
154
|
* User update input data
|
|
155
155
|
*/
|
|
156
|
-
interface UserUpdateInput {
|
|
156
|
+
export interface UserUpdateInput {
|
|
157
157
|
email?: string;
|
|
158
158
|
displayName?: string;
|
|
159
159
|
password?: string;
|
|
@@ -166,6 +166,16 @@ interface UserUpdateInput {
|
|
|
166
166
|
profilePage?: string;
|
|
167
167
|
[key: string]: unknown;
|
|
168
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* A change worked out from the account as it is now (#2025), the function form
|
|
171
|
+
* of `updateUser`. It runs inside the account's key, is handed a copy of the
|
|
172
|
+
* account without the password hash (an add-on's own fields included, as
|
|
173
|
+
* `UserUpdateInput` allows them), and returns the fields to merge, or
|
|
174
|
+
* `undefined` to change nothing.
|
|
175
|
+
*/
|
|
176
|
+
export type UserUpdateFn = (current: Omit<User, 'password'> & {
|
|
177
|
+
[key: string]: unknown;
|
|
178
|
+
}) => UserUpdateInput | undefined | Promise<UserUpdateInput | undefined>;
|
|
169
179
|
/**
|
|
170
180
|
* UserManager - Handles user authentication, authorization, and roles
|
|
171
181
|
*
|
|
@@ -208,6 +218,8 @@ declare class UserManager extends BaseManager {
|
|
|
208
218
|
private provider;
|
|
209
219
|
private providerClass?;
|
|
210
220
|
private passwordSalt?;
|
|
221
|
+
/** One read-merge-write at a time per account, keyed by the normalized username (#2025). */
|
|
222
|
+
private readonly accountWrites;
|
|
211
223
|
/**
|
|
212
224
|
* Creates a new UserManager instance
|
|
213
225
|
*
|
|
@@ -328,6 +340,17 @@ declare class UserManager extends BaseManager {
|
|
|
328
340
|
private withAccountRoles;
|
|
329
341
|
/** `ngdpbase.user.account-roles`: the roles every account holds (#1539). */
|
|
330
342
|
private accountRoles;
|
|
343
|
+
/**
|
|
344
|
+
* Every account has exactly one identity record, made by `createUser`
|
|
345
|
+
* (#2037). One that has none (written straight into the user store, or
|
|
346
|
+
* whose record was lost) gets it at boot, once, through PersonManager's
|
|
347
|
+
* door under the system context, each creation recorded as
|
|
348
|
+
* `contact-manage`. The identity records are listed once and only the
|
|
349
|
+
* missing ones are made, so a large user store costs one read. A failure
|
|
350
|
+
* is logged and the boot goes on: the role paths make the record when the
|
|
351
|
+
* account is next used (`getByUserIdentifier`).
|
|
352
|
+
*/
|
|
353
|
+
private giveIdentityRecordsToAccountsWithout;
|
|
331
354
|
/**
|
|
332
355
|
* Accounts made before a role joined `ngdpbase.user.account-roles` get it
|
|
333
356
|
* once, at boot (#1539) — RoleManager decides "once" and records each grant.
|
|
@@ -358,6 +381,15 @@ declare class UserManager extends BaseManager {
|
|
|
358
381
|
upgradePasswordHash(username: string, newHash: string, legacy: boolean): Promise<void>;
|
|
359
382
|
/** Count a completed sign-in on the account: last sign-in and sign-in count (#1758; AuthManager calls it for a sign-in, never a re-check). */
|
|
360
383
|
recordSignIn(username: string): Promise<void>;
|
|
384
|
+
/** Run `task` inside the account's key: one read-merge-write at a time per account (#2025). Not re-entrant. */
|
|
385
|
+
private inAccountKey;
|
|
386
|
+
/**
|
|
387
|
+
* Read an account, change it and write it back, inside its key (#2025): for
|
|
388
|
+
* core's own bookkeeping (sign-in counters, the hash upgrade, the profile
|
|
389
|
+
* page), which is not an edit and is not recorded. False when there is no
|
|
390
|
+
* such account.
|
|
391
|
+
*/
|
|
392
|
+
private patchAccount;
|
|
361
393
|
/**
|
|
362
394
|
* Check if a display name conflicts with existing page names or other users
|
|
363
395
|
* @param {string} displayName - Display name to check
|
|
@@ -400,6 +432,12 @@ declare class UserManager extends BaseManager {
|
|
|
400
432
|
* @returns {Promise<Omit<User, 'password'>>} Created user (without password)
|
|
401
433
|
*/
|
|
402
434
|
createUser(userData: UserCreateInput, ctx: ActorContext): Promise<Omit<User, 'password'>>;
|
|
435
|
+
/**
|
|
436
|
+
* Take back an account whose roles could not be given (#2015): its Person,
|
|
437
|
+
* then the account. A failure here is an error in the log; the role failure
|
|
438
|
+
* that caused it is what the caller is told.
|
|
439
|
+
*/
|
|
440
|
+
private undoAccountCreate;
|
|
403
441
|
/**
|
|
404
442
|
* Update user
|
|
405
443
|
*/
|
|
@@ -419,7 +457,31 @@ declare class UserManager extends BaseManager {
|
|
|
419
457
|
* which accounts exist.
|
|
420
458
|
*/
|
|
421
459
|
resetPasswordWithRecoveryWords(username: string, words: string, newPassword: string, ctx: ActorContext): Promise<boolean>;
|
|
422
|
-
|
|
460
|
+
/**
|
|
461
|
+
* Change an account (#2025: one change at a time per account).
|
|
462
|
+
*
|
|
463
|
+
* `updates` is the fields to merge, or a function that works them out from
|
|
464
|
+
* the account as it is now: `(current) => updates`. The function runs inside
|
|
465
|
+
* the account's `KeyedSerial` key, between the read and the write, so two
|
|
466
|
+
* changes to one account never read the same old record. It gets a copy of
|
|
467
|
+
* the account without the password hash, and returns the fields to merge, or
|
|
468
|
+
* `undefined` to change nothing (nothing is written or recorded). It must not
|
|
469
|
+
* call `updateUser` for the same account: the key is not re-entrant.
|
|
470
|
+
* Either form is checked, applied and recorded the same way.
|
|
471
|
+
*
|
|
472
|
+
* Returns the account as written (as read, when nothing changed), without
|
|
473
|
+
* the password hash: the hash never leaves UserManager except to the
|
|
474
|
+
* password check (`storedPasswordHash`).
|
|
475
|
+
*/
|
|
476
|
+
updateUser(username: string, updates: UserUpdateInput | UserUpdateFn, ctx: ActorContext): Promise<Omit<User, 'password'>>;
|
|
477
|
+
/**
|
|
478
|
+
* The steps of an account change that come after the write, outside the
|
|
479
|
+
* account's key: they reach other managers, which may change the account
|
|
480
|
+
* themselves.
|
|
481
|
+
*/
|
|
482
|
+
private finishEdit;
|
|
483
|
+
/** Read, check and write one account change. Runs inside the account's key (#2025). */
|
|
484
|
+
private editAccount;
|
|
423
485
|
/**
|
|
424
486
|
* Delete user
|
|
425
487
|
*/
|
|
@@ -469,6 +531,8 @@ declare class UserManager extends BaseManager {
|
|
|
469
531
|
* who changed it, so the record says why.
|
|
470
532
|
*/
|
|
471
533
|
private personSyncContext;
|
|
534
|
+
/** Core's bookkeeping for an account write, the read of its roles included (#2018): the system context, saying what and who changed the account. */
|
|
535
|
+
private lifecycleContext;
|
|
472
536
|
/**
|
|
473
537
|
* Persist a Person record paired with a newly-created User. The install's
|
|
474
538
|
* anchor org (when configured) is referenced via `memberOf`; without it
|
|
@@ -27,8 +27,15 @@ import type { PermissionSubject, JobSubject } from '../managers/UserManager.js';
|
|
|
27
27
|
import type { WikiEngine } from '../types/WikiEngine.js';
|
|
28
28
|
import type { Decision, DecisionRequest, Policy } from '../types/Policy.js';
|
|
29
29
|
import { type RoleGrant } from '../utils/roleGrants.js';
|
|
30
|
+
import { type ActorContext } from '../context/ActorContext.js';
|
|
30
31
|
export declare const POLICIES_KEY = "ngdpbase.access.policies";
|
|
31
32
|
export declare const POLICIES_ENABLED_KEY = "ngdpbase.access.policies.enabled";
|
|
33
|
+
/**
|
|
34
|
+
* What `decide`, `permits` and `holds` take (#2022): the caller's own context
|
|
35
|
+
* — a request's `PermissionSubject` or a job's `JobContext` — or a subject
|
|
36
|
+
* already in decision form (`JobSubject`). A caller passes what it was given.
|
|
37
|
+
*/
|
|
38
|
+
type DecisionInput = ActorContext | JobSubject | null | undefined;
|
|
32
39
|
export declare class PolicyDecisionPoint extends BaseManager {
|
|
33
40
|
/**
|
|
34
41
|
* A manager so every PEP can reach it through `engine.getManager`, the same
|
|
@@ -84,10 +91,11 @@ export declare class PolicyDecisionPoint extends BaseManager {
|
|
|
84
91
|
* of {@link decide} for a capability. It was `UserManager.hasPermission`
|
|
85
92
|
* until #1431 step 14; the decision was already here, only the door moved.
|
|
86
93
|
*
|
|
87
|
-
* Takes
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
94
|
+
* Takes the caller's context as it was given (#2022): a `PermissionSubject`
|
|
95
|
+
* — the request's own identity, forwarded from `req.userContext`
|
|
96
|
+
* (WikiContext, ApiContext, ParseContext), roles already resolved by the
|
|
97
|
+
* session middleware — or a `JobContext` for work with no request (#631),
|
|
98
|
+
* which the PDP turns into a subject itself. Never convert one first.
|
|
91
99
|
*
|
|
92
100
|
* #1173 Part B: the username-string overload this method once accepted is
|
|
93
101
|
* gone. A string cannot carry `viaToken`, so the agent-token ceiling below
|
|
@@ -98,12 +106,13 @@ export declare class PolicyDecisionPoint extends BaseManager {
|
|
|
98
106
|
* separate question {@link userHoldsPermission} — "does the named user hold
|
|
99
107
|
* this?" — which is a lookup about somebody else, not an authorisation.
|
|
100
108
|
*
|
|
101
|
-
* @param subject - The
|
|
102
|
-
* `viaToken` present when a bearer token authenticated it
|
|
109
|
+
* @param subject - The caller's context: the request's subject, with
|
|
110
|
+
* `viaToken` present when a bearer token authenticated it,
|
|
111
|
+
* or a job's context.
|
|
103
112
|
* @param action - Action/permission to check (e.g., 'page-create', 'user-read')
|
|
104
113
|
* @returns True if the subject may perform the action under current policy
|
|
105
114
|
*/
|
|
106
|
-
permits(subject:
|
|
115
|
+
permits(subject: DecisionInput, action: string): Promise<boolean>;
|
|
107
116
|
/**
|
|
108
117
|
* Does the subject HOLD this permission (#1635)? Policy alone — the
|
|
109
118
|
* step-up freshness rule is not applied. A different question from
|
|
@@ -112,7 +121,7 @@ export declare class PolicyDecisionPoint extends BaseManager {
|
|
|
112
121
|
* person whose sign-in is not fresh still sees the way to the prompt that
|
|
113
122
|
* makes it fresh. A door never asks this.
|
|
114
123
|
*/
|
|
115
|
-
holds(subject:
|
|
124
|
+
holds(subject: DecisionInput, action: string): Promise<boolean>;
|
|
116
125
|
/**
|
|
117
126
|
* Step-up (#1525, #1635), decided here — in the one place every door asks —
|
|
118
127
|
* rather than by routes in front of the doors. A permission marked
|
|
@@ -252,7 +261,7 @@ export declare class PolicyDecisionPoint extends BaseManager {
|
|
|
252
261
|
* time, not at enqueue time (#631, #1212).
|
|
253
262
|
* 4. __The policies__, through `PolicyEvaluator`.
|
|
254
263
|
*/
|
|
255
|
-
decide(subject:
|
|
264
|
+
decide(subject: DecisionInput, request: DecisionRequest): Promise<Decision>;
|
|
256
265
|
/** `decide`, with the step-up rule applied or not — {@link holds} is the only caller that leaves it out. */
|
|
257
266
|
private evaluate;
|
|
258
267
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import BaseManager from '../managers/BaseManager.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type ActorContext } from '../context/ActorContext.js';
|
|
3
3
|
import { WikiEngine } from '../types/WikiEngine.js';
|
|
4
4
|
import type { AgentTokenGrant, ProxyClaim } from '../managers/UserManager.js';
|
|
5
5
|
import type { Request } from 'express';
|
|
@@ -46,6 +46,12 @@ interface UserContext {
|
|
|
46
46
|
viaProxy?: ProxyClaim;
|
|
47
47
|
[key: string]: unknown;
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* What the page questions take (#2022): the caller's context as given — a
|
|
51
|
+
* request's subject or a job's `JobContext` — or a subject already in decision
|
|
52
|
+
* form. They convert it themselves, through {@link asSubject}.
|
|
53
|
+
*/
|
|
54
|
+
type PageQuestionCaller = ActorContext | UserContext | null | undefined;
|
|
49
55
|
/**
|
|
50
56
|
* Access decision log entry
|
|
51
57
|
*/
|
|
@@ -173,6 +179,15 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
173
179
|
* visitor holds, which is the safe answer for someone who no longer exists.
|
|
174
180
|
*/
|
|
175
181
|
resolveSubjectNow(username: string | undefined): Promise<UserContext>;
|
|
182
|
+
/**
|
|
183
|
+
* The subject as a page's own rules match it (#2022): for a job's subject
|
|
184
|
+
* (`resolveRolesNow`, #1212), its username with the roles its account
|
|
185
|
+
* holds NOW, read through {@link resolveSubjectNow} — the path the PDP
|
|
186
|
+
* takes for the same subject; anyone else as carried. A job carries no
|
|
187
|
+
* roles, so matching it without this matched none, and an audience naming
|
|
188
|
+
* a role its user holds left the job out.
|
|
189
|
+
*/
|
|
190
|
+
private matchableNow;
|
|
176
191
|
/**
|
|
177
192
|
* Who is making this request: the subject the session middleware resolved,
|
|
178
193
|
* else the session's account resolved now, else the anonymous visitor.
|
|
@@ -348,14 +363,14 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
348
363
|
* private page gets at Tier 0: the owner, or a delegate of the owner; never
|
|
349
364
|
* a role. A refusal is recorded like any page refusal (`authorization-deny`).
|
|
350
365
|
*
|
|
351
|
-
* @param
|
|
366
|
+
* @param caller - the caller's context, as given: a request's subject or a job's (#2022); null is anonymous
|
|
352
367
|
* @param owner - the container's owner (a private file's `creator`)
|
|
353
368
|
* @param resource - what is being reached, for the record (e.g. `attachment:<id>`)
|
|
354
369
|
* @param action - the action asked for (e.g. `view`)
|
|
355
370
|
* @param vault - the vault inside the container, when the question is about one:
|
|
356
371
|
* a share link the owner issued gets in only for a vault it names (#1388)
|
|
357
372
|
*/
|
|
358
|
-
canAccessPrivateContainer(
|
|
373
|
+
canAccessPrivateContainer(caller: PageQuestionCaller, owner: string, resource: string, action: string, vault?: string): boolean;
|
|
359
374
|
/**
|
|
360
375
|
* Check whether a user can access a given page — for cross-page checks where
|
|
361
376
|
* the current WikiContext describes a DIFFERENT page than the one being
|
|
@@ -381,14 +396,14 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
381
396
|
* case.
|
|
382
397
|
* - any tier returns deny.
|
|
383
398
|
*
|
|
384
|
-
* @param
|
|
399
|
+
* @param caller - The caller's context, as given: a request's subject or a job's (#2022); null / undefined is anonymous.
|
|
385
400
|
* @param pageName - Target page to check.
|
|
386
401
|
* @param action - Action verb (e.g., `'view'`, `'edit'`, `'delete'`).
|
|
387
402
|
* @param knownMetadata - The page's metadata when the caller has just
|
|
388
403
|
* resolved it: the page door (#1622), which reads the page by the same
|
|
389
404
|
* resolution it decides on. Omitted, it is loaded here.
|
|
390
405
|
*/
|
|
391
|
-
canUserAccessPage(
|
|
406
|
+
canUserAccessPage(caller: PageQuestionCaller, pageName: string, action: string, knownMetadata?: PageFrontmatter): Promise<boolean>;
|
|
392
407
|
/**
|
|
393
408
|
* {@link canUserAccessPage} as an affordance (#1668): would the door allow
|
|
394
409
|
* this action on that page — asked only to decide whether to show a control
|
|
@@ -399,7 +414,7 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
399
414
|
* @param knownMetadata - As for {@link canUserAccessPage}: the page's
|
|
400
415
|
* metadata when the caller has just resolved it (`PageManager.mayShowPage`).
|
|
401
416
|
*/
|
|
402
|
-
pageAffordanceFor(
|
|
417
|
+
pageAffordanceFor(caller: PageQuestionCaller, pageName: string, action: string, knownMetadata?: PageFrontmatter): Promise<boolean>;
|
|
403
418
|
/** The cross-page check, for the door (`record`) or an affordance (#1668). */
|
|
404
419
|
private crossPageDecision;
|
|
405
420
|
/**
|
|
@@ -425,7 +440,7 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
425
440
|
* A candidate without metadata is not listed (#714's convention). Order is
|
|
426
441
|
* preserved. Returns titles.
|
|
427
442
|
*/
|
|
428
|
-
filterAccessiblePages(
|
|
443
|
+
filterAccessiblePages(caller: PageQuestionCaller, action: string, candidates: ReadonlyArray<{
|
|
429
444
|
title: string;
|
|
430
445
|
metadata: PageFrontmatter | null | undefined;
|
|
431
446
|
}>): Promise<string[]>;
|
|
@@ -445,7 +460,7 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
445
460
|
* A refusal is audited as `authorization-deny` on the media resource, with
|
|
446
461
|
* the share attribution when there is one.
|
|
447
462
|
*/
|
|
448
|
-
canUserAccessMediaItem(
|
|
463
|
+
canUserAccessMediaItem(caller: PageQuestionCaller, item: MediaItem): Promise<boolean>;
|
|
449
464
|
/**
|
|
450
465
|
* Check front matter audience / access fields (Tier 1).
|
|
451
466
|
* Returns a decision object; decided=false means no front matter restriction — fall through.
|
|
@@ -462,10 +477,10 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
462
477
|
* write door, so a write cannot reach a page its writer is outside the
|
|
463
478
|
* audience of — whatever route it came by. Records nothing; the door does.
|
|
464
479
|
*
|
|
465
|
-
* @param
|
|
480
|
+
* @param caller - the caller's context, as given (a job's included, #2022)
|
|
466
481
|
* @param metadata - the page's current frontmatter
|
|
467
482
|
*/
|
|
468
|
-
audienceAdmits(
|
|
483
|
+
audienceAdmits(caller: PageQuestionCaller, metadata: PageFrontmatter | null | undefined): Promise<boolean>;
|
|
469
484
|
/**
|
|
470
485
|
* Record/audit an access decision.
|
|
471
486
|
* Accepts either a single object or positional args for backward compatibility.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consent to a site's documents (#1603): the shapes ConsentManager and its
|
|
3
|
+
* callers share, and what an add-on registers.
|
|
4
|
+
*/
|
|
5
|
+
/** Where a person is asked to accept (#1603): `?scope=` (core when absent) and `?returnTo=`. */
|
|
6
|
+
export const CONSENT_PATH = '/consent';
|
|
7
|
+
/** The scope of core's own documents, `ngdpbase.consent.documents`. */
|
|
8
|
+
export const CORE_CONSENT_SCOPE = 'core';
|
|
9
|
+
/**
|
|
10
|
+
* Thrown when a per-person add-on is turned on before its documents are
|
|
11
|
+
* accepted (#1603). `documents` names what is still to accept.
|
|
12
|
+
*/
|
|
13
|
+
export class ConsentRequiredError extends Error {
|
|
14
|
+
scope;
|
|
15
|
+
documents;
|
|
16
|
+
name = 'ConsentRequiredError';
|
|
17
|
+
constructor(scope, documents) {
|
|
18
|
+
super(`Turning ${scope} on needs its ${documents.join(' and ')} accepted first`);
|
|
19
|
+
this.scope = scope;
|
|
20
|
+
this.documents = documents;
|
|
21
|
+
}
|
|
22
|
+
}
|
package/lib/src/types/User.d.ts
CHANGED
|
@@ -73,7 +73,7 @@ export interface User {
|
|
|
73
73
|
/**
|
|
74
74
|
* @deprecated since #617 iteration 3b. Role membership is now stored
|
|
75
75
|
* canonically as OrganizationRole records owned by `RoleManager`; resolve
|
|
76
|
-
* a user's roles via `RoleManager.resolveUserRoles(username)`. The field
|
|
76
|
+
* a user's roles via `RoleManager.resolveUserRoles(ctx, username)`. The field
|
|
77
77
|
* is retained as optional for backwards compatibility while in-flight
|
|
78
78
|
* `users.json` files still carry it; the `strip-user-roles.ts` migration
|
|
79
79
|
* removes it. New code MUST NOT read or write this field directly.
|
|
@@ -50,9 +50,9 @@ export declare const AUDIT_EVENT: {
|
|
|
50
50
|
readonly CONFIG_CHANGE: "config-change";
|
|
51
51
|
/** Every custom configuration value discarded; recorded before the reset, which cannot proceed without it */
|
|
52
52
|
readonly CONFIG_RESET: "config-reset";
|
|
53
|
-
/** A
|
|
53
|
+
/** A user accepted a document they are asked to accept (#1603): which document (core's terms-of-service or privacy-policy, or an add-on's), its version and effective date, and how (sign-up, sign-in, turning an add-on on). Recorded before the acceptance is stored, which cannot proceed without it: consent must be provable */
|
|
54
54
|
readonly CONSENT_GRANT: "consent-grant";
|
|
55
|
-
/** A
|
|
55
|
+
/** A user's acceptances ended (#1603): today only when their account is deleted, which removes their consent records; names the documents and versions they had accepted. Recorded before the records are removed */
|
|
56
56
|
readonly CONSENT_WITHDRAW: "consent-withdraw";
|
|
57
57
|
/** Contact created, changed, deleted or linked to an account; metadata names the kind (person or organization), the operation and the fields changed, never their values */
|
|
58
58
|
readonly CONTACT_MANAGE: "contact-manage";
|
|
@@ -112,21 +112,21 @@ export declare const AUDIT_EVENT: {
|
|
|
112
112
|
readonly POLICY_EVALUATE: "policy-evaluate";
|
|
113
113
|
/** Security posture at startup, compared against the previous start */
|
|
114
114
|
readonly POSTURE_RECORDED: "posture-recorded";
|
|
115
|
-
/** Proxy grant made: someone may act for the
|
|
115
|
+
/** Proxy grant made: someone may act for the user named in user; actor is who made it (#1762) */
|
|
116
116
|
readonly PROXY_GRANT: "proxy-grant";
|
|
117
|
-
/** Proxy grant ended: revoked, or the account closed; user is the
|
|
117
|
+
/** Proxy grant ended: revoked, or the account closed; user is the user represented, actor who ended it (#1762) */
|
|
118
118
|
readonly PROXY_REVOKE: "proxy-revoke";
|
|
119
|
-
/** A signed-in session started or stopped acting for someone under a proxy grant, or opens their private store: chosen, switched-back, ended because the grant no longer allows it (grant-ended, proxy-access-off), or vault-opened; user is the
|
|
119
|
+
/** A signed-in session started or stopped acting for someone under a proxy grant, or opens their private store: chosen, switched-back, ended because the grant no longer allows it (grant-ended, proxy-access-off), or vault-opened; user is the user represented, actor the user signed in (#1762) */
|
|
120
120
|
readonly PROXY_SWITCH: "proxy-switch";
|
|
121
|
-
/** Step-up: a re-authentication failed (wrong password, another
|
|
121
|
+
/** Step-up: a re-authentication failed (wrong password, another user's passkey, throttled) */
|
|
122
122
|
readonly REAUTH_FAILURE: "reauth-failure";
|
|
123
123
|
/** Step-up: an action asked for a fresh sign-in (#1525) — or refused a delegated credential that cannot give one */
|
|
124
124
|
readonly REAUTH_PROMPT: "reauth-prompt";
|
|
125
|
-
/** Step-up: the
|
|
125
|
+
/** Step-up: the user re-authenticated, with the factor used */
|
|
126
126
|
readonly REAUTH_SUCCESS: "reauth-success";
|
|
127
127
|
/** Page search; not recorded, read volume */
|
|
128
128
|
readonly SEARCH_PAGE: "search-page";
|
|
129
|
-
/**
|
|
129
|
+
/** Users searched for; enumerating users is disclosive in a way searching pages is not. Off by default as read volume */
|
|
130
130
|
readonly SEARCH_USER: "search-user";
|
|
131
131
|
/** A masked configuration value shown to an administrator; the key is recorded, never the value */
|
|
132
132
|
readonly SECRET_REVEAL: "secret-reveal";
|
|
@@ -146,9 +146,9 @@ export declare const AUDIT_EVENT: {
|
|
|
146
146
|
readonly SHARE_REVOKE: "share-revoke";
|
|
147
147
|
/** Sign-in approval: the browser that was waiting gave up the sign-in (#1532) */
|
|
148
148
|
readonly SIGNINAPPROVAL_ABANDON: "signinapproval-abandon";
|
|
149
|
-
/** Sign-in approval: the
|
|
149
|
+
/** Sign-in approval: the user pressed Approve (#1532) */
|
|
150
150
|
readonly SIGNINAPPROVAL_APPROVE: "signinapproval-approve";
|
|
151
|
-
/** Sign-in approval: the
|
|
151
|
+
/** Sign-in approval: the user pressed Deny; that sign-in will not complete (#1532) */
|
|
152
152
|
readonly SIGNINAPPROVAL_DENY: "signinapproval-deny";
|
|
153
153
|
/** Sign-in approval: a waiting sign-in expired before it was completed (#1532) */
|
|
154
154
|
readonly SIGNINAPPROVAL_EXPIRE: "signinapproval-expire";
|
|
@@ -57,9 +57,9 @@ export const AUDIT_EVENT = {
|
|
|
57
57
|
CONFIG_CHANGE: 'config-change',
|
|
58
58
|
/** Every custom configuration value discarded; recorded before the reset, which cannot proceed without it */
|
|
59
59
|
CONFIG_RESET: 'config-reset',
|
|
60
|
-
/** A
|
|
60
|
+
/** A user accepted a document they are asked to accept (#1603): which document (core's terms-of-service or privacy-policy, or an add-on's), its version and effective date, and how (sign-up, sign-in, turning an add-on on). Recorded before the acceptance is stored, which cannot proceed without it: consent must be provable */
|
|
61
61
|
CONSENT_GRANT: 'consent-grant',
|
|
62
|
-
/** A
|
|
62
|
+
/** A user's acceptances ended (#1603): today only when their account is deleted, which removes their consent records; names the documents and versions they had accepted. Recorded before the records are removed */
|
|
63
63
|
CONSENT_WITHDRAW: 'consent-withdraw',
|
|
64
64
|
/** Contact created, changed, deleted or linked to an account; metadata names the kind (person or organization), the operation and the fields changed, never their values */
|
|
65
65
|
CONTACT_MANAGE: 'contact-manage',
|
|
@@ -119,21 +119,21 @@ export const AUDIT_EVENT = {
|
|
|
119
119
|
POLICY_EVALUATE: 'policy-evaluate',
|
|
120
120
|
/** Security posture at startup, compared against the previous start */
|
|
121
121
|
POSTURE_RECORDED: 'posture-recorded',
|
|
122
|
-
/** Proxy grant made: someone may act for the
|
|
122
|
+
/** Proxy grant made: someone may act for the user named in user; actor is who made it (#1762) */
|
|
123
123
|
PROXY_GRANT: 'proxy-grant',
|
|
124
|
-
/** Proxy grant ended: revoked, or the account closed; user is the
|
|
124
|
+
/** Proxy grant ended: revoked, or the account closed; user is the user represented, actor who ended it (#1762) */
|
|
125
125
|
PROXY_REVOKE: 'proxy-revoke',
|
|
126
|
-
/** A signed-in session started or stopped acting for someone under a proxy grant, or opens their private store: chosen, switched-back, ended because the grant no longer allows it (grant-ended, proxy-access-off), or vault-opened; user is the
|
|
126
|
+
/** A signed-in session started or stopped acting for someone under a proxy grant, or opens their private store: chosen, switched-back, ended because the grant no longer allows it (grant-ended, proxy-access-off), or vault-opened; user is the user represented, actor the user signed in (#1762) */
|
|
127
127
|
PROXY_SWITCH: 'proxy-switch',
|
|
128
|
-
/** Step-up: a re-authentication failed (wrong password, another
|
|
128
|
+
/** Step-up: a re-authentication failed (wrong password, another user's passkey, throttled) */
|
|
129
129
|
REAUTH_FAILURE: 'reauth-failure',
|
|
130
130
|
/** Step-up: an action asked for a fresh sign-in (#1525) — or refused a delegated credential that cannot give one */
|
|
131
131
|
REAUTH_PROMPT: 'reauth-prompt',
|
|
132
|
-
/** Step-up: the
|
|
132
|
+
/** Step-up: the user re-authenticated, with the factor used */
|
|
133
133
|
REAUTH_SUCCESS: 'reauth-success',
|
|
134
134
|
/** Page search; not recorded, read volume */
|
|
135
135
|
SEARCH_PAGE: 'search-page',
|
|
136
|
-
/**
|
|
136
|
+
/** Users searched for; enumerating users is disclosive in a way searching pages is not. Off by default as read volume */
|
|
137
137
|
SEARCH_USER: 'search-user',
|
|
138
138
|
/** A masked configuration value shown to an administrator; the key is recorded, never the value */
|
|
139
139
|
SECRET_REVEAL: 'secret-reveal',
|
|
@@ -153,9 +153,9 @@ export const AUDIT_EVENT = {
|
|
|
153
153
|
SHARE_REVOKE: 'share-revoke',
|
|
154
154
|
/** Sign-in approval: the browser that was waiting gave up the sign-in (#1532) */
|
|
155
155
|
SIGNINAPPROVAL_ABANDON: 'signinapproval-abandon',
|
|
156
|
-
/** Sign-in approval: the
|
|
156
|
+
/** Sign-in approval: the user pressed Approve (#1532) */
|
|
157
157
|
SIGNINAPPROVAL_APPROVE: 'signinapproval-approve',
|
|
158
|
-
/** Sign-in approval: the
|
|
158
|
+
/** Sign-in approval: the user pressed Deny; that sign-in will not complete (#1532) */
|
|
159
159
|
SIGNINAPPROVAL_DENY: 'signinapproval-deny',
|
|
160
160
|
/** Sign-in approval: a waiting sign-in expired before it was completed (#1532) */
|
|
161
161
|
SIGNINAPPROVAL_EXPIRE: 'signinapproval-expire',
|
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
* A route turns it into the answer with `ApiContext.enforceDeny(err.permission,
|
|
13
13
|
* err.decision)`, the one place a refusal is shaped (#1431); the door never
|
|
14
14
|
* builds an HTTP answer itself.
|
|
15
|
+
*
|
|
16
|
+
* Every lookup takes a context too (#2018, operator 2026-10-23), through
|
|
17
|
+
* {@link requireRecordAccess}: a user's own identity record under
|
|
18
|
+
* `profile-manage`, anyone else's under `contact-read` / `contact-manage`, and
|
|
19
|
+
* core's own reads under the system context without a policy evaluation, so
|
|
20
|
+
* resolving roles on every request stays cheap.
|
|
15
21
|
*/
|
|
16
22
|
import { type ActorContext } from '../context/ActorContext.js';
|
|
17
23
|
import type { CorePermission } from '../security/permissions.generated.js';
|
|
@@ -37,6 +43,30 @@ export declare class ContactRefusedError extends Error {
|
|
|
37
43
|
* refuses: closed, never open.
|
|
38
44
|
*/
|
|
39
45
|
export declare function requireContactPermission(engine: EngineLike, ctx: ActorContext, kind: keyof typeof CONTACT_PERMISSION, also?: readonly CorePermission[]): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Core's own work (#2018): a boot or schedule context naming the system
|
|
48
|
+
* principal, with no token, share or proxy claim. Recognised by its fields
|
|
49
|
+
* alone, with no policy evaluation, because RoleManager resolves roles
|
|
50
|
+
* through these lookups on every request. A request never carries this
|
|
51
|
+
* context: the system principal's name is reserved, so no account holds it.
|
|
52
|
+
*/
|
|
53
|
+
export declare function isSystemWork(engine: EngineLike, ctx: ActorContext | null | undefined): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* A lookup or change of one record at a contacts door (#2018). Throws
|
|
56
|
+
* {@link ContactRefusedError} unless `ctx` may:
|
|
57
|
+
*
|
|
58
|
+
* - read it as core's own work ({@link isSystemWork}): allowed, not evaluated;
|
|
59
|
+
* - read or change its own identity record (the context's username is the
|
|
60
|
+
* record's `identifier`): `profile-manage`, which every user holds;
|
|
61
|
+
* - read or change any other record: `contact-read` / `contact-manage`.
|
|
62
|
+
*
|
|
63
|
+
* `record` is the identity record (or what names it, `{ identifier }`); left
|
|
64
|
+
* out for a record that is never anyone's own (an organization, a role).
|
|
65
|
+
* "Own" is decided by comparing usernames, never by a role.
|
|
66
|
+
*/
|
|
67
|
+
export declare function requireRecordAccess(engine: EngineLike, ctx: ActorContext, kind: keyof typeof CONTACT_PERMISSION, record?: {
|
|
68
|
+
identifier?: unknown;
|
|
69
|
+
} | null): Promise<void>;
|
|
40
70
|
/** What a contacts record says was done. Names fields, never their values. */
|
|
41
71
|
export interface ContactChange {
|
|
42
72
|
kind: 'person' | 'organization';
|