@naylence/runtime 0.3.21 → 0.4.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/dist/browser/index.cjs +3144 -1307
- package/dist/browser/index.mjs +3116 -1301
- package/dist/cjs/naylence/fame/factory-manifest.js +6 -0
- package/dist/cjs/naylence/fame/node/node-event-listener.js +4 -0
- package/dist/cjs/naylence/fame/security/auth/default-policy-authorizer-factory.js +147 -0
- package/dist/cjs/naylence/fame/security/auth/default-policy-authorizer.js +291 -0
- package/dist/cjs/naylence/fame/security/auth/oauth2-authorizer-factory.js +7 -0
- package/dist/cjs/naylence/fame/security/auth/oauth2-authorizer.js +19 -4
- package/dist/cjs/naylence/fame/security/auth/policy/authorization-policy-definition.js +60 -0
- package/dist/cjs/naylence/fame/security/auth/policy/authorization-policy-factory.js +35 -0
- package/dist/cjs/naylence/fame/security/auth/policy/authorization-policy-source-factory.js +35 -0
- package/dist/cjs/naylence/fame/security/auth/policy/authorization-policy-source.js +2 -0
- package/dist/cjs/naylence/fame/security/auth/policy/authorization-policy.js +2 -0
- package/dist/cjs/naylence/fame/security/auth/policy/basic-authorization-policy-factory.js +99 -0
- package/dist/cjs/naylence/fame/security/auth/policy/basic-authorization-policy.js +449 -0
- package/dist/cjs/naylence/fame/security/auth/policy/index.js +40 -0
- package/dist/cjs/naylence/fame/security/auth/policy/local-file-authorization-policy-source-factory.js +101 -0
- package/dist/cjs/naylence/fame/security/auth/policy/local-file-authorization-policy-source.js +164 -0
- package/dist/cjs/naylence/fame/security/auth/policy/pattern-matcher.js +195 -0
- package/dist/cjs/naylence/fame/security/auth/policy/scope-matcher.js +169 -0
- package/dist/cjs/naylence/fame/security/auth/policy-authorizer.js +2 -0
- package/dist/cjs/naylence/fame/security/default-security-manager.js +94 -0
- package/dist/cjs/naylence/fame/security/index.js +3 -0
- package/dist/cjs/naylence/fame/security/node-security-profile-factory.js +3 -1
- package/dist/cjs/naylence/fame/sentinel/router.js +67 -1
- package/dist/cjs/naylence/fame/sentinel/sentinel.js +46 -2
- package/dist/cjs/naylence/fame/util/register-runtime-factories.js +2 -0
- package/dist/cjs/version.js +2 -2
- package/dist/esm/naylence/fame/factory-manifest.js +6 -0
- package/dist/esm/naylence/fame/node/node-event-listener.js +4 -0
- package/dist/esm/naylence/fame/security/auth/default-policy-authorizer-factory.js +110 -0
- package/dist/esm/naylence/fame/security/auth/default-policy-authorizer.js +287 -0
- package/dist/esm/naylence/fame/security/auth/oauth2-authorizer-factory.js +7 -0
- package/dist/esm/naylence/fame/security/auth/oauth2-authorizer.js +19 -4
- package/dist/esm/naylence/fame/security/auth/policy/authorization-policy-definition.js +57 -0
- package/dist/esm/naylence/fame/security/auth/policy/authorization-policy-factory.js +31 -0
- package/dist/esm/naylence/fame/security/auth/policy/authorization-policy-source-factory.js +31 -0
- package/dist/esm/naylence/fame/security/auth/policy/authorization-policy-source.js +1 -0
- package/dist/esm/naylence/fame/security/auth/policy/authorization-policy.js +1 -0
- package/dist/esm/naylence/fame/security/auth/policy/basic-authorization-policy-factory.js +62 -0
- package/dist/esm/naylence/fame/security/auth/policy/basic-authorization-policy.js +445 -0
- package/dist/esm/naylence/fame/security/auth/policy/index.js +20 -0
- package/dist/esm/naylence/fame/security/auth/policy/local-file-authorization-policy-source-factory.js +64 -0
- package/dist/esm/naylence/fame/security/auth/policy/local-file-authorization-policy-source.js +127 -0
- package/dist/esm/naylence/fame/security/auth/policy/pattern-matcher.js +185 -0
- package/dist/esm/naylence/fame/security/auth/policy/scope-matcher.js +162 -0
- package/dist/esm/naylence/fame/security/auth/policy-authorizer.js +1 -0
- package/dist/esm/naylence/fame/security/default-security-manager.js +94 -0
- package/dist/esm/naylence/fame/security/index.js +3 -0
- package/dist/esm/naylence/fame/security/node-security-profile-factory.js +2 -0
- package/dist/esm/naylence/fame/sentinel/router.js +64 -0
- package/dist/esm/naylence/fame/sentinel/sentinel.js +47 -3
- package/dist/esm/naylence/fame/util/register-runtime-factories.js +2 -0
- package/dist/esm/version.js +2 -2
- package/dist/node/index.cjs +3140 -1303
- package/dist/node/index.mjs +3116 -1301
- package/dist/node/node.cjs +3191 -1338
- package/dist/node/node.mjs +3167 -1336
- package/dist/types/naylence/fame/factory-manifest.d.ts +1 -1
- package/dist/types/naylence/fame/node/node-event-listener.d.ts +31 -0
- package/dist/types/naylence/fame/security/auth/authorizer.d.ts +37 -0
- package/dist/types/naylence/fame/security/auth/default-policy-authorizer-factory.d.ts +55 -0
- package/dist/types/naylence/fame/security/auth/default-policy-authorizer.d.ts +99 -0
- package/dist/types/naylence/fame/security/auth/oauth2-authorizer-factory.d.ts +2 -0
- package/dist/types/naylence/fame/security/auth/oauth2-authorizer.d.ts +2 -0
- package/dist/types/naylence/fame/security/auth/policy/authorization-policy-definition.d.ts +166 -0
- package/dist/types/naylence/fame/security/auth/policy/authorization-policy-factory.d.ts +38 -0
- package/dist/types/naylence/fame/security/auth/policy/authorization-policy-source-factory.d.ts +38 -0
- package/dist/types/naylence/fame/security/auth/policy/authorization-policy-source.d.ts +20 -0
- package/dist/types/naylence/fame/security/auth/policy/authorization-policy.d.ts +55 -0
- package/dist/types/naylence/fame/security/auth/policy/basic-authorization-policy-factory.d.ts +42 -0
- package/dist/types/naylence/fame/security/auth/policy/basic-authorization-policy.d.ts +78 -0
- package/dist/types/naylence/fame/security/auth/policy/index.d.ts +19 -0
- package/dist/types/naylence/fame/security/auth/policy/local-file-authorization-policy-source-factory.d.ts +51 -0
- package/dist/types/naylence/fame/security/auth/policy/local-file-authorization-policy-source.d.ts +67 -0
- package/dist/types/naylence/fame/security/auth/policy/pattern-matcher.d.ts +84 -0
- package/dist/types/naylence/fame/security/auth/policy/scope-matcher.d.ts +61 -0
- package/dist/types/naylence/fame/security/auth/policy-authorizer.d.ts +12 -0
- package/dist/types/naylence/fame/security/default-security-manager.d.ts +22 -0
- package/dist/types/naylence/fame/security/index.d.ts +2 -0
- package/dist/types/naylence/fame/security/node-security-profile-factory.d.ts +1 -0
- package/dist/types/naylence/fame/sentinel/router.d.ts +68 -0
- package/dist/types/naylence/fame/sentinel/sentinel.d.ts +16 -0
- package/dist/types/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Basic authorization policy implementation.
|
|
3
|
+
*
|
|
4
|
+
* Evaluates authorization rules defined in YAML/JSON policy files.
|
|
5
|
+
* Uses first-match-wins semantics with glob/regex pattern matching.
|
|
6
|
+
*/
|
|
7
|
+
import { getLogger } from '../../../util/logging.js';
|
|
8
|
+
import { KNOWN_POLICY_FIELDS, KNOWN_RULE_FIELDS, VALID_ACTIONS, VALID_EFFECTS, VALID_ORIGIN_TYPES, } from './authorization-policy-definition.js';
|
|
9
|
+
import { compileGlobPattern } from './pattern-matcher.js';
|
|
10
|
+
import { compileGlobOnlyScopeRequirement } from './scope-matcher.js';
|
|
11
|
+
const logger = getLogger('naylence.fame.security.auth.policy.basic_authorization_policy');
|
|
12
|
+
/**
|
|
13
|
+
* Extracts the target address string from the envelope.
|
|
14
|
+
*/
|
|
15
|
+
function extractAddress(envelope) {
|
|
16
|
+
const to = envelope.to;
|
|
17
|
+
if (!to) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
// FameAddress can be a string or object with toString()
|
|
21
|
+
if (typeof to === 'string') {
|
|
22
|
+
return to;
|
|
23
|
+
}
|
|
24
|
+
if (typeof to === 'object' && 'toString' in to) {
|
|
25
|
+
return to.toString();
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Extracts granted scopes from the authorization context.
|
|
31
|
+
*/
|
|
32
|
+
function extractGrantedScopes(context) {
|
|
33
|
+
const authContext = context?.security?.authorization;
|
|
34
|
+
if (!authContext) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
// Check grantedScopes first
|
|
38
|
+
if (Array.isArray(authContext.grantedScopes)) {
|
|
39
|
+
return authContext.grantedScopes;
|
|
40
|
+
}
|
|
41
|
+
// Fall back to claims.scope if available
|
|
42
|
+
const claims = authContext.claims;
|
|
43
|
+
if (claims) {
|
|
44
|
+
const scopeClaim = claims.scope ?? claims.scopes ?? claims.scp;
|
|
45
|
+
if (typeof scopeClaim === 'string') {
|
|
46
|
+
// Space-separated scopes (OAuth2 convention)
|
|
47
|
+
return scopeClaim.split(/\s+/).filter((s) => s.length > 0);
|
|
48
|
+
}
|
|
49
|
+
if (Array.isArray(scopeClaim)) {
|
|
50
|
+
return scopeClaim.filter((s) => typeof s === 'string');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Basic authorization policy that evaluates rules from a policy definition.
|
|
57
|
+
*
|
|
58
|
+
* Features:
|
|
59
|
+
* - First-match-wins rule evaluation
|
|
60
|
+
* - Glob and regex pattern matching for addresses
|
|
61
|
+
* - Scope matching with any_of/all_of/none_of operators
|
|
62
|
+
* - Action-based filtering (connect, send, receive)
|
|
63
|
+
*/
|
|
64
|
+
export class BasicAuthorizationPolicy {
|
|
65
|
+
constructor(options) {
|
|
66
|
+
const { policyDefinition, warnOnUnknownFields = true } = options;
|
|
67
|
+
// Validate and extract default effect
|
|
68
|
+
this.defaultEffect = this.validateDefaultEffect(policyDefinition.default_effect);
|
|
69
|
+
// Warn about unknown policy fields
|
|
70
|
+
if (warnOnUnknownFields) {
|
|
71
|
+
this.warnUnknownPolicyFields(policyDefinition);
|
|
72
|
+
}
|
|
73
|
+
// Compile rules for efficient evaluation
|
|
74
|
+
this.compiledRules = this.compileRules(policyDefinition.rules, warnOnUnknownFields);
|
|
75
|
+
logger.debug('policy_compiled', {
|
|
76
|
+
defaultEffect: this.defaultEffect,
|
|
77
|
+
ruleCount: this.compiledRules.length,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Evaluates the policy against a request with an explicitly provided action.
|
|
82
|
+
*
|
|
83
|
+
* @param _node - The node handling the request (unused in basic policy)
|
|
84
|
+
* @param envelope - The FAME envelope being authorized
|
|
85
|
+
* @param context - Optional delivery context with authorization info
|
|
86
|
+
* @param action - The authorization action token (required, no inference)
|
|
87
|
+
* @returns Authorization decision indicating allow/deny
|
|
88
|
+
*/
|
|
89
|
+
async evaluateRequest(_node, envelope, context, action) {
|
|
90
|
+
// Action must be explicitly provided; default to wildcard if omitted
|
|
91
|
+
// for backward compatibility during transition
|
|
92
|
+
const resolvedAction = action ?? '*';
|
|
93
|
+
const address = extractAddress(envelope);
|
|
94
|
+
const grantedScopes = extractGrantedScopes(context);
|
|
95
|
+
const rawFrameType = envelope.frame
|
|
96
|
+
?.type;
|
|
97
|
+
const frameTypeNormalized = typeof rawFrameType === 'string' && rawFrameType.trim().length > 0
|
|
98
|
+
? rawFrameType.trim().toLowerCase()
|
|
99
|
+
: '';
|
|
100
|
+
// Extract and normalize origin type for rule matching
|
|
101
|
+
const rawOriginType = context?.originType;
|
|
102
|
+
const originTypeNormalized = typeof rawOriginType === 'string' && rawOriginType.trim().length > 0
|
|
103
|
+
? rawOriginType.trim().toLowerCase()
|
|
104
|
+
: undefined;
|
|
105
|
+
const evaluationTrace = [];
|
|
106
|
+
// Evaluate rules in order (first match wins)
|
|
107
|
+
for (const rule of this.compiledRules) {
|
|
108
|
+
const step = {
|
|
109
|
+
ruleId: rule.id,
|
|
110
|
+
result: false,
|
|
111
|
+
};
|
|
112
|
+
// Skip rules with 'when' clause (handled by advanced policy)
|
|
113
|
+
if (rule.hasWhenClause) {
|
|
114
|
+
step.expression = 'when clause (skipped by basic policy)';
|
|
115
|
+
step.result = false;
|
|
116
|
+
evaluationTrace.push(step);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// Check frame type match
|
|
120
|
+
if (rule.frameTypes) {
|
|
121
|
+
if (!frameTypeNormalized) {
|
|
122
|
+
step.expression = 'frame_type: missing';
|
|
123
|
+
step.result = false;
|
|
124
|
+
evaluationTrace.push(step);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (!rule.frameTypes.has(frameTypeNormalized)) {
|
|
128
|
+
step.expression = `frame_type: ${rawFrameType ?? 'unknown'} not in rule set`;
|
|
129
|
+
step.result = false;
|
|
130
|
+
evaluationTrace.push(step);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Check origin type match (early gate for efficiency)
|
|
135
|
+
if (rule.originTypes) {
|
|
136
|
+
if (originTypeNormalized === undefined) {
|
|
137
|
+
step.expression = 'origin_type: missing (rule requires origin)';
|
|
138
|
+
step.result = false;
|
|
139
|
+
evaluationTrace.push(step);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (!rule.originTypes.has(originTypeNormalized)) {
|
|
143
|
+
step.expression = `origin_type: ${rawOriginType ?? 'unknown'} not in [${Array.from(rule.originTypes).join(', ')}]`;
|
|
144
|
+
step.result = false;
|
|
145
|
+
evaluationTrace.push(step);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Check action match
|
|
150
|
+
if (!rule.actions.has('*') && !rule.actions.has(resolvedAction)) {
|
|
151
|
+
step.expression = `action: ${resolvedAction} not in [${Array.from(rule.actions).join(', ')}]`;
|
|
152
|
+
step.result = false;
|
|
153
|
+
evaluationTrace.push(step);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
// Check address match (any pattern in the list matches)
|
|
157
|
+
if (rule.addressPatterns) {
|
|
158
|
+
if (!address) {
|
|
159
|
+
step.expression = `address: pattern requires address, but none provided`;
|
|
160
|
+
step.result = false;
|
|
161
|
+
evaluationTrace.push(step);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const matched = rule.addressPatterns.some((p) => p.match(address));
|
|
165
|
+
if (!matched) {
|
|
166
|
+
const patterns = rule.addressPatterns.map((p) => p.source).join(', ');
|
|
167
|
+
step.expression = `address: none of [${patterns}] matched ${address}`;
|
|
168
|
+
step.result = false;
|
|
169
|
+
evaluationTrace.push(step);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Check scope match
|
|
174
|
+
if (rule.scopeMatcher) {
|
|
175
|
+
if (!rule.scopeMatcher(grantedScopes)) {
|
|
176
|
+
step.expression = `scope: requirement not satisfied`;
|
|
177
|
+
step.boundValues = { grantedScopes: [...grantedScopes] };
|
|
178
|
+
step.result = false;
|
|
179
|
+
evaluationTrace.push(step);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Rule matched
|
|
184
|
+
step.result = true;
|
|
185
|
+
step.expression = 'all conditions matched';
|
|
186
|
+
step.boundValues = {
|
|
187
|
+
action: resolvedAction,
|
|
188
|
+
address,
|
|
189
|
+
grantedScopes: [...grantedScopes],
|
|
190
|
+
};
|
|
191
|
+
evaluationTrace.push(step);
|
|
192
|
+
logger.debug('rule_matched', {
|
|
193
|
+
ruleId: rule.id,
|
|
194
|
+
effect: rule.effect,
|
|
195
|
+
action: resolvedAction,
|
|
196
|
+
address,
|
|
197
|
+
});
|
|
198
|
+
return {
|
|
199
|
+
effect: rule.effect,
|
|
200
|
+
reason: rule.description ?? `Matched rule: ${rule.id}`,
|
|
201
|
+
matchedRule: rule.id,
|
|
202
|
+
evaluationTrace,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
// No rule matched, apply default effect
|
|
206
|
+
logger.debug('no_rule_matched', {
|
|
207
|
+
defaultEffect: this.defaultEffect,
|
|
208
|
+
action: resolvedAction,
|
|
209
|
+
address,
|
|
210
|
+
});
|
|
211
|
+
return {
|
|
212
|
+
effect: this.defaultEffect,
|
|
213
|
+
reason: `No rule matched, applying default effect: ${this.defaultEffect}`,
|
|
214
|
+
evaluationTrace,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
validateDefaultEffect(effect) {
|
|
218
|
+
if (effect !== 'allow' && effect !== 'deny') {
|
|
219
|
+
throw new Error(`Invalid default_effect: "${String(effect)}". Must be "allow" or "deny"`);
|
|
220
|
+
}
|
|
221
|
+
return effect;
|
|
222
|
+
}
|
|
223
|
+
warnUnknownPolicyFields(definition) {
|
|
224
|
+
for (const key of Object.keys(definition)) {
|
|
225
|
+
if (!KNOWN_POLICY_FIELDS.has(key)) {
|
|
226
|
+
logger.warning('unknown_policy_field', { field: key });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
compileRules(rules, warnOnUnknown) {
|
|
231
|
+
return rules.map((rule, index) => this.compileRule(rule, index, warnOnUnknown));
|
|
232
|
+
}
|
|
233
|
+
compileRule(rule, index, warnOnUnknown) {
|
|
234
|
+
// Generate ID if not provided
|
|
235
|
+
const id = rule.id ?? `rule_${index}`;
|
|
236
|
+
// Validate effect
|
|
237
|
+
if (!VALID_EFFECTS.includes(rule.effect)) {
|
|
238
|
+
throw new Error(`Invalid effect in rule "${id}": "${String(rule.effect)}". Must be "allow" or "deny"`);
|
|
239
|
+
}
|
|
240
|
+
// Validate and compile action(s)
|
|
241
|
+
const actions = this.compileActions(rule.action, id);
|
|
242
|
+
// Compile address patterns (glob-only, no regex)
|
|
243
|
+
const addressPatterns = this.compileAddress(rule.address, id);
|
|
244
|
+
// Compile frame type gating
|
|
245
|
+
const frameTypes = this.compileFrameTypes(rule.frame_type, id);
|
|
246
|
+
// Compile origin type gating
|
|
247
|
+
const originTypes = this.compileOriginTypes(rule.origin_type, id);
|
|
248
|
+
// Compile scope matcher (glob-only, no regex)
|
|
249
|
+
let scopeMatcher;
|
|
250
|
+
if (rule.scope !== undefined) {
|
|
251
|
+
try {
|
|
252
|
+
const compiled = compileGlobOnlyScopeRequirement(rule.scope, id);
|
|
253
|
+
scopeMatcher = (scopes) => compiled.evaluate(scopes);
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
throw new Error(`Invalid scope requirement in rule "${id}": ${error instanceof Error ? error.message : String(error)}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Warn about unknown fields
|
|
260
|
+
if (warnOnUnknown) {
|
|
261
|
+
for (const key of Object.keys(rule)) {
|
|
262
|
+
if (!KNOWN_RULE_FIELDS.has(key)) {
|
|
263
|
+
logger.warning('unknown_rule_field', { ruleId: id, field: key });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
id,
|
|
269
|
+
description: rule.description,
|
|
270
|
+
effect: rule.effect,
|
|
271
|
+
actions,
|
|
272
|
+
frameTypes,
|
|
273
|
+
originTypes,
|
|
274
|
+
addressPatterns,
|
|
275
|
+
scopeMatcher,
|
|
276
|
+
hasWhenClause: typeof rule.when === 'string' && rule.when.length > 0,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Compiles action field into a Set of valid actions.
|
|
281
|
+
* Supports single RuleAction or array of RuleAction (implicit any-of).
|
|
282
|
+
*/
|
|
283
|
+
compileActions(action, ruleId) {
|
|
284
|
+
// Default to wildcard if not specified
|
|
285
|
+
if (action === undefined) {
|
|
286
|
+
return new Set(['*']);
|
|
287
|
+
}
|
|
288
|
+
// Handle single action
|
|
289
|
+
if (typeof action === 'string') {
|
|
290
|
+
if (!VALID_ACTIONS.includes(action)) {
|
|
291
|
+
throw new Error(`Invalid action in rule "${ruleId}": "${action}". Must be one of: ${VALID_ACTIONS.join(', ')}`);
|
|
292
|
+
}
|
|
293
|
+
return new Set([action]);
|
|
294
|
+
}
|
|
295
|
+
// Handle array of actions
|
|
296
|
+
if (!Array.isArray(action)) {
|
|
297
|
+
throw new Error(`Invalid action in rule "${ruleId}": must be a string or array of strings`);
|
|
298
|
+
}
|
|
299
|
+
if (action.length === 0) {
|
|
300
|
+
throw new Error(`Invalid action in rule "${ruleId}": array must not be empty`);
|
|
301
|
+
}
|
|
302
|
+
const actions = new Set();
|
|
303
|
+
for (const a of action) {
|
|
304
|
+
if (typeof a !== 'string') {
|
|
305
|
+
throw new Error(`Invalid action in rule "${ruleId}": all values must be strings`);
|
|
306
|
+
}
|
|
307
|
+
if (!VALID_ACTIONS.includes(a)) {
|
|
308
|
+
throw new Error(`Invalid action in rule "${ruleId}": "${a}". Must be one of: ${VALID_ACTIONS.join(', ')}`);
|
|
309
|
+
}
|
|
310
|
+
actions.add(a);
|
|
311
|
+
}
|
|
312
|
+
return actions;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Compiles address field into an array of glob matchers.
|
|
316
|
+
* Supports single string or array of strings (implicit any-of).
|
|
317
|
+
* Returns undefined if not specified (no address gating).
|
|
318
|
+
*
|
|
319
|
+
* All patterns are treated as globs - `^` prefix is rejected as an error.
|
|
320
|
+
*/
|
|
321
|
+
compileAddress(address, ruleId) {
|
|
322
|
+
if (address === undefined) {
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
const context = `address in rule "${ruleId}"`;
|
|
326
|
+
// Handle single address pattern
|
|
327
|
+
if (typeof address === 'string') {
|
|
328
|
+
const trimmed = address.trim();
|
|
329
|
+
if (!trimmed) {
|
|
330
|
+
throw new Error(`Invalid address in rule "${ruleId}": value must not be empty`);
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
return [compileGlobPattern(trimmed, context)];
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
throw new Error(`Invalid address in rule "${ruleId}": ${error instanceof Error ? error.message : String(error)}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// Handle array of address patterns
|
|
340
|
+
if (!Array.isArray(address)) {
|
|
341
|
+
throw new Error(`Invalid address in rule "${ruleId}": must be a string or array of strings`);
|
|
342
|
+
}
|
|
343
|
+
if (address.length === 0) {
|
|
344
|
+
throw new Error(`Invalid address in rule "${ruleId}": array must not be empty`);
|
|
345
|
+
}
|
|
346
|
+
const patterns = [];
|
|
347
|
+
for (const addr of address) {
|
|
348
|
+
if (typeof addr !== 'string') {
|
|
349
|
+
throw new Error(`Invalid address in rule "${ruleId}": all values must be strings`);
|
|
350
|
+
}
|
|
351
|
+
const trimmed = addr.trim();
|
|
352
|
+
if (!trimmed) {
|
|
353
|
+
throw new Error(`Invalid address in rule "${ruleId}": values must not be empty`);
|
|
354
|
+
}
|
|
355
|
+
try {
|
|
356
|
+
patterns.push(compileGlobPattern(trimmed, context));
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
throw new Error(`Invalid address in rule "${ruleId}": ${error instanceof Error ? error.message : String(error)}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return patterns;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Compiles frame_type field into a Set of normalized frame types.
|
|
366
|
+
* Supports single string or array of strings (implicit any-of).
|
|
367
|
+
* Returns undefined if not specified (no frame type gating).
|
|
368
|
+
*/
|
|
369
|
+
compileFrameTypes(frameType, ruleId) {
|
|
370
|
+
if (frameType === undefined) {
|
|
371
|
+
return undefined;
|
|
372
|
+
}
|
|
373
|
+
// Handle single frame type
|
|
374
|
+
if (typeof frameType === 'string') {
|
|
375
|
+
const normalized = frameType.trim().toLowerCase();
|
|
376
|
+
if (!normalized) {
|
|
377
|
+
throw new Error(`Invalid frame_type in rule "${ruleId}": value must not be empty`);
|
|
378
|
+
}
|
|
379
|
+
return new Set([normalized]);
|
|
380
|
+
}
|
|
381
|
+
// Handle array of frame types
|
|
382
|
+
if (!Array.isArray(frameType)) {
|
|
383
|
+
throw new Error(`Invalid frame_type in rule "${ruleId}": must be a string or array of strings`);
|
|
384
|
+
}
|
|
385
|
+
if (frameType.length === 0) {
|
|
386
|
+
throw new Error(`Invalid frame_type in rule "${ruleId}": array must not be empty`);
|
|
387
|
+
}
|
|
388
|
+
const frameTypes = new Set();
|
|
389
|
+
for (const ft of frameType) {
|
|
390
|
+
if (typeof ft !== 'string') {
|
|
391
|
+
throw new Error(`Invalid frame_type in rule "${ruleId}": all values must be strings`);
|
|
392
|
+
}
|
|
393
|
+
const normalized = ft.trim().toLowerCase();
|
|
394
|
+
if (!normalized) {
|
|
395
|
+
throw new Error(`Invalid frame_type in rule "${ruleId}": values must not be empty`);
|
|
396
|
+
}
|
|
397
|
+
frameTypes.add(normalized);
|
|
398
|
+
}
|
|
399
|
+
return frameTypes;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Compiles origin_type field into a Set of normalized origin types.
|
|
403
|
+
* Supports single string or array of strings (implicit any-of).
|
|
404
|
+
* Returns undefined if not specified (no origin type gating).
|
|
405
|
+
* Valid values: 'downstream', 'upstream', 'peer', 'local' (case-insensitive).
|
|
406
|
+
*/
|
|
407
|
+
compileOriginTypes(originType, ruleId) {
|
|
408
|
+
if (originType === undefined) {
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
// Handle single origin type
|
|
412
|
+
if (typeof originType === 'string') {
|
|
413
|
+
const normalized = originType.trim().toLowerCase();
|
|
414
|
+
if (!normalized) {
|
|
415
|
+
throw new Error(`Invalid origin_type in rule "${ruleId}": value must not be empty`);
|
|
416
|
+
}
|
|
417
|
+
if (!VALID_ORIGIN_TYPES.includes(normalized)) {
|
|
418
|
+
throw new Error(`Invalid origin_type in rule "${ruleId}": "${originType}". Must be one of: ${VALID_ORIGIN_TYPES.join(', ')}`);
|
|
419
|
+
}
|
|
420
|
+
return new Set([normalized]);
|
|
421
|
+
}
|
|
422
|
+
// Handle array of origin types
|
|
423
|
+
if (!Array.isArray(originType)) {
|
|
424
|
+
throw new Error(`Invalid origin_type in rule "${ruleId}": must be a string or array of strings`);
|
|
425
|
+
}
|
|
426
|
+
if (originType.length === 0) {
|
|
427
|
+
throw new Error(`Invalid origin_type in rule "${ruleId}": array must not be empty`);
|
|
428
|
+
}
|
|
429
|
+
const originTypes = new Set();
|
|
430
|
+
for (const ot of originType) {
|
|
431
|
+
if (typeof ot !== 'string') {
|
|
432
|
+
throw new Error(`Invalid origin_type in rule "${ruleId}": all values must be strings`);
|
|
433
|
+
}
|
|
434
|
+
const normalized = ot.trim().toLowerCase();
|
|
435
|
+
if (!normalized) {
|
|
436
|
+
throw new Error(`Invalid origin_type in rule "${ruleId}": values must not be empty`);
|
|
437
|
+
}
|
|
438
|
+
if (!VALID_ORIGIN_TYPES.includes(normalized)) {
|
|
439
|
+
throw new Error(`Invalid origin_type in rule "${ruleId}": "${ot}". Must be one of: ${VALID_ORIGIN_TYPES.join(', ')}`);
|
|
440
|
+
}
|
|
441
|
+
originTypes.add(normalized);
|
|
442
|
+
}
|
|
443
|
+
return originTypes;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authorization policy module exports.
|
|
3
|
+
*
|
|
4
|
+
* This module provides interfaces and factories for pluggable authorization policies.
|
|
5
|
+
*/
|
|
6
|
+
// Core interfaces and types
|
|
7
|
+
export * from './authorization-policy.js';
|
|
8
|
+
export * from './authorization-policy-source.js';
|
|
9
|
+
export * from './authorization-policy-definition.js';
|
|
10
|
+
// Pattern and scope matchers
|
|
11
|
+
export { compilePattern, compileGlobPattern, getCompiledGlobPattern, matchPattern, isRegexPattern, assertNotRegexPattern, } from './pattern-matcher.js';
|
|
12
|
+
export { evaluateScopeRequirement, compileScopeRequirement, normalizeScopeRequirement, compileGlobOnlyScopeRequirement, } from './scope-matcher.js';
|
|
13
|
+
// Factory base classes
|
|
14
|
+
export { AUTHORIZATION_POLICY_FACTORY_BASE_TYPE, AuthorizationPolicyFactory, } from './authorization-policy-factory.js';
|
|
15
|
+
export { AUTHORIZATION_POLICY_SOURCE_FACTORY_BASE_TYPE, AuthorizationPolicySourceFactory, } from './authorization-policy-source-factory.js';
|
|
16
|
+
// Basic authorization policy (browser and node)
|
|
17
|
+
export { BasicAuthorizationPolicy } from './basic-authorization-policy.js';
|
|
18
|
+
export { BasicAuthorizationPolicyFactory } from './basic-authorization-policy-factory.js';
|
|
19
|
+
// Note: LocalFileAuthorizationPolicySource and its factory are node-only
|
|
20
|
+
// and are registered via the factory manifest, not exported here directly.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { safeImport } from '../../../util/lazy-import.js';
|
|
2
|
+
import { AUTHORIZATION_POLICY_SOURCE_FACTORY_BASE_TYPE, AuthorizationPolicySourceFactory, } from './authorization-policy-source-factory.js';
|
|
3
|
+
let localFileModulePromise = null;
|
|
4
|
+
async function getLocalFileModule() {
|
|
5
|
+
if (!localFileModulePromise) {
|
|
6
|
+
localFileModulePromise = safeImport(() => import('./local-file-authorization-policy-source.js'), 'local-file-authorization-policy-source');
|
|
7
|
+
}
|
|
8
|
+
return localFileModulePromise;
|
|
9
|
+
}
|
|
10
|
+
function normalizeConfig(config) {
|
|
11
|
+
if (!config) {
|
|
12
|
+
throw new Error('LocalFileAuthorizationPolicySourceFactory requires a configuration with a path');
|
|
13
|
+
}
|
|
14
|
+
const candidate = config;
|
|
15
|
+
const path = candidate.path;
|
|
16
|
+
if (typeof path !== 'string' || path.trim().length === 0) {
|
|
17
|
+
throw new Error('LocalFileAuthorizationPolicySourceConfig requires a non-empty path');
|
|
18
|
+
}
|
|
19
|
+
const format = candidate.format;
|
|
20
|
+
if (format !== undefined && !['yaml', 'json', 'auto'].includes(format)) {
|
|
21
|
+
throw new Error(`Invalid format "${String(format)}". Must be "yaml", "json", or "auto"`);
|
|
22
|
+
}
|
|
23
|
+
const policyFactory = candidate.policyFactory;
|
|
24
|
+
return {
|
|
25
|
+
path: path.trim(),
|
|
26
|
+
format: format ?? 'auto',
|
|
27
|
+
policyFactory,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Factory metadata for registration.
|
|
32
|
+
*/
|
|
33
|
+
export const FACTORY_META = {
|
|
34
|
+
base: AUTHORIZATION_POLICY_SOURCE_FACTORY_BASE_TYPE,
|
|
35
|
+
key: 'LocalFileAuthorizationPolicySource',
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Factory for creating LocalFileAuthorizationPolicySource instances.
|
|
39
|
+
*
|
|
40
|
+
* This factory uses lazy loading to avoid pulling in Node.js-specific
|
|
41
|
+
* code (filesystem operations) in browser environments.
|
|
42
|
+
*/
|
|
43
|
+
export class LocalFileAuthorizationPolicySourceFactory extends AuthorizationPolicySourceFactory {
|
|
44
|
+
constructor() {
|
|
45
|
+
super(...arguments);
|
|
46
|
+
this.type = 'LocalFileAuthorizationPolicySource';
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Creates a LocalFileAuthorizationPolicySource from the given configuration.
|
|
50
|
+
*
|
|
51
|
+
* @param config - Configuration specifying the policy file path and options
|
|
52
|
+
* @returns The created policy source
|
|
53
|
+
*/
|
|
54
|
+
async create(config) {
|
|
55
|
+
const normalized = normalizeConfig(config);
|
|
56
|
+
const { LocalFileAuthorizationPolicySource } = await getLocalFileModule();
|
|
57
|
+
return new LocalFileAuthorizationPolicySource({
|
|
58
|
+
path: normalized.path,
|
|
59
|
+
format: normalized.format,
|
|
60
|
+
policyFactory: normalized.policyFactory,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export default LocalFileAuthorizationPolicySourceFactory;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { parse as parseYaml } from 'yaml';
|
|
2
|
+
import { getLogger } from '../../../util/logging.js';
|
|
3
|
+
import { AuthorizationPolicyFactory, } from './authorization-policy-factory.js';
|
|
4
|
+
const logger = getLogger('naylence.fame.security.auth.policy.local_file_authorization_policy_source');
|
|
5
|
+
function isPlainObject(value) {
|
|
6
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function parseJson(content) {
|
|
9
|
+
const parsed = JSON.parse(content);
|
|
10
|
+
if (!isPlainObject(parsed)) {
|
|
11
|
+
throw new Error('Parsed JSON policy must be an object');
|
|
12
|
+
}
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
15
|
+
function parseYamlContent(content) {
|
|
16
|
+
const parsed = parseYaml(content ?? '');
|
|
17
|
+
if (parsed == null) {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
if (!isPlainObject(parsed)) {
|
|
21
|
+
throw new Error('Parsed YAML policy must be an object');
|
|
22
|
+
}
|
|
23
|
+
return parsed;
|
|
24
|
+
}
|
|
25
|
+
function detectFormat(filePath) {
|
|
26
|
+
const lower = filePath.toLowerCase();
|
|
27
|
+
if (lower.endsWith('.yaml') || lower.endsWith('.yml')) {
|
|
28
|
+
return 'yaml';
|
|
29
|
+
}
|
|
30
|
+
if (lower.endsWith('.json')) {
|
|
31
|
+
return 'json';
|
|
32
|
+
}
|
|
33
|
+
// Default to YAML for unknown extensions
|
|
34
|
+
return 'yaml';
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* An authorization policy source that loads policy definitions from a local file.
|
|
38
|
+
*
|
|
39
|
+
* Supports YAML and JSON formats. The file must contain a valid policy
|
|
40
|
+
* configuration object that can be used to create an AuthorizationPolicy
|
|
41
|
+
* via the factory system.
|
|
42
|
+
*
|
|
43
|
+
* This is a Node.js-only implementation that uses the filesystem.
|
|
44
|
+
*/
|
|
45
|
+
export class LocalFileAuthorizationPolicySource {
|
|
46
|
+
constructor(options) {
|
|
47
|
+
this.cachedPolicy = null;
|
|
48
|
+
this.path = options.path;
|
|
49
|
+
this.format = options.format ?? 'auto';
|
|
50
|
+
this.policyFactoryConfig = options.policyFactory;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Loads the authorization policy from the configured file.
|
|
54
|
+
*
|
|
55
|
+
* The file is read and parsed according to the configured format.
|
|
56
|
+
* The parsed content is then used to create an AuthorizationPolicy
|
|
57
|
+
* via the factory system.
|
|
58
|
+
*
|
|
59
|
+
* @returns The loaded authorization policy
|
|
60
|
+
*/
|
|
61
|
+
async loadPolicy() {
|
|
62
|
+
// Return cached policy if available
|
|
63
|
+
if (this.cachedPolicy) {
|
|
64
|
+
return this.cachedPolicy;
|
|
65
|
+
}
|
|
66
|
+
logger.debug('loading_policy_from_file', { path: this.path });
|
|
67
|
+
// Dynamic import of fs for Node.js
|
|
68
|
+
const fs = await import('node:fs/promises');
|
|
69
|
+
// Read the file
|
|
70
|
+
const content = await fs.readFile(this.path, 'utf-8');
|
|
71
|
+
// Determine format
|
|
72
|
+
const effectiveFormat = this.format === 'auto' ? detectFormat(this.path) : this.format;
|
|
73
|
+
// Parse the content
|
|
74
|
+
let policyDefinition;
|
|
75
|
+
if (effectiveFormat === 'json') {
|
|
76
|
+
policyDefinition = parseJson(content);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
policyDefinition = parseYamlContent(content);
|
|
80
|
+
}
|
|
81
|
+
logger.debug('parsed_policy_definition', {
|
|
82
|
+
path: this.path,
|
|
83
|
+
format: effectiveFormat,
|
|
84
|
+
hasType: 'type' in policyDefinition,
|
|
85
|
+
});
|
|
86
|
+
// Determine the factory configuration to use
|
|
87
|
+
const factoryConfig = this.policyFactoryConfig ?? policyDefinition;
|
|
88
|
+
// Ensure we have a type field for the factory
|
|
89
|
+
if (!('type' in factoryConfig) || typeof factoryConfig.type !== 'string') {
|
|
90
|
+
throw new Error(`Policy definition at ${this.path} must have a 'type' field, ` +
|
|
91
|
+
`or policyFactory config must be provided`);
|
|
92
|
+
}
|
|
93
|
+
// Build the factory config with the policy definition
|
|
94
|
+
// The file content IS the policy definition, so we extract the type
|
|
95
|
+
// and wrap the remaining content as the policyDefinition
|
|
96
|
+
const { type, ...restOfFile } = policyDefinition;
|
|
97
|
+
const mergedConfig = this.policyFactoryConfig != null
|
|
98
|
+
? { ...this.policyFactoryConfig, policyDefinition }
|
|
99
|
+
: { type: factoryConfig.type, policyDefinition: restOfFile };
|
|
100
|
+
// Create the policy using the factory system
|
|
101
|
+
const policy = await AuthorizationPolicyFactory.createAuthorizationPolicy(mergedConfig);
|
|
102
|
+
if (!policy) {
|
|
103
|
+
throw new Error(`Failed to create authorization policy from ${this.path}`);
|
|
104
|
+
}
|
|
105
|
+
this.cachedPolicy = policy;
|
|
106
|
+
logger.info('loaded_policy_from_file', {
|
|
107
|
+
path: this.path,
|
|
108
|
+
policyType: factoryConfig.type,
|
|
109
|
+
});
|
|
110
|
+
return policy;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Clears the cached policy, forcing a reload on the next loadPolicy() call.
|
|
114
|
+
*/
|
|
115
|
+
clearCache() {
|
|
116
|
+
this.cachedPolicy = null;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Reloads the policy from the file, clearing any cached version.
|
|
120
|
+
*
|
|
121
|
+
* @returns The reloaded authorization policy
|
|
122
|
+
*/
|
|
123
|
+
async reloadPolicy() {
|
|
124
|
+
this.clearCache();
|
|
125
|
+
return this.loadPolicy();
|
|
126
|
+
}
|
|
127
|
+
}
|