@jwilleke/ngdpbase-types 5.41.1 → 5.43.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/src/context/ApiContext.js +1 -1
- package/lib/src/context/BaseContext.d.ts +20 -20
- package/lib/src/context/BaseContext.js +41 -24
- package/lib/src/context/JobContext.d.ts +31 -0
- package/lib/src/context/JobContext.js +34 -1
- package/lib/src/managers/AddonsManager.d.ts +8 -28
- package/lib/src/managers/AgentTokenManager.d.ts +8 -0
- package/lib/src/managers/AttachmentManager.d.ts +59 -0
- package/lib/src/managers/AuditManager.d.ts +477 -0
- package/lib/src/managers/AuthManager.d.ts +247 -15
- package/lib/src/managers/AuthManager.js +588 -45
- package/lib/src/managers/ConfigurationManager.d.ts +15 -0
- package/lib/src/managers/ConsentManager.d.ts +15 -0
- package/lib/src/managers/ImportManager.d.ts +15 -0
- package/lib/src/managers/PageManager.d.ts +185 -23
- package/lib/src/managers/PolicyEvaluator.d.ts +99 -24
- package/lib/src/managers/ShareManager.d.ts +3 -2
- package/lib/src/managers/UserManager.d.ts +19 -5
- package/lib/src/managers/ValidationManager.d.ts +3 -1
- package/lib/src/providers/AgentTokenAuthProvider.js +1 -1
- package/lib/src/providers/BaseAttachmentProvider.d.ts +4 -0
- package/lib/src/providers/BaseAuditProvider.d.ts +492 -0
- package/lib/src/providers/BaseAuthProvider.d.ts +4 -0
- package/lib/src/providers/BaseCredentialsProvider.d.ts +2 -2
- package/lib/src/providers/BaseCredentialsProvider.js +1 -1
- package/lib/src/providers/BasicAttachmentProvider.d.ts +9 -0
- package/lib/src/providers/FileCredentialsProvider.js +1 -1
- package/lib/src/providers/NullAuditProvider.d.ts +82 -0
- package/lib/src/providers/TotpAuthProvider.d.ts +55 -0
- package/lib/src/providers/TotpAuthProvider.js +145 -0
- package/lib/src/security/PolicyDecisionPoint.d.ts +57 -1
- package/lib/src/security/PolicyInformationPoint.d.ts +86 -6
- package/lib/src/security/permissions.generated.d.ts +8 -0
- package/lib/src/types/Consent.d.ts +17 -4
- package/lib/src/types/Consent.js +11 -1
- package/lib/src/types/Page.d.ts +0 -7
- package/lib/src/types/Policy.d.ts +67 -6
- package/lib/src/types/Policy.js +6 -0
- package/lib/src/types/Provider.d.ts +18 -0
- package/lib/src/types/User.d.ts +6 -0
- package/lib/src/types/express.d.ts +22 -0
- package/lib/src/utils/addonPageSync.d.ts +0 -12
- package/lib/src/utils/auditChain.d.ts +83 -0
- package/lib/src/utils/auditEventNames.generated.d.ts +2 -0
- package/lib/src/utils/auditEventNames.generated.js +2 -0
- package/lib/src/utils/auditEvents.d.ts +2 -0
- package/lib/src/utils/auditEvents.js +3 -1
- package/lib/src/utils/auditHeadWitness.d.ts +97 -0
- package/lib/src/utils/auditLifecycle.d.ts +71 -0
- package/lib/src/utils/browserDescription.d.ts +18 -0
- package/lib/src/utils/browserDescription.js +12 -0
- package/lib/src/utils/frontmatterAccess.d.ts +15 -24
- package/lib/src/utils/legacyStoreKeys.d.ts +16 -0
- package/lib/src/utils/pageWritePolicies.d.ts +55 -0
- package/lib/src/utils/permissionCatalog.d.ts +2 -0
- package/lib/src/utils/permissionCatalog.js +19 -0
- package/lib/src/utils/policyAttributes.d.ts +141 -0
- package/lib/src/utils/policyAttributes.js +219 -0
- package/lib/src/utils/postureRecord.d.ts +73 -0
- package/lib/src/utils/privateStoreAccess.d.ts +24 -4
- package/lib/src/utils/privateStoreAccess.js +19 -4
- package/lib/src/utils/privateStorePath.d.ts +6 -0
- package/lib/src/utils/privateStorePath.js +8 -0
- package/lib/src/utils/securityPosture.d.ts +52 -0
- package/lib/src/utils/tlsConfig.d.ts +59 -0
- package/lib/src/utils/totpSeedCipher.d.ts +4 -0
- package/lib/src/utils/totpSeedCipher.js +45 -0
- package/lib/src/utils/username.d.ts +13 -2
- package/lib/src/utils/username.js +18 -3
- package/package.json +5 -2
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import BaseAuditProvider, { WikiEngine, AuditFilters, AuditSearchResults, AuditStats } from './BaseAuditProvider.js';
|
|
2
|
+
import type { AuditReport } from './BaseAuditProvider.js';
|
|
3
|
+
/**
|
|
4
|
+
* NullAuditProvider - No-op audit provider
|
|
5
|
+
*
|
|
6
|
+
* Used when auditing is disabled or for testing.
|
|
7
|
+
* All audit operations are no-ops that return immediately.
|
|
8
|
+
*/
|
|
9
|
+
declare class NullAuditProvider extends BaseAuditProvider {
|
|
10
|
+
/**
|
|
11
|
+
* Not chained (#1119): this provider stores nothing, so there is nothing to
|
|
12
|
+
* chain. A legitimate configuration rather than a hole — provided the
|
|
13
|
+
* instance cannot then claim tamper evidence, which is what
|
|
14
|
+
* getProviderInfo().guarantees reports.
|
|
15
|
+
*/
|
|
16
|
+
protected chainEnabled(): boolean;
|
|
17
|
+
/** Nothing is stored, so nothing is guaranteed (#1119). */
|
|
18
|
+
getGuarantees(): AuditReport;
|
|
19
|
+
constructor(engine: WikiEngine);
|
|
20
|
+
/**
|
|
21
|
+
* Initialize the null audit provider (no-op)
|
|
22
|
+
* @returns {Promise<void>}
|
|
23
|
+
*/
|
|
24
|
+
initialize(): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Get provider information
|
|
27
|
+
* @returns {Object} Provider metadata
|
|
28
|
+
*/
|
|
29
|
+
getProviderInfo(): {
|
|
30
|
+
name: string;
|
|
31
|
+
version: string;
|
|
32
|
+
description: string;
|
|
33
|
+
features: string[];
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Log an audit event (no-op)
|
|
37
|
+
* @param {AuditEvent} _auditEvent - Audit event data
|
|
38
|
+
* @returns {Promise<string>} Dummy event ID
|
|
39
|
+
*/
|
|
40
|
+
writeEvent(_record: Record<string, unknown>): Promise<string>;
|
|
41
|
+
/**
|
|
42
|
+
* Search audit logs (returns empty results)
|
|
43
|
+
* @param {AuditFilters} _filters - Search filters
|
|
44
|
+
* @param {Record<string, number>} options - Search options
|
|
45
|
+
* @returns {Promise<AuditSearchResults>} Empty search results
|
|
46
|
+
*/
|
|
47
|
+
searchAuditLogs(_filters?: AuditFilters, options?: Record<string, number>): Promise<AuditSearchResults>;
|
|
48
|
+
/**
|
|
49
|
+
* Get audit statistics (returns zeros)
|
|
50
|
+
* @param {AuditFilters} _filters - Optional filters
|
|
51
|
+
* @returns {Promise<AuditStats>} Empty statistics
|
|
52
|
+
*/
|
|
53
|
+
getAuditStats(_filters?: AuditFilters): Promise<AuditStats>;
|
|
54
|
+
/**
|
|
55
|
+
* Export audit logs (returns empty data)
|
|
56
|
+
* @param {AuditFilters} _filters - Export filters
|
|
57
|
+
* @param {string} format - Export format ('json', 'csv')
|
|
58
|
+
* @returns {Promise<string>} Empty export data
|
|
59
|
+
*/
|
|
60
|
+
exportAuditLogs(_filters?: AuditFilters, format?: 'json' | 'csv'): Promise<string>;
|
|
61
|
+
/**
|
|
62
|
+
* Flush pending audit events (no-op)
|
|
63
|
+
* @returns {Promise<void>}
|
|
64
|
+
*/
|
|
65
|
+
flush(): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Clean up old audit logs (no-op)
|
|
68
|
+
* @returns {Promise<void>}
|
|
69
|
+
*/
|
|
70
|
+
cleanup(): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Check if the audit provider is healthy (always true)
|
|
73
|
+
* @returns {Promise<boolean>} Always true
|
|
74
|
+
*/
|
|
75
|
+
isHealthy(): Promise<boolean>;
|
|
76
|
+
/**
|
|
77
|
+
* Close/cleanup the audit provider (no-op)
|
|
78
|
+
* @returns {Promise<void>}
|
|
79
|
+
*/
|
|
80
|
+
close(): Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
export default NullAuditProvider;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { AuthProvider, AuthResult, AuthVerifyCredentials, FactorDescription } from './BaseAuthProvider.js';
|
|
2
|
+
import type { CredentialRecord } from './BaseCredentialsProvider.js';
|
|
3
|
+
/** Steps either side of now that are accepted. */
|
|
4
|
+
export declare const TOTP_DRIFT_STEPS = 1;
|
|
5
|
+
/** The store access AuthManager grants: an account's TOTP rows, and recording a use. */
|
|
6
|
+
export interface TotpStoreAccess {
|
|
7
|
+
rows(username: string): CredentialRecord[];
|
|
8
|
+
used(id: string, at: string, secret: string): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
/** A new seed, offered while enrolling and kept by the caller until it is confirmed. */
|
|
11
|
+
export interface TotpEnrolment {
|
|
12
|
+
/** Base32, for typing into the app by hand. */
|
|
13
|
+
seed: string;
|
|
14
|
+
/** The `otpauth://` URI the QR code carries. */
|
|
15
|
+
uri: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The time step `code` belongs to within the drift window, or null. Pure; the
|
|
19
|
+
* caller decides about replay.
|
|
20
|
+
*/
|
|
21
|
+
export declare function matchingStep(seed: string, code: string, now: number): number | null;
|
|
22
|
+
export declare class TotpAuthProvider implements AuthProvider {
|
|
23
|
+
private readonly credentialsKey;
|
|
24
|
+
private readonly issuer;
|
|
25
|
+
private readonly store;
|
|
26
|
+
private readonly now;
|
|
27
|
+
readonly id = "totp";
|
|
28
|
+
readonly displayName = "Authenticator app";
|
|
29
|
+
readonly factor: FactorDescription;
|
|
30
|
+
private readonly serial;
|
|
31
|
+
/**
|
|
32
|
+
* @param credentialsKey `NGDPBASE_CREDENTIALS_KEY`, from which the seed key is derived
|
|
33
|
+
* @param issuer shown in the authenticator app beside the account
|
|
34
|
+
*/
|
|
35
|
+
constructor(credentialsKey: string, issuer: string, store: TotpStoreAccess, now?: () => number);
|
|
36
|
+
/** A fresh 160-bit seed and its URI for `username`. Nothing is stored. */
|
|
37
|
+
newEnrolment(username: string): TotpEnrolment;
|
|
38
|
+
/**
|
|
39
|
+
* Seal a seed that is offered but not yet confirmed, so it can wait in the
|
|
40
|
+
* session without being readable there or in a session log (#421). Its own
|
|
41
|
+
* shape, so it never opens as a stored secret.
|
|
42
|
+
*/
|
|
43
|
+
sealPending(username: string, seed: string): string;
|
|
44
|
+
/** Open what `sealPending` made for `username`; null when it is not that. */
|
|
45
|
+
openPending(username: string, sealed: unknown): string | null;
|
|
46
|
+
/**
|
|
47
|
+
* Check the code that confirms an enrolment and seal the seed for the store.
|
|
48
|
+
* The confirming code's step is recorded, so it cannot then sign anyone in.
|
|
49
|
+
* Null when the seed is malformed or the code does not match.
|
|
50
|
+
*/
|
|
51
|
+
confirmEnrolment(username: string, seed: string, code: string): string | null;
|
|
52
|
+
/** Verify `credentials.code` for `credentials.username`. A failure gives no reason. */
|
|
53
|
+
verify(credentials: AuthVerifyCredentials): Promise<AuthResult | null>;
|
|
54
|
+
private open;
|
|
55
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOTP — an authenticator app's six-digit code (#421), on `otpauth` (decided
|
|
3
|
+
* on #1523, 2026-10-31).
|
|
4
|
+
*
|
|
5
|
+
* - A second factor only (`primary: false`): something you have, `amr`
|
|
6
|
+
* `otp`, AAL1 on its own. After a password the sign-in covers two factor
|
|
7
|
+
* types and AuthManager's `assess()` lifts it to AAL2. Not phishing-resistant.
|
|
8
|
+
* - The seed lives in the credentials store (#1524), kind `totp`, AES-GCM
|
|
9
|
+
* encrypted under a subkey of `NGDPBASE_CREDENTIALS_KEY`
|
|
10
|
+
* (`utils/totpSeedCipher`). The row's sealed secret also holds the last time
|
|
11
|
+
* step used, so a code cannot be used twice in its window.
|
|
12
|
+
* - A code is accepted for the current 30-second step and one step either
|
|
13
|
+
* side (RFC 6238 §5.2 drift), never for a step at or before the last one used.
|
|
14
|
+
* - Verification for one account runs one at a time, so two requests carrying
|
|
15
|
+
* the same code cannot both pass before the step is recorded.
|
|
16
|
+
*/
|
|
17
|
+
import { Secret, TOTP, URI } from 'otpauth';
|
|
18
|
+
import { openTotpSecret, sealTotpSecret } from '../utils/totpSeedCipher.js';
|
|
19
|
+
import { KeyedSerial } from '../utils/keyedSerial.js';
|
|
20
|
+
/** RFC 6238 defaults, which every authenticator app supports. */
|
|
21
|
+
const PERIOD = 30;
|
|
22
|
+
const DIGITS = 6;
|
|
23
|
+
/** Steps either side of now that are accepted. */
|
|
24
|
+
export const TOTP_DRIFT_STEPS = 1;
|
|
25
|
+
function totpFor(seed, issuer = '', label = '') {
|
|
26
|
+
return new TOTP({ issuer, label, algorithm: 'SHA1', digits: DIGITS, period: PERIOD, secret: Secret.fromBase32(seed) });
|
|
27
|
+
}
|
|
28
|
+
function isCode(code) {
|
|
29
|
+
return typeof code === 'string' && /^\d{6}$/.test(code);
|
|
30
|
+
}
|
|
31
|
+
function currentStep(now) {
|
|
32
|
+
return Math.floor(now / 1000 / PERIOD);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The time step `code` belongs to within the drift window, or null. Pure; the
|
|
36
|
+
* caller decides about replay.
|
|
37
|
+
*/
|
|
38
|
+
export function matchingStep(seed, code, now) {
|
|
39
|
+
if (!isCode(code))
|
|
40
|
+
return null;
|
|
41
|
+
const delta = totpFor(seed).validate({ token: code, timestamp: now, window: TOTP_DRIFT_STEPS });
|
|
42
|
+
return delta === null ? null : currentStep(now) + delta;
|
|
43
|
+
}
|
|
44
|
+
export class TotpAuthProvider {
|
|
45
|
+
credentialsKey;
|
|
46
|
+
issuer;
|
|
47
|
+
store;
|
|
48
|
+
now;
|
|
49
|
+
id = 'totp';
|
|
50
|
+
displayName = 'Authenticator app';
|
|
51
|
+
factor = { amr: ['otp'], aal: 1, primary: false };
|
|
52
|
+
serial = new KeyedSerial();
|
|
53
|
+
/**
|
|
54
|
+
* @param credentialsKey `NGDPBASE_CREDENTIALS_KEY`, from which the seed key is derived
|
|
55
|
+
* @param issuer shown in the authenticator app beside the account
|
|
56
|
+
*/
|
|
57
|
+
constructor(credentialsKey, issuer, store, now = Date.now) {
|
|
58
|
+
this.credentialsKey = credentialsKey;
|
|
59
|
+
this.issuer = issuer;
|
|
60
|
+
this.store = store;
|
|
61
|
+
this.now = now;
|
|
62
|
+
if (!credentialsKey)
|
|
63
|
+
throw new Error('TOTP needs NGDPBASE_CREDENTIALS_KEY (#421)');
|
|
64
|
+
}
|
|
65
|
+
/** A fresh 160-bit seed and its URI for `username`. Nothing is stored. */
|
|
66
|
+
newEnrolment(username) {
|
|
67
|
+
const seed = new Secret({ size: 20 }).base32;
|
|
68
|
+
return { seed, uri: URI.stringify(totpFor(seed, this.issuer, username)) };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Seal a seed that is offered but not yet confirmed, so it can wait in the
|
|
72
|
+
* session without being readable there or in a session log (#421). Its own
|
|
73
|
+
* shape, so it never opens as a stored secret.
|
|
74
|
+
*/
|
|
75
|
+
sealPending(username, seed) {
|
|
76
|
+
return sealTotpSecret(this.credentialsKey, username, JSON.stringify({ pendingSeed: seed }));
|
|
77
|
+
}
|
|
78
|
+
/** Open what `sealPending` made for `username`; null when it is not that. */
|
|
79
|
+
openPending(username, sealed) {
|
|
80
|
+
if (typeof sealed !== 'string')
|
|
81
|
+
return null;
|
|
82
|
+
const plain = openTotpSecret(this.credentialsKey, username, sealed);
|
|
83
|
+
if (!plain)
|
|
84
|
+
return null;
|
|
85
|
+
try {
|
|
86
|
+
const p = JSON.parse(plain);
|
|
87
|
+
return typeof p.pendingSeed === 'string' ? p.pendingSeed : null;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Check the code that confirms an enrolment and seal the seed for the store.
|
|
95
|
+
* The confirming code's step is recorded, so it cannot then sign anyone in.
|
|
96
|
+
* Null when the seed is malformed or the code does not match.
|
|
97
|
+
*/
|
|
98
|
+
confirmEnrolment(username, seed, code) {
|
|
99
|
+
let step;
|
|
100
|
+
try {
|
|
101
|
+
step = matchingStep(seed, code, this.now());
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return null; // not base32
|
|
105
|
+
}
|
|
106
|
+
if (step === null)
|
|
107
|
+
return null;
|
|
108
|
+
return sealTotpSecret(this.credentialsKey, username, JSON.stringify({ seed, lastStep: step }));
|
|
109
|
+
}
|
|
110
|
+
/** Verify `credentials.code` for `credentials.username`. A failure gives no reason. */
|
|
111
|
+
async verify(credentials) {
|
|
112
|
+
const { username, code } = credentials;
|
|
113
|
+
if (typeof username !== 'string' || !username || !isCode(code))
|
|
114
|
+
return null;
|
|
115
|
+
return this.serial.run(username, async () => {
|
|
116
|
+
const now = this.now();
|
|
117
|
+
for (const row of this.store.rows(username)) {
|
|
118
|
+
const secret = this.open(row);
|
|
119
|
+
if (!secret)
|
|
120
|
+
continue;
|
|
121
|
+
const step = matchingStep(secret.seed, code, now);
|
|
122
|
+
if (step === null || step <= secret.lastStep)
|
|
123
|
+
continue;
|
|
124
|
+
const next = sealTotpSecret(this.credentialsKey, username, JSON.stringify({ seed: secret.seed, lastStep: step }));
|
|
125
|
+
await this.store.used(row.id, new Date(now).toISOString(), next);
|
|
126
|
+
return { username };
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
open(row) {
|
|
132
|
+
if (row.kind !== 'totp')
|
|
133
|
+
return null;
|
|
134
|
+
const plain = openTotpSecret(this.credentialsKey, row.username, row.secret);
|
|
135
|
+
if (!plain)
|
|
136
|
+
return null;
|
|
137
|
+
try {
|
|
138
|
+
const s = JSON.parse(plain);
|
|
139
|
+
return typeof s.seed === 'string' && typeof s.lastStep === 'number' ? s : null;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -30,6 +30,8 @@ import { type RoleGrant } from '../utils/roleGrants.js';
|
|
|
30
30
|
import { type ActorContext } from '../context/ActorContext.js';
|
|
31
31
|
export declare const POLICIES_KEY = "ngdpbase.access.policies";
|
|
32
32
|
export declare const POLICIES_ENABLED_KEY = "ngdpbase.access.policies.enabled";
|
|
33
|
+
/** Whether an administrator may enter another person's unencrypted vault (#2255). */
|
|
34
|
+
export declare const VAULTS_ADMIN_ACCESS_KEY = "ngdpbase.vaults.admin-access";
|
|
33
35
|
/**
|
|
34
36
|
* What `decide`, `permits` and `holds` take (#2022): the caller's own context
|
|
35
37
|
* — a request's `PermissionSubject` or a job's `JobContext` — or a subject
|
|
@@ -256,7 +258,20 @@ export declare class PolicyDecisionPoint extends BaseManager {
|
|
|
256
258
|
* own sign-in, once policy has said yes.
|
|
257
259
|
*/
|
|
258
260
|
private proxyCeiling;
|
|
259
|
-
/**
|
|
261
|
+
/**
|
|
262
|
+
* Does the named actor HOLD the action (policy alone, live)? Step-up is the
|
|
263
|
+
* proxy subject's question, not theirs. Asked about the same page when the
|
|
264
|
+
* request names one (#2255), so an actor who may edit only some categories
|
|
265
|
+
* is asked about this page's.
|
|
266
|
+
*
|
|
267
|
+
* Inside a vault or a shared folder the actor is asked the bare capability,
|
|
268
|
+
* as before #2255 (#2267): the `vault` and `shared-folder` matchers assume
|
|
269
|
+
* the container rule proved the subject may be there, and for the actor it
|
|
270
|
+
* did not — the person represented passed it. Asked with the container, a
|
|
271
|
+
* reader actor would hold everything `vault-owner` holds in someone else's
|
|
272
|
+
* vault. The door (`permitsInVault`), the page door and the list filter all
|
|
273
|
+
* come through here, so they agree.
|
|
274
|
+
*/
|
|
260
275
|
private actorHolds;
|
|
261
276
|
/**
|
|
262
277
|
* Decide. The order is load-bearing:
|
|
@@ -277,5 +292,46 @@ export declare class PolicyDecisionPoint extends BaseManager {
|
|
|
277
292
|
decide(subject: DecisionInput, request: DecisionRequest): Promise<Decision>;
|
|
278
293
|
/** `decide`, with the step-up rule applied or not — {@link holds} is the only caller that leaves it out. */
|
|
279
294
|
private evaluate;
|
|
295
|
+
/**
|
|
296
|
+
* The first declared system category on which the policies permit a page
|
|
297
|
+
* write (#2255, decision 10), or null. Asked once per category, so a deny
|
|
298
|
+
* on one still wins there. Owner conditions are unknown here (no page), so
|
|
299
|
+
* a grant that needs the author does not count. Only enabled categories
|
|
300
|
+
* that are not bound to a location count (#2268): `shared` is a shared
|
|
301
|
+
* folder's, decided with the folder, and every account holds `vault-owner`,
|
|
302
|
+
* so counting it would give a reader every page write as a capability.
|
|
303
|
+
*/
|
|
304
|
+
private onSomeCategory;
|
|
305
|
+
/**
|
|
306
|
+
* Every system category the catalogue declares, core and add-ons, enabled
|
|
307
|
+
* or not, lowercased; with `inUse`, only those enabled and not bound to a
|
|
308
|
+
* shared folder (`storageLocation.sharedfolder`).
|
|
309
|
+
*/
|
|
310
|
+
private declaredCategories;
|
|
311
|
+
/**
|
|
312
|
+
* May an administrator enter `owner`'s vault (#2255,
|
|
313
|
+
* `ngdpbase.vaults.admin-access`)? Asked only after the container rule has
|
|
314
|
+
* refused someone who is not the owner. Yes when all hold:
|
|
315
|
+
*
|
|
316
|
+
* - the key is true, read now, on every request (shipped true);
|
|
317
|
+
* - the caller is a signed-in session or a job, not a share link, a proxy
|
|
318
|
+
* or an agent token;
|
|
319
|
+
* - the caller holds `admin-system`;
|
|
320
|
+
* - the vault is not encrypted: an encrypted vault always needs its key,
|
|
321
|
+
* whatever the key says, so it is not entered at all.
|
|
322
|
+
*
|
|
323
|
+
* `record` true (a door) writes `vault-admin-access` with the administrator
|
|
324
|
+
* as the actor and the owner, vault and page named; an affordance passes
|
|
325
|
+
* false and records nothing. Lists and search never ask this, so an
|
|
326
|
+
* administrator does not see other people's vault pages listed.
|
|
327
|
+
*/
|
|
328
|
+
adminMayEnterVault(ctx: ActorContext | null | undefined, owner: string, vault: string, resource: string, record?: boolean): Promise<boolean>;
|
|
329
|
+
/**
|
|
330
|
+
* The declared system categories on which an allow policy grants `action`
|
|
331
|
+
* through the `system-category` resource type (#2255). The sitemap asks it
|
|
332
|
+
* with `pushed-pages-edit` to leave pushed pages out. A summary of the
|
|
333
|
+
* policies, never an answer to "may this subject".
|
|
334
|
+
*/
|
|
335
|
+
categoriesGranting(action: string): string[];
|
|
280
336
|
}
|
|
281
337
|
export default PolicyDecisionPoint;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import BaseManager from '../managers/BaseManager.js';
|
|
2
|
+
import { type AalFacts } from '../utils/policyAttributes.js';
|
|
2
3
|
import { type ActorContext } from '../context/ActorContext.js';
|
|
3
4
|
import { WikiEngine } from '../types/WikiEngine.js';
|
|
4
5
|
import type { AgentTokenGrant, ProxyClaim } from '../managers/UserManager.js';
|
|
@@ -52,6 +53,10 @@ interface UserContext {
|
|
|
52
53
|
* form. They convert it themselves, through {@link asSubject}.
|
|
53
54
|
*/
|
|
54
55
|
type PageQuestionCaller = ActorContext | UserContext | null | undefined;
|
|
56
|
+
/** What a subject attribute is read from (#2261): who, and what bears on `subject:aal`. */
|
|
57
|
+
export type SubjectFacts = AalFacts & {
|
|
58
|
+
username?: string;
|
|
59
|
+
};
|
|
55
60
|
/**
|
|
56
61
|
* Access decision log entry
|
|
57
62
|
*/
|
|
@@ -61,8 +66,24 @@ interface AccessDecisionLog {
|
|
|
61
66
|
action?: string;
|
|
62
67
|
allowed?: boolean;
|
|
63
68
|
reason?: string;
|
|
69
|
+
/** The policies that decided (#2255), recorded on a denial. */
|
|
70
|
+
policyIds?: string[];
|
|
71
|
+
/** Allows that matched but whose condition was not met (#2288), recorded on a denial. */
|
|
72
|
+
conditionNotMet?: string[];
|
|
64
73
|
context?: Record<string, unknown>;
|
|
65
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* What the policies may read about a page (#2255), resolved by the PIP from
|
|
77
|
+
* the page itself. `attributes` holds what a resource type matches on
|
|
78
|
+
* (`system-category`), read eagerly; `read` answers a condition's
|
|
79
|
+
* `resource:<name>` for an attribute declared on the `page` resource type,
|
|
80
|
+
* lazily, so a lookup (the owner's account) is made only when a policy needs
|
|
81
|
+
* it. `undefined` is a missing attribute.
|
|
82
|
+
*/
|
|
83
|
+
export interface PageAttributes {
|
|
84
|
+
attributes: Record<string, unknown>;
|
|
85
|
+
read(name: string): unknown;
|
|
86
|
+
}
|
|
66
87
|
/**
|
|
67
88
|
* PolicyInformationPoint — what a page decision needs to know about the page
|
|
68
89
|
* (#1431; was ACLManager until step 8).
|
|
@@ -353,10 +374,12 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
353
374
|
* Tier 1 is skipped for a vault page: `audience` grants nothing there.
|
|
354
375
|
* - __Tier 0.5, author-lock__, `edit` only. It only denies: an author, or a
|
|
355
376
|
* subject holding `admin-system` (#1431 7b), falls through to Tier 1+.
|
|
356
|
-
* - __Tier 1, frontmatter__ `
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
377
|
+
* - __Tier 1, frontmatter__ `audience`. Page-level, and decides a view
|
|
378
|
+
* when it states a rule (#1054). An `audience` covers every action, and a
|
|
379
|
+
* subject holding `admin-system` is its one exception (#1964). `access`
|
|
380
|
+
* is no longer read (#2255).
|
|
381
|
+
* - A page in a shared folder is decided by the folder's permission, and a
|
|
382
|
+
* change also by the `shared` category's policy and author-lock (#2255).
|
|
360
383
|
* - __Tier 2.__ For a share visitor the share IS the policy (#1222); for
|
|
361
384
|
* anyone else, global policy.
|
|
362
385
|
* - __Default deny.__ Nothing below Tier 2 grants — JSPWiki's page-body ACL
|
|
@@ -477,14 +500,65 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
477
500
|
*/
|
|
478
501
|
canUserAccessMediaItem(caller: PageQuestionCaller, item: MediaItem): Promise<boolean>;
|
|
479
502
|
/**
|
|
480
|
-
* Check front matter audience
|
|
503
|
+
* Check the front matter audience (Tier 1).
|
|
481
504
|
* Returns a decision object; decided=false means no front matter restriction — fall through.
|
|
482
505
|
*
|
|
483
506
|
* An audience that leaves the subject out refuses every action, unless the
|
|
484
507
|
* subject holds `admin-system` (#1964, operator 2026-10-23): asked only
|
|
485
|
-
* then. `access
|
|
508
|
+
* then. `access` is no longer read (#2255).
|
|
486
509
|
*/
|
|
487
510
|
private checkFrontmatterAccess;
|
|
511
|
+
/**
|
|
512
|
+
* A page's attributes for the policies (#2255, XACML's PIP): the PDP asks
|
|
513
|
+
* this from the page id, so no route passes them. `knownMetadata` is the
|
|
514
|
+
* frontmatter when this PIP already holds it (the page door); otherwise the
|
|
515
|
+
* page is read with the caller's own context, so a sealed page stays closed
|
|
516
|
+
* to anyone without its key (#1422). A page that cannot be read has no
|
|
517
|
+
* attributes: no category policy matches it and every condition on it is
|
|
518
|
+
* unknown.
|
|
519
|
+
*/
|
|
520
|
+
pageAttributes(caller: PageQuestionCaller, pageName: string, knownMetadata?: Record<string, unknown> | null): Promise<PageAttributes>;
|
|
521
|
+
/**
|
|
522
|
+
* {@link pageAttributes} from frontmatter already in hand (the list
|
|
523
|
+
* filter). Attributes are those declared on the `page` resource type in
|
|
524
|
+
* `ngdpbase.access.resource-types`, each read from its `from` source; the
|
|
525
|
+
* sources are code (`frontmatter:<field>` today).
|
|
526
|
+
*
|
|
527
|
+
* - `system-category` is lowercased; a page with none is in the default
|
|
528
|
+
* category (the entry with `default: true`); a page in a shared folder is
|
|
529
|
+
* `shared`, whatever its frontmatter says (#2255).
|
|
530
|
+
* - An `identity` attribute (`owner`, from `author`) is compared by the
|
|
531
|
+
* account it names: an @id or a username not yet converted both read as
|
|
532
|
+
* that account's username, an orphan as missing (#2097).
|
|
533
|
+
*/
|
|
534
|
+
pageAttributesFrom(pageName: string, metadata: PageFrontmatter | null | undefined): PageAttributes;
|
|
535
|
+
/**
|
|
536
|
+
* A subject attribute a condition reads (#2255, #2261): `subject:<name>`,
|
|
537
|
+
* declared in `ngdpbase.access.subject-attributes` (`id` always is) and read
|
|
538
|
+
* from its `from` source here, the only place a subject attribute is read. A
|
|
539
|
+
* request cannot send one. Missing when it is not declared, cannot be read, or
|
|
540
|
+
* the subject has none (`subject:aal` of a visitor who is not signed in, of a
|
|
541
|
+
* token made before it was stored); the evaluator then fails closed. Returns
|
|
542
|
+
* the value, or a Promise of it for a field read from the account (the
|
|
543
|
+
* evaluator awaits every read).
|
|
544
|
+
*/
|
|
545
|
+
subjectAttribute(subject: SubjectFacts | null | undefined, name: string): unknown;
|
|
546
|
+
/** One read of a declared subject attribute from its source. A value of the wrong type, or a source that throws, is missing. */
|
|
547
|
+
private readSubjectSource;
|
|
548
|
+
/** The page's category for the policies (#2255): lowercased; the default when it has none; `shared` in a shared folder. */
|
|
549
|
+
private pageCategory;
|
|
550
|
+
/** The attributes declared on the `page` resource type (#2255). */
|
|
551
|
+
private declaredPageAttributes;
|
|
552
|
+
/** An identity reference as the policies compare it: the username of the account it names, or missing (#2097). */
|
|
553
|
+
private identityValue;
|
|
554
|
+
/** Pages already warned about for an `access:` that is not the legacy stamp (#2255): once per page per process. */
|
|
555
|
+
private readonly warnedAccess;
|
|
556
|
+
/**
|
|
557
|
+
* `access:` frontmatter no longer has any effect (#2255). The stamp install
|
|
558
|
+
* wrote (`{ edit: [admin] }`) is left alone silently; any other value is
|
|
559
|
+
* someone's rule that is now ignored, so it is said once per page.
|
|
560
|
+
*/
|
|
561
|
+
private warnLegacyAccess;
|
|
488
562
|
/**
|
|
489
563
|
* Does the page's `audience` admit this subject (#1964)? True when the page
|
|
490
564
|
* names no audience, the subject matches it, or the subject holds
|
|
@@ -501,6 +575,12 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
501
575
|
* Accepts either a single object or positional args for backward compatibility.
|
|
502
576
|
*/
|
|
503
577
|
logAccessDecision(userOrObj: UserContext | AccessDecisionLog, pageName?: string, action?: string, allowed?: boolean, reason?: string, _context?: Record<string, unknown>): void;
|
|
578
|
+
/**
|
|
579
|
+
* Record a page write a door refused on a capability alone (#2272): the
|
|
580
|
+
* route's `page-edit` / `page-create` check asks the PDP, which records
|
|
581
|
+
* nothing. Same record as every other page denial, `policyIds` included.
|
|
582
|
+
*/
|
|
583
|
+
recordPageRefusal(subject: UserContext | null | undefined, pageName: string, action: string, reason: string, policyIds?: readonly string[], conditionNotMet?: readonly string[]): Promise<void>;
|
|
504
584
|
/** The name a decision is recorded under: the subject's, or `anonymous`. */
|
|
505
585
|
private static recordedUser;
|
|
506
586
|
/**
|
|
@@ -30,6 +30,8 @@ export type CorePermission =
|
|
|
30
30
|
| 'identity-read'
|
|
31
31
|
/** Browse and view the media library (not delegated to anonymous visitors) */
|
|
32
32
|
| 'media-read'
|
|
33
|
+
/** Set or change a page's system-category (#2255). A page created in a shared folder gets the shared category without it */
|
|
34
|
+
| 'page-categorize'
|
|
33
35
|
/** Create new pages */
|
|
34
36
|
| 'page-create'
|
|
35
37
|
/** Delete pages */
|
|
@@ -50,6 +52,12 @@ export type CorePermission =
|
|
|
50
52
|
| 'profile-manage'
|
|
51
53
|
/** Let another person act for you, within permissions you hold, and revoke it (#1762). Every signed-in role except demo-admin; an administrator making or revoking a grant for someone else also needs user-edit, and that grant never carries a copy of the person's vault key. A grant that includes proxy-grant lets the proxy give others access too. Asks for a fresh sign-in */
|
|
52
54
|
| 'proxy-grant'
|
|
55
|
+
/** Delete pushed pages (#2255) */
|
|
56
|
+
| 'pushed-pages-delete'
|
|
57
|
+
/** Edit pushed pages: pages shipped by the system or an add-on (the categories a policy grants it on; system, documentation and addon as shipped) (#2255) */
|
|
58
|
+
| 'pushed-pages-edit'
|
|
59
|
+
/** Rename pushed pages (#2255) */
|
|
60
|
+
| 'pushed-pages-rename'
|
|
53
61
|
/** Search pages */
|
|
54
62
|
| 'search-page'
|
|
55
63
|
/** Search users */
|
|
@@ -33,6 +33,15 @@ export declare const RECOVERY_WORDS_DOCUMENT = "recovery-words";
|
|
|
33
33
|
export declare const SHARED_FOLDERS_WORDS_CONSENT_SCOPE = "shared-folders-recovery-words";
|
|
34
34
|
/** The page whose `version:` is the version of the shared-folders recovery-words wording (#2124). */
|
|
35
35
|
export declare const SHARED_FOLDERS_WORDS_DOCUMENT = "shared-folders-recovery-words";
|
|
36
|
+
/**
|
|
37
|
+
* The scope of "Remember this browser" (#1523 slice 1c, Q5): a person's
|
|
38
|
+
* opt-in, at the second-factor step, to a device key on this browser that
|
|
39
|
+
* skips the second-factor prompt next time. Bound to a subject: the remembered
|
|
40
|
+
* device's id, so each browser is its own consent, withdrawn by removing it.
|
|
41
|
+
*/
|
|
42
|
+
export declare const REMEMBER_BROWSER_CONSENT_SCOPE = "remember-browser";
|
|
43
|
+
/** The page whose `version:` is the version of the "Remember this browser" wording (#1523 slice 1c). */
|
|
44
|
+
export declare const REMEMBER_BROWSER_DOCUMENT = "remember-browser";
|
|
36
45
|
/** Core's scopes bound to a subject, each with its one document (#1928, #2124). */
|
|
37
46
|
export declare const SUBJECT_CONSENT_DOCUMENTS: Readonly<Record<ConsentScope, string>>;
|
|
38
47
|
/** Scopes whose acceptance is about one thing (a subject), not the account as a whole (#1928). */
|
|
@@ -61,17 +70,21 @@ export interface ConsentSeen {
|
|
|
61
70
|
version: string;
|
|
62
71
|
}
|
|
63
72
|
/**
|
|
64
|
-
* One acceptance, one line of the person's file.
|
|
65
|
-
* version is a later line,
|
|
73
|
+
* One acceptance, or its withdrawal, one line of the person's file.
|
|
74
|
+
* Append-only: a later version is a later line, and a withdrawal is a later
|
|
75
|
+
* `withdraw` line naming the same document, version and subject; nothing is
|
|
76
|
+
* rewritten.
|
|
66
77
|
*/
|
|
67
78
|
export interface ConsentRecord {
|
|
68
|
-
kind: 'accept';
|
|
79
|
+
kind: 'accept' | 'withdraw';
|
|
69
80
|
document: string;
|
|
70
81
|
scope: ConsentScope;
|
|
71
82
|
version: string;
|
|
72
83
|
effective: string | null;
|
|
73
|
-
/** ISO 8601 time the person accepted. */
|
|
84
|
+
/** ISO 8601 time the person accepted (on a `withdraw` line, of the acceptance withdrawn). */
|
|
74
85
|
acceptedAt: string;
|
|
86
|
+
/** ISO 8601 time it was withdrawn; on a `withdraw` line only. */
|
|
87
|
+
withdrawnAt?: string;
|
|
75
88
|
/**
|
|
76
89
|
* The account this was accepted on, by its creation time (#1937): a later
|
|
77
90
|
* account that takes the same username never inherits it.
|
package/lib/src/types/Consent.js
CHANGED
|
@@ -31,10 +31,20 @@ export const RECOVERY_WORDS_DOCUMENT = 'recovery-words';
|
|
|
31
31
|
export const SHARED_FOLDERS_WORDS_CONSENT_SCOPE = 'shared-folders-recovery-words';
|
|
32
32
|
/** The page whose `version:` is the version of the shared-folders recovery-words wording (#2124). */
|
|
33
33
|
export const SHARED_FOLDERS_WORDS_DOCUMENT = 'shared-folders-recovery-words';
|
|
34
|
+
/**
|
|
35
|
+
* The scope of "Remember this browser" (#1523 slice 1c, Q5): a person's
|
|
36
|
+
* opt-in, at the second-factor step, to a device key on this browser that
|
|
37
|
+
* skips the second-factor prompt next time. Bound to a subject: the remembered
|
|
38
|
+
* device's id, so each browser is its own consent, withdrawn by removing it.
|
|
39
|
+
*/
|
|
40
|
+
export const REMEMBER_BROWSER_CONSENT_SCOPE = 'remember-browser';
|
|
41
|
+
/** The page whose `version:` is the version of the "Remember this browser" wording (#1523 slice 1c). */
|
|
42
|
+
export const REMEMBER_BROWSER_DOCUMENT = 'remember-browser';
|
|
34
43
|
/** Core's scopes bound to a subject, each with its one document (#1928, #2124). */
|
|
35
44
|
export const SUBJECT_CONSENT_DOCUMENTS = {
|
|
36
45
|
[RECOVERY_WORDS_CONSENT_SCOPE]: RECOVERY_WORDS_DOCUMENT,
|
|
37
|
-
[SHARED_FOLDERS_WORDS_CONSENT_SCOPE]: SHARED_FOLDERS_WORDS_DOCUMENT
|
|
46
|
+
[SHARED_FOLDERS_WORDS_CONSENT_SCOPE]: SHARED_FOLDERS_WORDS_DOCUMENT,
|
|
47
|
+
[REMEMBER_BROWSER_CONSENT_SCOPE]: REMEMBER_BROWSER_DOCUMENT
|
|
38
48
|
};
|
|
39
49
|
/** Scopes whose acceptance is about one thing (a subject), not the account as a whole (#1928). */
|
|
40
50
|
export const SUBJECT_CONSENT_SCOPES = Object.keys(SUBJECT_CONSENT_DOCUMENTS);
|
package/lib/src/types/Page.d.ts
CHANGED
|
@@ -95,13 +95,6 @@ export interface PageFrontmatter {
|
|
|
95
95
|
* in Slice E (v3.7.0). All datasets migrated; this field is the only source of truth.
|
|
96
96
|
*/
|
|
97
97
|
private?: boolean;
|
|
98
|
-
/** Per-action principal lists — overrides audience for the named action */
|
|
99
|
-
access?: {
|
|
100
|
-
view?: string[];
|
|
101
|
-
edit?: string[];
|
|
102
|
-
delete?: string[];
|
|
103
|
-
[key: string]: string[] | undefined;
|
|
104
|
-
};
|
|
105
98
|
/** Additional custom metadata */
|
|
106
99
|
[key: string]: unknown;
|
|
107
100
|
}
|