@jwilleke/ngdpbase-types 5.43.0 → 5.44.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.d.ts +10 -3
- package/lib/src/context/ApiContext.js +36 -3
- package/lib/src/ice/runExport.d.ts +8 -3
- package/lib/src/ice/searchExportSource.d.ts +44 -0
- package/lib/src/ice/types.d.ts +6 -0
- package/lib/src/managers/AttachmentManager.d.ts +11 -0
- package/lib/src/managers/AuditManager.d.ts +0 -1
- package/lib/src/managers/AuthManager.d.ts +38 -0
- package/lib/src/managers/AuthManager.js +84 -12
- package/lib/src/managers/BackupManager.d.ts +9 -2
- package/lib/src/managers/BackupManager.js +42 -19
- package/lib/src/managers/BaseManager.d.ts +17 -3
- package/lib/src/managers/BaseManager.js +12 -8
- package/lib/src/managers/ConfigurationManager.d.ts +13 -0
- package/lib/src/managers/ConsentManager.d.ts +1 -1
- package/lib/src/managers/DatabaseManager.js +17 -8
- package/lib/src/managers/EmailManager.d.ts +1 -1
- package/lib/src/managers/ExportManager.d.ts +1 -44
- package/lib/src/managers/ExportManager.js +3 -115
- package/lib/src/managers/ImportManager.d.ts +7 -0
- package/lib/src/managers/PageManager.d.ts +9 -0
- package/lib/src/managers/PolicyEvaluator.d.ts +113 -30
- package/lib/src/managers/ThemeManager.d.ts +20 -1
- package/lib/src/providers/FileLoggingProvider.d.ts +12 -1
- package/lib/src/providers/FileLoggingProvider.js +37 -4
- package/lib/src/security/PolicyDecisionPoint.d.ts +50 -3
- package/lib/src/security/PolicyInformationPoint.d.ts +27 -1
- package/lib/src/security/ownedRecords.d.ts +69 -0
- package/lib/src/security/signedOutWrites.d.ts +69 -0
- package/lib/src/security/signedOutWrites.js +146 -0
- package/lib/src/types/Policy.d.ts +26 -30
- package/lib/src/utils/addonConfigLayer.d.ts +6 -3
- package/lib/src/utils/addonConfigLayer.js +61 -3
- package/lib/src/utils/addonHost.d.ts +25 -0
- package/lib/src/utils/addonHost.js +29 -1
- package/lib/src/utils/availability.d.ts +15 -0
- package/lib/src/utils/availability.js +88 -0
- package/lib/src/utils/pageTitleConflict.d.ts +32 -0
- package/lib/src/utils/pageWritePolicies.d.ts +48 -29
- package/lib/src/utils/plainAttention.d.ts +19 -0
- package/lib/src/utils/plainAttention.js +18 -0
- package/lib/src/utils/policyAttributes.d.ts +113 -22
- package/lib/src/utils/policyAttributes.js +228 -43
- package/lib/src/utils/roleGrants.d.ts +1 -1
- package/package.json +7 -7
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import BaseManager from './BaseManager.js';
|
|
2
2
|
import type { Policy, PolicySubject, PolicyResource, PolicyCondition } from '../types/Policy.js';
|
|
3
|
-
import { type DeclaredSubjectAttribute } from '../utils/policyAttributes.js';
|
|
3
|
+
import { type DeclaredEnvironmentAttribute, type DeclaredSubjectAttribute } from '../utils/policyAttributes.js';
|
|
4
4
|
import { WikiEngine } from '../types/WikiEngine.js';
|
|
5
|
+
import type { ActorContext } from '../context/ActorContext.js';
|
|
5
6
|
/**
|
|
6
7
|
* User context for policy evaluation
|
|
7
8
|
*/
|
|
@@ -43,28 +44,34 @@ interface AccessContext {
|
|
|
43
44
|
readAttribute?: AttributeReader;
|
|
44
45
|
}
|
|
45
46
|
/**
|
|
46
|
-
* What is wrong with a policy's condition, one sentence each (#2255, #2261
|
|
47
|
-
* empty when it is fine. Each sentence says what is wrong, then what to
|
|
48
|
-
* change. A node is
|
|
49
|
-
* `
|
|
50
|
-
* {@link MAX_CONDITION_DEPTH} deep, at most
|
|
51
|
-
* in all, an `in` list of at most
|
|
52
|
-
* `
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
47
|
+
* What is wrong with a policy's condition, one sentence each (#2255, #2261,
|
|
48
|
+
* #2330); empty when it is fine. Each sentence says what is wrong, then what to
|
|
49
|
+
* change. A node is one XACML function in lowercase: a comparison such as
|
|
50
|
+
* `string-equal` over two arguments, or `and`, `or` or `not` over other
|
|
51
|
+
* conditions, nested at most {@link MAX_CONDITION_DEPTH} deep, at most
|
|
52
|
+
* {@link MAX_CONDITION_NODES} function nodes in all, an `is-in` list of at most
|
|
53
|
+
* {@link MAX_IN_ITEMS} and an `and` or `or` of at most {@link MAX_GROUP_CHILDREN}.
|
|
54
|
+
* An argument is an attribute (`attributeid`, `datatype`) or a literal (`value`,
|
|
55
|
+
* `datatype`); `datatype` is required, must be the type the attribute is declared with
|
|
56
|
+
* and the type the function takes. Each attribute must be declared: a
|
|
57
|
+
* `subject:<name>` in `ngdpbase.access.subject-attributes` (`id` always, `aal`
|
|
58
|
+
* as shipped), or a `resource:<name>` declared on the resource type the policy names (the
|
|
59
|
+
* `page` type when it names a page, or that add-on's type when it names only
|
|
60
|
+
* that type, #1823), or an `environment:<name>` in `ngdpbase.access.environment-attributes`. The old
|
|
61
|
+
* shape (`attribute` with `equals` and the like) is a problem that shows the
|
|
62
|
+
* new one. A condition with a problem never holds: an allow with it does not
|
|
63
|
+
* apply and a deny with it does, as for a missing attribute.
|
|
58
64
|
*/
|
|
59
65
|
export declare function conditionProblems(condition: unknown, pageAttributes: Record<string, {
|
|
60
66
|
type?: string;
|
|
61
|
-
}>, subjectAttributes?: Record<string, DeclaredSubjectAttribute
|
|
67
|
+
}>, subjectAttributes?: Record<string, DeclaredSubjectAttribute>, environmentAttributes?: Record<string, DeclaredEnvironmentAttribute>, resourceTypeLabel?: string): string[];
|
|
62
68
|
/**
|
|
63
69
|
* Is the condition true, false or unknown (#2255, #2261)? `true` or `false`,
|
|
64
70
|
* or `undefined` when it cannot be told because an attribute it reads is
|
|
65
|
-
* missing
|
|
66
|
-
*
|
|
67
|
-
* a
|
|
71
|
+
* missing, or an argument lacks the datatype its function takes; the caller
|
|
72
|
+
* fails closed on that. Each node is judged on its own: a missing attribute
|
|
73
|
+
* makes its comparison unknown, a true branch decides `or`, a false branch
|
|
74
|
+
* decides `and`, and `not` of unknown is unknown.
|
|
68
75
|
*/
|
|
69
76
|
export declare function conditionHolds(condition: PolicyCondition | undefined, read: AttributeReader | undefined, depth?: number): Promise<boolean | undefined>;
|
|
70
77
|
/**
|
|
@@ -87,6 +94,25 @@ export declare const SUBJECT_MATCHERS: Record<string, (value: string | undefined
|
|
|
87
94
|
* on one is refused rather than ignored.
|
|
88
95
|
*/
|
|
89
96
|
export declare const VALUELESS_SUBJECT_TYPES: readonly string[];
|
|
97
|
+
/**
|
|
98
|
+
* Which resource attributes a policy's condition may read (#1823).
|
|
99
|
+
*
|
|
100
|
+
* A policy that names exactly one add-on type (no page matcher) uses that
|
|
101
|
+
* type's attributes. A policy on a core type, including `system-category`,
|
|
102
|
+
* still reads the `page` attributes: those conditions are about the page.
|
|
103
|
+
*/
|
|
104
|
+
export declare function conditionAttributeSource(resources: readonly {
|
|
105
|
+
type?: string;
|
|
106
|
+
}[] | undefined, types: Record<string, {
|
|
107
|
+
attributes?: Record<string, {
|
|
108
|
+
type?: string;
|
|
109
|
+
}>;
|
|
110
|
+
}> | null): {
|
|
111
|
+
attributes: Record<string, {
|
|
112
|
+
type?: string;
|
|
113
|
+
}>;
|
|
114
|
+
label: string;
|
|
115
|
+
};
|
|
90
116
|
/**
|
|
91
117
|
* The resource types configuration declares (#1504), for the evaluator and the
|
|
92
118
|
* validator alike. When the key is not set at all — a configuration older than
|
|
@@ -112,8 +138,8 @@ interface EvaluationResult {
|
|
|
112
138
|
*
|
|
113
139
|
* PolicyEvaluator mimics how JSPWiki uses Java's built-in security framework
|
|
114
140
|
* (java.security) to load and evaluate security policies from a policy file.
|
|
115
|
-
* Among the policies that match, a deny overrides any allow
|
|
116
|
-
*
|
|
141
|
+
* Among the policies that match, a deny overrides any allow, the
|
|
142
|
+
* rule-combining algorithm `deny-overrides` (#2330), and no match is a deny (#2255). Each decision names the policies
|
|
117
143
|
* that decided it.
|
|
118
144
|
*
|
|
119
145
|
* @class PolicyEvaluator
|
|
@@ -136,6 +162,17 @@ interface EvaluationResult {
|
|
|
136
162
|
*/
|
|
137
163
|
declare class PolicyEvaluator extends BaseManager {
|
|
138
164
|
private configManager;
|
|
165
|
+
/** The headline of the access-policies item now up, so marking the last deny takes down this one and no other (#2355). */
|
|
166
|
+
private reportedReason;
|
|
167
|
+
/**
|
|
168
|
+
* Each condition checked and compiled, by the condition object configuration
|
|
169
|
+
* holds (#2279), kept while `ConfigurationManager.getRevision()` is
|
|
170
|
+
* `revision`. Any change to the configuration (a save, a reload, a reset, a
|
|
171
|
+
* restore, an add-on's runtime value) changes the revision, and the next
|
|
172
|
+
* decision starts again from nothing. The policies themselves are still read
|
|
173
|
+
* live for every decision; only a condition's compiled form is reused.
|
|
174
|
+
*/
|
|
175
|
+
private preparedConditions;
|
|
139
176
|
/**
|
|
140
177
|
* The policies in force NOW. Only the PDP interprets them (#1431 step 14b);
|
|
141
178
|
* this evaluator is the PDP's matching engine and asks it for the list, read
|
|
@@ -163,29 +200,67 @@ declare class PolicyEvaluator extends BaseManager {
|
|
|
163
200
|
*/
|
|
164
201
|
initialize(): Promise<void>;
|
|
165
202
|
/**
|
|
166
|
-
* Say at boot what
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* changed by nobody, admin included).
|
|
203
|
+
* Say at boot what a developer should look at in the policies after #2255,
|
|
204
|
+
* one warning each: stored policies the upgrade rewrote, policies it left
|
|
205
|
+
* that now decide differently (a deny) or reach pushed pages, and conditions
|
|
206
|
+
* that never hold. These are log lines only; categories no policy lets anyone
|
|
207
|
+
* change are an upgrade check (`upgradeChecks/uneditablePages.ts`).
|
|
172
208
|
*/
|
|
173
209
|
private reportPageWritePolicies;
|
|
210
|
+
/** The stored marks whose rule is still a deny in force (#2355). A mark for a rule since removed is not kept: the same rule added later must be shown again. */
|
|
211
|
+
private marksOfExistingDenies;
|
|
212
|
+
/** Save the marks without those of rules that are gone (#2355); nothing is written when none is. */
|
|
213
|
+
private dropMarksOfGoneRules;
|
|
214
|
+
/**
|
|
215
|
+
* An administrator marks a deny as checked (#2355). Kept as the rule's id and a fingerprint of
|
|
216
|
+
* its content in `ngdpbase.access.checked-denies`, written through ConfigurationManager with the
|
|
217
|
+
* person's context, which records the change. It hides the item and nothing else: no decision
|
|
218
|
+
* reads it. Answers `changed` when the rule is no longer what the page showed (nothing is
|
|
219
|
+
* saved) and `missing` when no deny has that id.
|
|
220
|
+
*/
|
|
221
|
+
markDenyChecked(ctx: ActorContext, id: string, fingerprint: string): Promise<'checked' | 'changed' | 'missing'>;
|
|
174
222
|
/**
|
|
175
223
|
* Evaluates all relevant policies to make an access decision (#2255).
|
|
176
224
|
*
|
|
177
|
-
* Deny overrides allow
|
|
178
|
-
*
|
|
179
|
-
* no policy decided. A policy with a `condition` applies only when it holds;
|
|
225
|
+
* Deny overrides allow (the rule-combining algorithm, #2330): if any matching
|
|
226
|
+
* deny applies, the answer is deny; otherwise any matching allow permits;
|
|
227
|
+
* otherwise no policy decided. A policy with a `condition` applies only when it holds;
|
|
180
228
|
* an unknown condition (a missing attribute) keeps an allow from applying
|
|
181
229
|
* and lets a deny apply, so a missing attribute fails closed.
|
|
182
230
|
*/
|
|
183
231
|
evaluateAccess(context: AccessContext): Promise<EvaluationResult>;
|
|
184
232
|
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
233
|
+
* The policies with their conditions checked and compiled (#2279): a
|
|
234
|
+
* condition already compiled under the configuration in force is reused,
|
|
235
|
+
* and `prepare` runs only for the rest. With no configuration revision to
|
|
236
|
+
* key on, everything is prepared again.
|
|
237
|
+
*/
|
|
238
|
+
private prepared;
|
|
239
|
+
/**
|
|
240
|
+
* Checks and compiles each policy's condition (#2261). `prepared()` keeps
|
|
241
|
+
* the result until the configuration changes (#2279).
|
|
187
242
|
*/
|
|
188
243
|
private prepare;
|
|
244
|
+
/**
|
|
245
|
+
* Decide one add-on record type by id (#1823).
|
|
246
|
+
*
|
|
247
|
+
* Only policies that name this type are considered, and a policy with no
|
|
248
|
+
* resources does not match: a page grant cannot cover the type, and a
|
|
249
|
+
* blanket policy cannot either. The id is matched with the same globs as a
|
|
250
|
+
* page name, a leading dot included. A core type (`page`, `system-category`, `vault`,
|
|
251
|
+
* `shared-folder`) is not decided here.
|
|
252
|
+
*/
|
|
253
|
+
evaluateDeclaredResource(context: {
|
|
254
|
+
resourceType: string;
|
|
255
|
+
id: string;
|
|
256
|
+
action: string;
|
|
257
|
+
actions?: readonly string[];
|
|
258
|
+
userContext?: UserContext;
|
|
259
|
+
readAttribute?: AttributeReader;
|
|
260
|
+
}): Promise<EvaluationResult>;
|
|
261
|
+
/** True when a policy resource names this type and its pattern covers the id. No resources is not a match. */
|
|
262
|
+
private namesDeclaredType;
|
|
263
|
+
private resourceTypeMap;
|
|
189
264
|
/**
|
|
190
265
|
* Deny overrides allow, over policies that already match subject, resource
|
|
191
266
|
* and action (#2255). Every matching deny whose condition is true or
|
|
@@ -264,6 +339,14 @@ declare class PolicyEvaluator extends BaseManager {
|
|
|
264
339
|
}>;
|
|
265
340
|
/** The subject attributes a condition may read (#2261): `id` always, and those declared in `ngdpbase.access.subject-attributes`. */
|
|
266
341
|
declaredSubjectAttributes(): Record<string, DeclaredSubjectAttribute>;
|
|
342
|
+
/** The environment attributes a condition may read (#2261): those declared in `ngdpbase.access.environment-attributes`. */
|
|
343
|
+
declaredEnvironmentAttributes(): Record<string, DeclaredEnvironmentAttribute>;
|
|
344
|
+
/**
|
|
345
|
+
* One line for each declared `schedule:` attribute whose schedule is not in
|
|
346
|
+
* `ngdpbase.schedules` (the attribute is missing and a condition on it fails
|
|
347
|
+
* closed) or has no rules (it reads as always open), for the boot warning.
|
|
348
|
+
*/
|
|
349
|
+
private scheduleProblems;
|
|
267
350
|
/** Each policy in force whose condition has a problem, with the problems (#2255): for the boot warning. */
|
|
268
351
|
conditionProblemsInForce(): Array<{
|
|
269
352
|
id: string;
|
|
@@ -5,6 +5,12 @@ export interface ThemeInfo {
|
|
|
5
5
|
author: string;
|
|
6
6
|
/** Optional Google Fonts (or other) stylesheet URLs to inject in <head> */
|
|
7
7
|
fonts?: string[];
|
|
8
|
+
/**
|
|
9
|
+
* Optional CSS filenames in this theme's css/ directory. Loaded after
|
|
10
|
+
* core.css so a site theme can adjust layout without editing the shared
|
|
11
|
+
* sheet. Basenames only; paths are rejected.
|
|
12
|
+
*/
|
|
13
|
+
stylesheets?: string[];
|
|
8
14
|
}
|
|
9
15
|
export interface ThemePaths {
|
|
10
16
|
/** Active theme name (folder name under themes/) */
|
|
@@ -23,6 +29,8 @@ export interface ThemePaths {
|
|
|
23
29
|
themeInfo: ThemeInfo | null;
|
|
24
30
|
/** Optional font stylesheet URLs from theme.json fonts[] */
|
|
25
31
|
fontUrls: string[];
|
|
32
|
+
/** Theme CSS files to load after core.css. Public URLs. */
|
|
33
|
+
themeStylesheets: string[];
|
|
26
34
|
}
|
|
27
35
|
/**
|
|
28
36
|
* Returns a cached ThemeManager for the given active theme + themes directory.
|
|
@@ -44,11 +52,22 @@ export declare class ThemeManager {
|
|
|
44
52
|
constructor(activeTheme: string, themesDir: string);
|
|
45
53
|
private loadThemeInfo;
|
|
46
54
|
private buildPaths;
|
|
55
|
+
/**
|
|
56
|
+
* Publish theme.json stylesheets[] as /themes/<theme>/css/<file>.
|
|
57
|
+
* Only a basename ending in .css that exists in that directory is kept,
|
|
58
|
+
* so a theme cannot point the browser at another path.
|
|
59
|
+
*/
|
|
60
|
+
private resolveThemeStylesheets;
|
|
47
61
|
/** Resolve variables.css path — falls back to empty placeholder if theme has no custom vars */
|
|
48
62
|
private resolveVariablesCssPath;
|
|
49
63
|
/** Resolve favicon path: prefer favicon.svg → favicon.png → public fallback */
|
|
50
64
|
private resolveFaviconPath;
|
|
51
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the header logo path: prefer logo-header.svg → logo-header.png →
|
|
67
|
+
* logo.svg → logo.png → favicon.svg → favicon.png → public fallback.
|
|
68
|
+
* logo-header.* lets a theme show a different copy in the header than the
|
|
69
|
+
* logo.* file that pages may link to.
|
|
70
|
+
*/
|
|
52
71
|
private resolveLogoPath;
|
|
53
72
|
get paths(): ThemePaths;
|
|
54
73
|
/** List all available themes (subdirs of themes/ that have a theme.json) */
|
|
@@ -21,6 +21,15 @@ declare class FileLoggingProvider extends BaseLoggingProvider {
|
|
|
21
21
|
*/
|
|
22
22
|
createTransports(config: LoggingProviderConfig): Transport[];
|
|
23
23
|
createFormat(): Logform.Format;
|
|
24
|
+
/** CR and LF as the visible two-character sequences `\r` and `\n`, so a value cannot start a line of its own (#2294). */
|
|
25
|
+
private static oneLine;
|
|
26
|
+
/**
|
|
27
|
+
* A stack keeps one frame per line, but its message portion (`Name: message`, which can span lines and can
|
|
28
|
+
* hold a caller-sent value) must not start a log line of its own (#2294). Only a line that begins with
|
|
29
|
+
* optional whitespace and `at ` starts a new output line; every other line is joined to the one before it
|
|
30
|
+
* with a visible `\n`.
|
|
31
|
+
*/
|
|
32
|
+
private static stackLines;
|
|
24
33
|
/**
|
|
25
34
|
* Render whatever a caller passed as the second argument to `logger.*`.
|
|
26
35
|
*
|
|
@@ -30,7 +39,9 @@ declare class FileLoggingProvider extends BaseLoggingProvider {
|
|
|
30
39
|
* line would be noise at best.
|
|
31
40
|
*
|
|
32
41
|
* A `stack` is emitted on its own lines because a one-line stack is
|
|
33
|
-
* unreadable, and reading the stack is the entire reason this exists.
|
|
42
|
+
* unreadable, and reading the stack is the entire reason this exists. Its
|
|
43
|
+
* frames keep their own lines; CR and LF in its message portion are escaped,
|
|
44
|
+
* as are those in the other keys and their string values.
|
|
34
45
|
*
|
|
35
46
|
* @param info - The winston info object for one log record
|
|
36
47
|
* @returns The rendered metadata, or an empty string when there is none
|
|
@@ -45,7 +45,17 @@ class FileLoggingProvider extends BaseLoggingProvider {
|
|
|
45
45
|
// #1461: private page titles never reach a log line or /admin/logs.
|
|
46
46
|
redactPrivateNamesFormat(), format.printf((info) => {
|
|
47
47
|
const ts = typeof info.timestamp === 'string' ? info.timestamp : JSON.stringify(info.timestamp);
|
|
48
|
-
|
|
48
|
+
// #2294: a caller-sent value (a page name) in the message could carry a CR or LF and start a log line of
|
|
49
|
+
// its own. Escaped here, once, for every transport and every call site; the escape is visible, not removed.
|
|
50
|
+
// Never throws: a logger that throws breaks its caller. Any non-string message (undefined, symbol, function,
|
|
51
|
+
// a circular object) is coerced with String(), and a formatting error falls back to a fixed marker.
|
|
52
|
+
let msg;
|
|
53
|
+
try {
|
|
54
|
+
msg = FileLoggingProvider.oneLine(typeof info.message === 'string' ? info.message : String(info.message));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
msg = '[unprintable log message]';
|
|
58
|
+
}
|
|
49
59
|
// #1141: render the metadata too. This printf used to emit only
|
|
50
60
|
// timestamp/level/message, so the second argument of every
|
|
51
61
|
// `logger.error(msg, { error, stack })` call in the codebase was
|
|
@@ -57,6 +67,27 @@ class FileLoggingProvider extends BaseLoggingProvider {
|
|
|
57
67
|
return meta ? `${ts} [${info.level}]: ${msg} ${meta}` : `${ts} [${info.level}]: ${msg}`;
|
|
58
68
|
}));
|
|
59
69
|
}
|
|
70
|
+
/** CR and LF as the visible two-character sequences `\r` and `\n`, so a value cannot start a line of its own (#2294). */
|
|
71
|
+
static oneLine(text) {
|
|
72
|
+
return text.replace(/[\r\n]/g, (c) => (c === '\r' ? '\\r' : '\\n'));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A stack keeps one frame per line, but its message portion (`Name: message`, which can span lines and can
|
|
76
|
+
* hold a caller-sent value) must not start a log line of its own (#2294). Only a line that begins with
|
|
77
|
+
* optional whitespace and `at ` starts a new output line; every other line is joined to the one before it
|
|
78
|
+
* with a visible `\n`.
|
|
79
|
+
*/
|
|
80
|
+
static stackLines(stack) {
|
|
81
|
+
const out = [];
|
|
82
|
+
stack.split('\n').forEach((raw, i) => {
|
|
83
|
+
const line = FileLoggingProvider.oneLine(raw);
|
|
84
|
+
if (i === 0 || /^\s*at /.test(raw))
|
|
85
|
+
out.push(line);
|
|
86
|
+
else
|
|
87
|
+
out[out.length - 1] += `\\n${line}`;
|
|
88
|
+
});
|
|
89
|
+
return out.join('\n');
|
|
90
|
+
}
|
|
60
91
|
/**
|
|
61
92
|
* Render whatever a caller passed as the second argument to `logger.*`.
|
|
62
93
|
*
|
|
@@ -66,7 +97,9 @@ class FileLoggingProvider extends BaseLoggingProvider {
|
|
|
66
97
|
* line would be noise at best.
|
|
67
98
|
*
|
|
68
99
|
* A `stack` is emitted on its own lines because a one-line stack is
|
|
69
|
-
* unreadable, and reading the stack is the entire reason this exists.
|
|
100
|
+
* unreadable, and reading the stack is the entire reason this exists. Its
|
|
101
|
+
* frames keep their own lines; CR and LF in its message portion are escaped,
|
|
102
|
+
* as are those in the other keys and their string values.
|
|
70
103
|
*
|
|
71
104
|
* @param info - The winston info object for one log record
|
|
72
105
|
* @returns The rendered metadata, or an empty string when there is none
|
|
@@ -82,10 +115,10 @@ class FileLoggingProvider extends BaseLoggingProvider {
|
|
|
82
115
|
if (value === undefined)
|
|
83
116
|
continue;
|
|
84
117
|
if (key === 'stack' && typeof value === 'string') {
|
|
85
|
-
stack = value;
|
|
118
|
+
stack = FileLoggingProvider.stackLines(value);
|
|
86
119
|
continue;
|
|
87
120
|
}
|
|
88
|
-
parts.push(`${key}=${typeof value === 'string' ? value : JSON.stringify(value)}`);
|
|
121
|
+
parts.push(`${FileLoggingProvider.oneLine(key)}=${typeof value === 'string' ? FileLoggingProvider.oneLine(value) : JSON.stringify(value)}`);
|
|
89
122
|
}
|
|
90
123
|
const inline = parts.length > 0 ? parts.join(' ') : '';
|
|
91
124
|
if (stack) {
|
|
@@ -28,6 +28,7 @@ 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
30
|
import { type ActorContext } from '../context/ActorContext.js';
|
|
31
|
+
import { type OwnedListAsk, type OwnedListDecision, type OwnedRecordAsk } from './ownedRecords.js';
|
|
31
32
|
export declare const POLICIES_KEY = "ngdpbase.access.policies";
|
|
32
33
|
export declare const POLICIES_ENABLED_KEY = "ngdpbase.access.policies.enabled";
|
|
33
34
|
/** Whether an administrator may enter another person's unencrypted vault (#2255). */
|
|
@@ -50,7 +51,8 @@ export declare class PolicyDecisionPoint extends BaseManager {
|
|
|
50
51
|
/** Warn, once per policy and name, that a policy names a retired permission and what it is read as (#2038). */
|
|
51
52
|
private warnRetiredOnce;
|
|
52
53
|
/**
|
|
53
|
-
* The policies in force right now,
|
|
54
|
+
* The policies in force right now, in configuration order. Order carries no
|
|
55
|
+
* meaning: the rule-combining algorithm is `deny-overrides` (#2330).
|
|
54
56
|
*
|
|
55
57
|
* - `ngdpbase.access.policies.enabled` false — its default when unset —
|
|
56
58
|
* means none;
|
|
@@ -73,8 +75,8 @@ export declare class PolicyDecisionPoint extends BaseManager {
|
|
|
73
75
|
* What each role permits: role name → the actions its holders are granted.
|
|
74
76
|
*
|
|
75
77
|
* It mirrors the evaluator's ROLE matching — an allow policy naming a role
|
|
76
|
-
* grants its actions, a deny policy naming it takes them away,
|
|
77
|
-
*
|
|
78
|
+
* grants its actions, a deny policy naming it takes them away, whatever the
|
|
79
|
+
* order (deny-overrides, #2330). It does not
|
|
78
80
|
* evaluate resource patterns or the ceilings, so it is a summary for a human
|
|
79
81
|
* reading a table (the admin Security Policy Summary, a profile), never an
|
|
80
82
|
* authorisation answer — that is {@link permits} with the caller's subject.
|
|
@@ -332,6 +334,51 @@ export declare class PolicyDecisionPoint extends BaseManager {
|
|
|
332
334
|
* with `pushed-pages-edit` to leave pushed pages out. A summary of the
|
|
333
335
|
* policies, never an answer to "may this subject".
|
|
334
336
|
*/
|
|
337
|
+
/**
|
|
338
|
+
* May this subject act on one stored add-on record (#1823)?
|
|
339
|
+
*
|
|
340
|
+
* The manager passes attributes it read from the row. `owner` is the
|
|
341
|
+
* identity @id. A type that declares `owner` is allowed only when that @id
|
|
342
|
+
* is the subject's (the person a proxy grant represents, not the actor),
|
|
343
|
+
* and policy also allows the action. A role does not bypass the owner. A
|
|
344
|
+
* type with no `owner` is policy alone. A core type (`page`, `vault`, …)
|
|
345
|
+
* is not decided here. Missing data throws {@link OwnedRecordError}; the
|
|
346
|
+
* caller fails closed. This records nothing.
|
|
347
|
+
*/
|
|
348
|
+
decideOwnedRecord(input: DecisionInput, request: OwnedRecordAsk): Promise<Decision>;
|
|
349
|
+
/**
|
|
350
|
+
* May this subject list a type, and which rows (#1823)?
|
|
351
|
+
*
|
|
352
|
+
* An allow for a type that declares `owner` carries one filter, `owner`
|
|
353
|
+
* equals the subject's identity @id. The manager binds that value; it is
|
|
354
|
+
* not SQL. An allow for a type with no owner carries an empty filter: policy
|
|
355
|
+
* alone, no row constraint. A refusal is `{ decision: 'deny' }`, never an
|
|
356
|
+
* empty filter and never an empty row set from this method. Missing data
|
|
357
|
+
* throws {@link OwnedRecordError}. This records nothing: the list is logged
|
|
358
|
+
* once by the manager that applies it.
|
|
359
|
+
*/
|
|
360
|
+
decideOwnedList(input: DecisionInput, request: OwnedListAsk): Promise<OwnedListDecision>;
|
|
361
|
+
/**
|
|
362
|
+
* The ceilings, the declared type, and a reader for conditions. A ceiling
|
|
363
|
+
* denial is the whole answer. Anything that cannot be decided throws.
|
|
364
|
+
*/
|
|
365
|
+
private prepareOwned;
|
|
366
|
+
private ownedTypeEntry;
|
|
367
|
+
private typeDeclaresOwner;
|
|
368
|
+
/** The owner the manager read. Missing on an owner-typed record cannot be decided. */
|
|
369
|
+
private storedOwner;
|
|
370
|
+
private ownerMismatch;
|
|
371
|
+
/** The subject's identity @id, or a denial when nobody is signed in. A signed-in subject with no identity cannot be decided. */
|
|
372
|
+
private subjectOwnerId;
|
|
373
|
+
private ownedUserContext;
|
|
374
|
+
/**
|
|
375
|
+
* Condition values (#1823). A `resource:` attribute is the value the manager
|
|
376
|
+
* read from the stored row, never a request field. Subject and environment
|
|
377
|
+
* attributes come from the PIP.
|
|
378
|
+
*/
|
|
379
|
+
private ownedReader;
|
|
380
|
+
private ownedTypeResult;
|
|
381
|
+
private decisionFrom;
|
|
335
382
|
categoriesGranting(action: string): string[];
|
|
336
383
|
}
|
|
337
384
|
export default PolicyDecisionPoint;
|
|
@@ -138,6 +138,7 @@ export interface PageAttributes {
|
|
|
138
138
|
* if (await pip.checkPagePermissionWithContext(wikiContext, 'edit')) { ... }
|
|
139
139
|
*/
|
|
140
140
|
declare class PolicyInformationPoint extends BaseManager {
|
|
141
|
+
private readonly clock;
|
|
141
142
|
/** Read at the moment of use: this manager starts before PolicyEvaluator (#1431 step 13). */
|
|
142
143
|
private get policyEvaluator();
|
|
143
144
|
/**
|
|
@@ -145,8 +146,9 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
145
146
|
*
|
|
146
147
|
* @constructor
|
|
147
148
|
* @param {WikiEngine} engine - The wiki engine instance
|
|
149
|
+
* @param {() => Date} [clock] - The time `environment:` attributes are read at; injectable for tests
|
|
148
150
|
*/
|
|
149
|
-
constructor(engine: WikiEngine);
|
|
151
|
+
constructor(engine: WikiEngine, clock?: () => Date);
|
|
150
152
|
/**
|
|
151
153
|
* Initializes the PolicyInformationPoint by loading policies and configurations
|
|
152
154
|
*
|
|
@@ -543,6 +545,23 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
543
545
|
* evaluator awaits every read).
|
|
544
546
|
*/
|
|
545
547
|
subjectAttribute(subject: SubjectFacts | null | undefined, name: string): unknown;
|
|
548
|
+
/**
|
|
549
|
+
* An environment attribute a condition reads (#2261): `environment:<name>`,
|
|
550
|
+
* declared in `ngdpbase.access.environment-attributes` and read from its
|
|
551
|
+
* `from` source here, the only place one is read. `schedule:<name>` is whether
|
|
552
|
+
* that schedule in `ngdpbase.schedules` is open at `now`; `config:<key>` is
|
|
553
|
+
* the boolean stored at that configuration key, any key, secrets included.
|
|
554
|
+
* Missing when it is not declared, the schedule is absent, the stored value is
|
|
555
|
+
* not a boolean, or the read fails; the evaluator then fails closed. The value
|
|
556
|
+
* is returned to the evaluator and goes nowhere else: it is never logged.
|
|
557
|
+
*/
|
|
558
|
+
environmentAttribute(name: string, now?: Date): boolean | undefined;
|
|
559
|
+
/**
|
|
560
|
+
* A reader of environment attributes for one decision or one listing
|
|
561
|
+
* (#2261): the clock is read once, on the first attribute asked for, and
|
|
562
|
+
* each attribute once, so every policy and every page sees one instant.
|
|
563
|
+
*/
|
|
564
|
+
environmentReader(): (name: string) => unknown;
|
|
546
565
|
/** One read of a declared subject attribute from its source. A value of the wrong type, or a source that throws, is missing. */
|
|
547
566
|
private readSubjectSource;
|
|
548
567
|
/** The page's category for the policies (#2255): lowercased; the default when it has none; `shared` in a shared folder. */
|
|
@@ -581,6 +600,13 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
581
600
|
* nothing. Same record as every other page denial, `policyIds` included.
|
|
582
601
|
*/
|
|
583
602
|
recordPageRefusal(subject: UserContext | null | undefined, pageName: string, action: string, reason: string, policyIds?: readonly string[], conditionNotMet?: readonly string[]): Promise<void>;
|
|
603
|
+
/**
|
|
604
|
+
* Record a write the signed-out write gate refused before any route ran
|
|
605
|
+
* (#2298). The resource is the route as asked (`POST /save/Home`): method
|
|
606
|
+
* and path, never the query or the body, which can carry a password or a
|
|
607
|
+
* token. Standard tier and best-effort, like every other denial.
|
|
608
|
+
*/
|
|
609
|
+
recordRouteRefusal(subject: UserContext | null | undefined, route: string): Promise<void>;
|
|
584
610
|
/** The name a decision is recorded under: the subject's, or `anonymous`. */
|
|
585
611
|
private static recordedUser;
|
|
586
612
|
/**
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add-on record decisions (#1823).
|
|
3
|
+
*
|
|
4
|
+
* The add-on's manager is the enforcement point. It reads `owner` from the
|
|
5
|
+
* stored row and asks {@link PolicyDecisionPoint.decideOwnedRecord} or
|
|
6
|
+
* {@link PolicyDecisionPoint.decideOwnedList}. The answer is never null.
|
|
7
|
+
* A type that declares `owner` (an identity @id) is refused to anyone but
|
|
8
|
+
* that owner: under a proxy grant the subject is the person represented, so
|
|
9
|
+
* the delegate is allowed and a role is not. A type with no `owner` is
|
|
10
|
+
* decided by policy alone. A list that is refused is a denial, not an empty
|
|
11
|
+
* page of rows.
|
|
12
|
+
*/
|
|
13
|
+
/** The PDP cannot decide. The caller fails closed; this is not a deny. */
|
|
14
|
+
export declare class OwnedRecordError extends Error {
|
|
15
|
+
constructor(message: string);
|
|
16
|
+
}
|
|
17
|
+
/** One record, with the attributes the manager read from its stored row. */
|
|
18
|
+
export interface OwnedRecordAsk {
|
|
19
|
+
action: string;
|
|
20
|
+
resource: {
|
|
21
|
+
type: string;
|
|
22
|
+
id: string;
|
|
23
|
+
/** From the stored row. `owner`, when the type declares it, is the identity @id. */
|
|
24
|
+
attributes?: Record<string, unknown>;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** A list of one type. There is no owner argument: the filter comes from the subject. */
|
|
28
|
+
export interface OwnedListAsk {
|
|
29
|
+
action: string;
|
|
30
|
+
resource: {
|
|
31
|
+
type: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A constraint the manager turns into a bound parameter. Never SQL.
|
|
36
|
+
* `equals` means the column must equal the one value.
|
|
37
|
+
*/
|
|
38
|
+
export interface OwnedRecordFilter {
|
|
39
|
+
attribute: string;
|
|
40
|
+
operator: 'equals';
|
|
41
|
+
values: readonly string[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The resource a refusal is audited against (#1823). `owner` is the identity
|
|
45
|
+
* @id from the stored row, `ownerUser` that person's username. Both come from
|
|
46
|
+
* storage, never from the request body.
|
|
47
|
+
*/
|
|
48
|
+
export interface OwnedAuditResource {
|
|
49
|
+
type: string;
|
|
50
|
+
id?: string;
|
|
51
|
+
owner?: string;
|
|
52
|
+
ownerUser?: string;
|
|
53
|
+
}
|
|
54
|
+
/** An explicit list answer (#1823). `filter` is empty only when the type has no owner. */
|
|
55
|
+
export type OwnedListDecision = {
|
|
56
|
+
decision: 'allow';
|
|
57
|
+
filter: readonly OwnedRecordFilter[];
|
|
58
|
+
} | {
|
|
59
|
+
decision: 'deny';
|
|
60
|
+
reason: string;
|
|
61
|
+
policyIds: readonly string[];
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* What is wrong with a resource type's `owner` declaration, or null (#1823).
|
|
65
|
+
* `owner`, when present at all, must be exactly `{ "type": "identity" }`.
|
|
66
|
+
* Anything else, `null` included, would quietly make an owner type
|
|
67
|
+
* policy-alone, so the decision throws and start-up refuses the config.
|
|
68
|
+
*/
|
|
69
|
+
export declare function ownerDeclarationProblem(type: string, entry: unknown): string | null;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The signed-out write list (#2298): the one place that names which routes may
|
|
3
|
+
* be written to by someone who is not signed in.
|
|
4
|
+
*
|
|
5
|
+
* Every request from a caller who is not signed in, other than GET, HEAD and
|
|
6
|
+
* OPTIONS, is refused by `signedOutWriteGate` (src/middleware/signedOutWriteGate.ts),
|
|
7
|
+
* before any route runs, unless the route is on this list. A new write route
|
|
8
|
+
* is therefore closed to signed-out callers until someone adds it here with a
|
|
9
|
+
* reason, which is the point (#2294 came from routes that were open until
|
|
10
|
+
* their author remembered to close them).
|
|
11
|
+
*
|
|
12
|
+
* What belongs here: a route a person reaches BEFORE they are signed in (the
|
|
13
|
+
* steps of signing in, registering, recovering a password, accepting an
|
|
14
|
+
* invitation), a link followed from an email in any browser, and a public form.
|
|
15
|
+
* What does not: anything that asks for a permission a signed-out caller
|
|
16
|
+
* never holds. A route that answers its own token (a Bearer token, which the
|
|
17
|
+
* gate counts as signed in) needs no entry.
|
|
18
|
+
*
|
|
19
|
+
* Core's routes are listed below. An add-on lists its own through the add-on
|
|
20
|
+
* host, `host.allowSignedOutWrite(...)`, which accepts only a route under the
|
|
21
|
+
* add-on's own `/api/<slug>/` or `/addons/<slug>/`, and refuses a slug that
|
|
22
|
+
* names one of core's own `/api/` or `/addons/` prefixes. Those are the only
|
|
23
|
+
* ways onto the list, and it is sealed once the add-ons have loaded; outside
|
|
24
|
+
* tests (`NODE_ENV=test`) the seal cannot be undone.
|
|
25
|
+
*
|
|
26
|
+
* Permission to write is still asked by each route, as always: an entry only
|
|
27
|
+
* lets the request reach the route, it grants nothing.
|
|
28
|
+
*/
|
|
29
|
+
export type WriteMethod = 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
30
|
+
export interface SignedOutWriteRoute {
|
|
31
|
+
method: WriteMethod;
|
|
32
|
+
/** An Express path: literal segments and `:name` parameters only. */
|
|
33
|
+
path: string;
|
|
34
|
+
/** Why a caller who is not signed in must reach it. Required. */
|
|
35
|
+
reason: string;
|
|
36
|
+
}
|
|
37
|
+
/** Whether the method may change things, and so is held by the gate: anything but GET, HEAD and OPTIONS. */
|
|
38
|
+
export declare function isWriteMethod(method: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Slugs an add-on may not use to list a route, because core already answers
|
|
41
|
+
* under `/api/<slug>/` or `/addons/<slug>/`: an add-on in a directory named
|
|
42
|
+
* `admin` must not open `/api/admin/...`. Kept in step with core's route
|
|
43
|
+
* files; `signedOutWriteGate.test.ts` checks it against the route inventory.
|
|
44
|
+
*/
|
|
45
|
+
export declare const RESERVED_ADDON_SLUGS: ReadonlySet<string>;
|
|
46
|
+
/**
|
|
47
|
+
* Core's list. Each reason says why the route cannot wait for a sign-in.
|
|
48
|
+
* Routes that need a signed-in session by their nature (consent, the
|
|
49
|
+
* account-setup password, the second-factor enrolment, reauthentication) are
|
|
50
|
+
* not here: their callers are signed in when they reach them.
|
|
51
|
+
*/
|
|
52
|
+
export declare const CORE_SIGNED_OUT_WRITES: readonly SignedOutWriteRoute[];
|
|
53
|
+
/**
|
|
54
|
+
* An add-on's declaration of a route a signed-out caller may write to.
|
|
55
|
+
* Called through `AddonHost.allowSignedOutWrite`, bound to the add-on's slug.
|
|
56
|
+
* Throws on anything outside the add-on's own paths, so one add-on can never
|
|
57
|
+
* open another's, or core's.
|
|
58
|
+
*/
|
|
59
|
+
export declare function declareAddonSignedOutWrite(slug: string, route: SignedOutWriteRoute): void;
|
|
60
|
+
/** Close the list to add-ons. Called once, after they have loaded. */
|
|
61
|
+
export declare function sealSignedOutWrites(): void;
|
|
62
|
+
/** Test support: forget add-on declarations and reopen the list. Refuses outside `NODE_ENV=test`, so a sealed list stays sealed. */
|
|
63
|
+
export declare function resetSignedOutWrites(): void;
|
|
64
|
+
/** Every route on the list, core's then the add-ons'. */
|
|
65
|
+
export declare function signedOutWriteRoutes(): Array<SignedOutWriteRoute & {
|
|
66
|
+
owner: string;
|
|
67
|
+
}>;
|
|
68
|
+
/** The reason the route is on the list, or null when a signed-out caller may not write to it. */
|
|
69
|
+
export declare function signedOutWriteReason(method: string, path: string): string | null;
|