@api-client/core 0.19.40 → 0.19.42
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/build/src/index.d.ts +1 -1
- package/build/src/index.d.ts.map +1 -1
- package/build/src/index.js.map +1 -1
- package/build/src/modeling/RuntimeApiModel.d.ts +20 -0
- package/build/src/modeling/RuntimeApiModel.d.ts.map +1 -1
- package/build/src/modeling/RuntimeApiModel.js +133 -0
- package/build/src/modeling/RuntimeApiModel.js.map +1 -1
- package/build/src/modeling/index.d.ts +2 -0
- package/build/src/modeling/index.d.ts.map +1 -1
- package/build/src/modeling/index.js +1 -0
- package/build/src/modeling/index.js.map +1 -1
- package/build/src/modeling/rules/AccessRule.d.ts +40 -1
- package/build/src/modeling/rules/AccessRule.d.ts.map +1 -1
- package/build/src/modeling/rules/AccessRule.js +44 -2
- package/build/src/modeling/rules/AccessRule.js.map +1 -1
- package/build/src/modeling/rules/AllowAuthenticated.d.ts.map +1 -1
- package/build/src/modeling/rules/AllowAuthenticated.js +9 -2
- package/build/src/modeling/rules/AllowAuthenticated.js.map +1 -1
- package/build/src/modeling/rules/AllowPublic.d.ts.map +1 -1
- package/build/src/modeling/rules/AllowPublic.js +9 -2
- package/build/src/modeling/rules/AllowPublic.js.map +1 -1
- package/build/src/modeling/rules/LifecycleStatus.d.ts +36 -0
- package/build/src/modeling/rules/LifecycleStatus.d.ts.map +1 -0
- package/build/src/modeling/rules/LifecycleStatus.js +60 -0
- package/build/src/modeling/rules/LifecycleStatus.js.map +1 -0
- package/build/src/modeling/rules/MatchEmailDomain.d.ts.map +1 -1
- package/build/src/modeling/rules/MatchEmailDomain.js +9 -2
- package/build/src/modeling/rules/MatchEmailDomain.js.map +1 -1
- package/build/src/modeling/rules/MatchResourceAttribute.d.ts +38 -0
- package/build/src/modeling/rules/MatchResourceAttribute.d.ts.map +1 -0
- package/build/src/modeling/rules/MatchResourceAttribute.js +68 -0
- package/build/src/modeling/rules/MatchResourceAttribute.js.map +1 -0
- package/build/src/modeling/rules/MatchResourceOwner.d.ts.map +1 -1
- package/build/src/modeling/rules/MatchResourceOwner.js +8 -2
- package/build/src/modeling/rules/MatchResourceOwner.js.map +1 -1
- package/build/src/modeling/rules/MatchUserProperty.d.ts.map +1 -1
- package/build/src/modeling/rules/MatchUserProperty.js +9 -2
- package/build/src/modeling/rules/MatchUserProperty.js.map +1 -1
- package/build/src/modeling/rules/MatchUserRole.d.ts.map +1 -1
- package/build/src/modeling/rules/MatchUserRole.js +9 -2
- package/build/src/modeling/rules/MatchUserRole.js.map +1 -1
- package/build/src/modeling/rules/index.d.ts +8 -6
- package/build/src/modeling/rules/index.d.ts.map +1 -1
- package/build/src/modeling/rules/index.js +8 -2
- package/build/src/modeling/rules/index.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/modeling/RuntimeApiModel.ts +166 -2
- package/src/modeling/rules/AccessRule.ts +70 -2
- package/src/modeling/rules/AllowAuthenticated.ts +13 -2
- package/src/modeling/rules/AllowPublic.ts +13 -2
- package/src/modeling/rules/LifecycleStatus.ts +71 -0
- package/src/modeling/rules/MatchEmailDomain.ts +13 -2
- package/src/modeling/rules/MatchResourceAttribute.ts +82 -0
- package/src/modeling/rules/MatchResourceOwner.ts +12 -2
- package/src/modeling/rules/MatchUserProperty.ts +13 -2
- package/src/modeling/rules/MatchUserRole.ts +13 -2
- package/tests/unit/modeling/RuntimeApiModel.spec.ts +189 -1
- package/tests/unit/modeling/actions/Action.spec.ts +2 -2
- package/tests/unit/modeling/actions/CreateAction.spec.ts +1 -1
- package/tests/unit/modeling/actions/ReadAction.spec.ts +2 -2
- package/tests/unit/modeling/exposed_entity.spec.ts +1 -1
- package/tests/unit/modeling/rules/AccessRule.spec.ts +5 -5
- package/tests/unit/modeling/rules/LifecycleStatus.spec.ts +55 -0
- package/tests/unit/modeling/rules/MatchResourceAttribute.spec.ts +66 -0
package/package.json
CHANGED
|
@@ -7,6 +7,7 @@ import type { Action } from './actions/Action.js'
|
|
|
7
7
|
import { SemanticType } from './Semantics.js'
|
|
8
8
|
import type { DomainEntity } from './DomainEntity.js'
|
|
9
9
|
import type { DomainProperty } from './DomainProperty.js'
|
|
10
|
+
import { AccessRule, AccessRuleExecutionPhase } from './rules/AccessRule.js'
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Identifies a specific exposed entity and its action kind.
|
|
@@ -42,6 +43,19 @@ export interface RuntimeResolvedAction {
|
|
|
42
43
|
params: Record<string, string>
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
export type RuleEvaluator = (rule: AccessRule) => Promise<boolean | undefined> | boolean | undefined
|
|
47
|
+
|
|
48
|
+
interface PhaseRules {
|
|
49
|
+
permissionRules: AccessRule[]
|
|
50
|
+
mandatoryRules: AccessRule[]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ActionRulesCache {
|
|
54
|
+
preFetch: PhaseRules
|
|
55
|
+
fetch: PhaseRules
|
|
56
|
+
postFetch: PhaseRules
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
/**
|
|
46
60
|
* An optimized API Model subclass designed for fast runtime lookups.
|
|
47
61
|
* It pre-compiles the RoutingMap into a radix tree for O(log N) or faster endpoint resolution.
|
|
@@ -74,6 +88,11 @@ export class RuntimeApiModel extends ApiModel {
|
|
|
74
88
|
role?: DomainProperty
|
|
75
89
|
} = {}
|
|
76
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Cached access rules for each action to avoid runtime computation overhead.
|
|
93
|
+
*/
|
|
94
|
+
#actionRulesCache = new WeakMap<Action, ActionRulesCache>()
|
|
95
|
+
|
|
77
96
|
constructor(schema: RuntimeApiModelSchema, domainSchema: DataDomainSchema) {
|
|
78
97
|
super(schema, domainSchema)
|
|
79
98
|
|
|
@@ -82,9 +101,93 @@ export class RuntimeApiModel extends ApiModel {
|
|
|
82
101
|
}
|
|
83
102
|
|
|
84
103
|
this.#cacheEntitiesAndProperties()
|
|
104
|
+
this.#precomputeAccessRules()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#precomputeAccessRules(): void {
|
|
108
|
+
for (const entity of this.exposes.values()) {
|
|
109
|
+
for (const action of entity.actions) {
|
|
110
|
+
this.#actionRulesCache.set(action, this.#computeEffectiveRules(action, entity))
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#computeEffectiveRules(action: Action, entity: ExposedEntity): ActionRulesCache {
|
|
116
|
+
const hierarchy: AccessRule[][] = []
|
|
117
|
+
|
|
118
|
+
if (action.accessRule && action.accessRule.length > 0) {
|
|
119
|
+
hierarchy.push(action.accessRule)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let currentEntity: ExposedEntity | undefined = entity
|
|
123
|
+
while (currentEntity) {
|
|
124
|
+
if (currentEntity.accessRule && currentEntity.accessRule.length > 0) {
|
|
125
|
+
hierarchy.push(currentEntity.accessRule)
|
|
126
|
+
}
|
|
127
|
+
currentEntity = currentEntity.parent ? this.exposes.get(currentEntity.parent.key) : undefined
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (this.accessRule && this.accessRule.length > 0) {
|
|
131
|
+
hierarchy.push(this.accessRule)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const result: ActionRulesCache = {
|
|
135
|
+
preFetch: {
|
|
136
|
+
permissionRules: [],
|
|
137
|
+
mandatoryRules: [],
|
|
138
|
+
},
|
|
139
|
+
fetch: {
|
|
140
|
+
permissionRules: [],
|
|
141
|
+
mandatoryRules: [],
|
|
142
|
+
},
|
|
143
|
+
postFetch: {
|
|
144
|
+
permissionRules: [],
|
|
145
|
+
mandatoryRules: [],
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const seenTypes = new Set<string>()
|
|
150
|
+
|
|
151
|
+
for (const rulesLevel of hierarchy) {
|
|
152
|
+
const typesInThisLevel = new Set<string>()
|
|
153
|
+
for (const rule of rulesLevel) {
|
|
154
|
+
const phase = rule.metadata[action.kind as keyof typeof rule.metadata]
|
|
155
|
+
|
|
156
|
+
// If the rule does not specify an execution phase for this action kind, it is not evaluated
|
|
157
|
+
if (!phase) {
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Shadowing: If a rule of the same type was defined closer to the action, ignore this one.
|
|
162
|
+
if (seenTypes.has(rule.type)) {
|
|
163
|
+
continue
|
|
164
|
+
}
|
|
165
|
+
typesInThisLevel.add(rule.type)
|
|
166
|
+
|
|
167
|
+
let bucket
|
|
168
|
+
if (phase === AccessRuleExecutionPhase.POST_FETCH) {
|
|
169
|
+
bucket = result.postFetch
|
|
170
|
+
} else if (phase === AccessRuleExecutionPhase.FETCH) {
|
|
171
|
+
bucket = result.fetch
|
|
172
|
+
} else {
|
|
173
|
+
bucket = result.preFetch
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (rule.mandatory) {
|
|
177
|
+
bucket.mandatoryRules.push(rule)
|
|
178
|
+
} else {
|
|
179
|
+
bucket.permissionRules.push(rule)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
for (const type of typesInThisLevel) {
|
|
183
|
+
seenTypes.add(type)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return result
|
|
85
188
|
}
|
|
86
189
|
|
|
87
|
-
#cacheEntitiesAndProperties() {
|
|
190
|
+
#cacheEntitiesAndProperties(): void {
|
|
88
191
|
if (!this.user || !this.domain) {
|
|
89
192
|
return
|
|
90
193
|
}
|
|
@@ -109,7 +212,7 @@ export class RuntimeApiModel extends ApiModel {
|
|
|
109
212
|
}
|
|
110
213
|
}
|
|
111
214
|
|
|
112
|
-
#initializeRouter(routingMap: RoutingMap) {
|
|
215
|
+
#initializeRouter(routingMap: RoutingMap): void {
|
|
113
216
|
for (const [method, definitions] of Object.entries(routingMap)) {
|
|
114
217
|
const parsedRoutes = []
|
|
115
218
|
for (const def of definitions) {
|
|
@@ -167,6 +270,67 @@ export class RuntimeApiModel extends ApiModel {
|
|
|
167
270
|
}
|
|
168
271
|
}
|
|
169
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Evaluates access rules for a given action and phase.
|
|
275
|
+
*
|
|
276
|
+
* The evaluation process follows two phases per execution phase (PRE_FETCH, FETCH, POST_FETCH):
|
|
277
|
+
* 1. Mandatory Phase: All rules marked as `mandatory: true` across all levels must return true.
|
|
278
|
+
* If any fail, the request is immediately rejected.
|
|
279
|
+
* 2. Permission Phase: Evaluation follows the hierarchy from most specific to most general:
|
|
280
|
+
* Action -> Endpoint (ExposedEntity) -> API Model.
|
|
281
|
+
* If an explicit allow (true) or deny (false) is hit, evaluation stops and returns the result.
|
|
282
|
+
* If no rules match (all return undefined), the request is rejected by default.
|
|
283
|
+
*
|
|
284
|
+
* @param action The resolved action to evaluate.
|
|
285
|
+
* @param evaluator A callback that evaluates a single rule. Should return true (allow),
|
|
286
|
+
* false (deny), or undefined (no hit).
|
|
287
|
+
* @param phase The execution phase to evaluate.
|
|
288
|
+
* @returns A promise that resolves to true if access is granted, false if denied.
|
|
289
|
+
*/
|
|
290
|
+
async evaluateAccess(
|
|
291
|
+
action: RuntimeResolvedAction,
|
|
292
|
+
evaluator: RuleEvaluator,
|
|
293
|
+
phase: AccessRuleExecutionPhase
|
|
294
|
+
): Promise<boolean> {
|
|
295
|
+
let cachedRules = this.#actionRulesCache.get(action.action)
|
|
296
|
+
if (!cachedRules) {
|
|
297
|
+
// Fallback if somehow action is not cached (e.g. dynamically added after initialization or in tests)
|
|
298
|
+
cachedRules = this.#computeEffectiveRules(action.action, action.entity)
|
|
299
|
+
this.#actionRulesCache.set(action.action, cachedRules)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
let rulesForPhase
|
|
303
|
+
if (phase === AccessRuleExecutionPhase.POST_FETCH) {
|
|
304
|
+
rulesForPhase = cachedRules.postFetch
|
|
305
|
+
} else if (phase === AccessRuleExecutionPhase.FETCH) {
|
|
306
|
+
rulesForPhase = cachedRules.fetch
|
|
307
|
+
} else {
|
|
308
|
+
rulesForPhase = cachedRules.preFetch
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Step 1: Mandatory Phase
|
|
312
|
+
for (const rule of rulesForPhase.mandatoryRules) {
|
|
313
|
+
const result = await evaluator(rule)
|
|
314
|
+
if (result !== true) {
|
|
315
|
+
return false // Immediately reject if any mandatory rule fails
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Step 2-4: Permission Phase
|
|
320
|
+
for (const rule of rulesForPhase.permissionRules) {
|
|
321
|
+
const result = await evaluator(rule)
|
|
322
|
+
if (result === true) {
|
|
323
|
+
return true
|
|
324
|
+
}
|
|
325
|
+
if (result === false) {
|
|
326
|
+
return false
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Default: no hit
|
|
331
|
+
return false
|
|
332
|
+
}
|
|
333
|
+
|
|
170
334
|
override toJSON(): RuntimeApiModelSchema {
|
|
171
335
|
const base = super.toJSON() as RuntimeApiModelSchema
|
|
172
336
|
|
|
@@ -2,12 +2,47 @@ import type { Action } from '../actions/Action.js'
|
|
|
2
2
|
import type { ApiModel } from '../ApiModel.js'
|
|
3
3
|
import type { ExposedEntity } from '../ExposedEntity.js'
|
|
4
4
|
|
|
5
|
+
export enum AccessRuleExecutionPhase {
|
|
6
|
+
/**
|
|
7
|
+
* The action that happens before the fetch.
|
|
8
|
+
* For example MatchUserRole and MatchUserProperty are executed in this phase.
|
|
9
|
+
*/
|
|
10
|
+
PRE_FETCH = 'pre-fetch',
|
|
11
|
+
/**
|
|
12
|
+
* The action that happens during the fetch, for example injecting clauses into a query.
|
|
13
|
+
*/
|
|
14
|
+
FETCH = 'fetch',
|
|
15
|
+
/**
|
|
16
|
+
* The action that happens after the resource is fetched from the database.
|
|
17
|
+
*/
|
|
18
|
+
POST_FETCH = 'post-fetch',
|
|
19
|
+
}
|
|
20
|
+
|
|
5
21
|
export interface AccessRuleSchema {
|
|
6
22
|
/**
|
|
7
23
|
* The unique identifier for the access rule.
|
|
8
24
|
* This is used to reference the rule in the API configuration.
|
|
9
25
|
*/
|
|
10
26
|
type: string
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Whether this rule is mandatory. If true, the rule must be satisfied for the action to be allowed,
|
|
30
|
+
* regardless of other rules. If a mandatory rule is not satisfied, the action is denied.
|
|
31
|
+
*/
|
|
32
|
+
mandatory?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The definition of when exactly the access rule should be evaluated.
|
|
37
|
+
* If the action is not specified, the rule is not evaluated for that action.
|
|
38
|
+
*/
|
|
39
|
+
export interface AccessRuleExecutionMetadata {
|
|
40
|
+
readonly list?: AccessRuleExecutionPhase
|
|
41
|
+
readonly create?: AccessRuleExecutionPhase
|
|
42
|
+
readonly search?: AccessRuleExecutionPhase
|
|
43
|
+
readonly read?: AccessRuleExecutionPhase
|
|
44
|
+
readonly update?: AccessRuleExecutionPhase
|
|
45
|
+
readonly delete?: AccessRuleExecutionPhase
|
|
11
46
|
}
|
|
12
47
|
|
|
13
48
|
/**
|
|
@@ -17,16 +52,49 @@ export class AccessRule extends EventTarget implements AccessRuleSchema {
|
|
|
17
52
|
readonly type: string
|
|
18
53
|
readonly parent: ExposedEntity | ApiModel | Action
|
|
19
54
|
|
|
20
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Whether this rule is mandatory. If true, the rule must be satisfied for the action to be allowed,
|
|
57
|
+
* regardless of other rules. If a mandatory rule is not satisfied, the action is denied.
|
|
58
|
+
*/
|
|
59
|
+
mandatory: boolean
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The execution phase dictates when the rule is evaluated during the request lifecycle.
|
|
63
|
+
* - 'pre-fetch': Evaluated before the underlying resource is fetched from the database.
|
|
64
|
+
* - 'post-fetch': Evaluated after the underlying resource is fetched from the database.
|
|
65
|
+
*
|
|
66
|
+
* In some situations (like List and MatchResourceOwner) this is executed when a query is
|
|
67
|
+
* build to inject additional clauses for example.
|
|
68
|
+
*/
|
|
69
|
+
#metadata: AccessRuleExecutionMetadata
|
|
70
|
+
|
|
71
|
+
get metadata(): AccessRuleExecutionMetadata {
|
|
72
|
+
if (!this.#metadata) {
|
|
73
|
+
throw new Error('Metadata not set for access rule ' + this.type)
|
|
74
|
+
}
|
|
75
|
+
return this.#metadata
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
constructor(
|
|
79
|
+
parent: ExposedEntity | ApiModel | Action,
|
|
80
|
+
metadata: AccessRuleExecutionMetadata,
|
|
81
|
+
state: Partial<AccessRuleSchema> = {}
|
|
82
|
+
) {
|
|
21
83
|
super()
|
|
22
84
|
this.parent = parent
|
|
23
85
|
this.type = state.type ?? ''
|
|
86
|
+
this.mandatory = state.mandatory ?? false
|
|
87
|
+
this.#metadata = metadata
|
|
24
88
|
}
|
|
25
89
|
|
|
26
90
|
toJSON(): AccessRuleSchema {
|
|
27
|
-
|
|
91
|
+
const result: AccessRuleSchema = {
|
|
28
92
|
type: this.type,
|
|
29
93
|
}
|
|
94
|
+
if (this.mandatory) {
|
|
95
|
+
result.mandatory = true
|
|
96
|
+
}
|
|
97
|
+
return result
|
|
30
98
|
}
|
|
31
99
|
|
|
32
100
|
notifyChange() {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Action } from '../actions/Action.js'
|
|
2
2
|
import type { ApiModel } from '../ApiModel.js'
|
|
3
3
|
import type { ExposedEntity } from '../ExposedEntity.js'
|
|
4
|
-
import { AccessRule, type AccessRuleSchema } from './AccessRule.js'
|
|
4
|
+
import { AccessRule, AccessRuleExecutionPhase, type AccessRuleSchema } from './AccessRule.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The action is allowed for any authenticated user.
|
|
@@ -21,7 +21,18 @@ export class AllowAuthenticatedAccessRule extends AccessRule implements AllowAut
|
|
|
21
21
|
override readonly type: 'allowAuthenticated'
|
|
22
22
|
|
|
23
23
|
constructor(parent: ExposedEntity | ApiModel | Action, state: Partial<AllowAuthenticatedAccessRuleSchema> = {}) {
|
|
24
|
-
super(
|
|
24
|
+
super(
|
|
25
|
+
parent,
|
|
26
|
+
{
|
|
27
|
+
list: AccessRuleExecutionPhase.PRE_FETCH,
|
|
28
|
+
create: AccessRuleExecutionPhase.PRE_FETCH,
|
|
29
|
+
search: AccessRuleExecutionPhase.PRE_FETCH,
|
|
30
|
+
read: AccessRuleExecutionPhase.PRE_FETCH,
|
|
31
|
+
update: AccessRuleExecutionPhase.PRE_FETCH,
|
|
32
|
+
delete: AccessRuleExecutionPhase.PRE_FETCH,
|
|
33
|
+
},
|
|
34
|
+
state
|
|
35
|
+
)
|
|
25
36
|
this.type = 'allowAuthenticated'
|
|
26
37
|
}
|
|
27
38
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Action } from '../actions/Action.js'
|
|
2
2
|
import type { ApiModel } from '../ApiModel.js'
|
|
3
3
|
import type { ExposedEntity } from '../ExposedEntity.js'
|
|
4
|
-
import { AccessRule, type AccessRuleSchema } from './AccessRule.js'
|
|
4
|
+
import { AccessRule, AccessRuleExecutionPhase, type AccessRuleSchema } from './AccessRule.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The action is allowed for all users, including unauthenticated ones.
|
|
@@ -21,7 +21,18 @@ export class AllowPublicAccessRule extends AccessRule implements AllowPublicAcce
|
|
|
21
21
|
override readonly type: 'allowPublic'
|
|
22
22
|
|
|
23
23
|
constructor(parent: ExposedEntity | ApiModel | Action, state: Partial<AllowPublicAccessRuleSchema> = {}) {
|
|
24
|
-
super(
|
|
24
|
+
super(
|
|
25
|
+
parent,
|
|
26
|
+
{
|
|
27
|
+
list: AccessRuleExecutionPhase.PRE_FETCH,
|
|
28
|
+
create: AccessRuleExecutionPhase.PRE_FETCH,
|
|
29
|
+
search: AccessRuleExecutionPhase.PRE_FETCH,
|
|
30
|
+
read: AccessRuleExecutionPhase.PRE_FETCH,
|
|
31
|
+
update: AccessRuleExecutionPhase.PRE_FETCH,
|
|
32
|
+
delete: AccessRuleExecutionPhase.PRE_FETCH,
|
|
33
|
+
},
|
|
34
|
+
state
|
|
35
|
+
)
|
|
25
36
|
this.type = 'allowPublic'
|
|
26
37
|
}
|
|
27
38
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Action } from '../actions/Action.js'
|
|
2
|
+
import type { ApiModel } from '../ApiModel.js'
|
|
3
|
+
import type { ExposedEntity } from '../ExposedEntity.js'
|
|
4
|
+
import { AccessRule, AccessRuleExecutionPhase, type AccessRuleSchema } from './AccessRule.js'
|
|
5
|
+
import { observed, toRaw } from '../../decorators/observed.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A specialized rule for entities using Status Semantics;
|
|
9
|
+
* ensures specific status records (e.g. "Archived" or "Draft") are granted or restricted.
|
|
10
|
+
*
|
|
11
|
+
* Since the data domain requires for an entity to have at the most one Status semantic, it is
|
|
12
|
+
* clear which field to use.
|
|
13
|
+
*/
|
|
14
|
+
export interface LifecycleStatusAccessRuleSchema extends AccessRuleSchema {
|
|
15
|
+
type: 'lifecycleStatus'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The statuses that are allowed access. If the resource's status is not in this list,
|
|
19
|
+
* access will be denied by this rule (or un-handled if permission phase).
|
|
20
|
+
*/
|
|
21
|
+
allowedStatuses?: string[]
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The statuses that are explicitly denied access. If the resource's status is in this list,
|
|
25
|
+
* access will be denied.
|
|
26
|
+
*/
|
|
27
|
+
deniedStatuses?: string[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A specialized rule for entities using Status Semantics;
|
|
32
|
+
* ensures specific status records (e.g. "Archived" or "Draft") are granted or restricted.
|
|
33
|
+
*/
|
|
34
|
+
export class LifecycleStatusAccessRule extends AccessRule implements LifecycleStatusAccessRuleSchema {
|
|
35
|
+
override readonly type: 'lifecycleStatus'
|
|
36
|
+
|
|
37
|
+
@observed({ deep: true }) accessor allowedStatuses: string[]
|
|
38
|
+
@observed({ deep: true }) accessor deniedStatuses: string[]
|
|
39
|
+
|
|
40
|
+
constructor(parent: ExposedEntity | ApiModel | Action, state: Partial<LifecycleStatusAccessRuleSchema> = {}) {
|
|
41
|
+
super(
|
|
42
|
+
parent,
|
|
43
|
+
{
|
|
44
|
+
list: AccessRuleExecutionPhase.FETCH,
|
|
45
|
+
search: AccessRuleExecutionPhase.FETCH,
|
|
46
|
+
read: AccessRuleExecutionPhase.POST_FETCH,
|
|
47
|
+
update: AccessRuleExecutionPhase.POST_FETCH,
|
|
48
|
+
delete: AccessRuleExecutionPhase.POST_FETCH,
|
|
49
|
+
},
|
|
50
|
+
state
|
|
51
|
+
)
|
|
52
|
+
this.type = 'lifecycleStatus'
|
|
53
|
+
this.allowedStatuses = state.allowedStatuses ? [...state.allowedStatuses] : []
|
|
54
|
+
this.deniedStatuses = state.deniedStatuses ? [...state.deniedStatuses] : []
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override toJSON(): LifecycleStatusAccessRuleSchema {
|
|
58
|
+
const json: LifecycleStatusAccessRuleSchema = {
|
|
59
|
+
...(super.toJSON() as LifecycleStatusAccessRuleSchema),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (this.allowedStatuses.length > 0) {
|
|
63
|
+
json.allowedStatuses = structuredClone(toRaw(this, this.allowedStatuses)) as string[]
|
|
64
|
+
}
|
|
65
|
+
if (this.deniedStatuses.length > 0) {
|
|
66
|
+
json.deniedStatuses = structuredClone(toRaw(this, this.deniedStatuses)) as string[]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return json
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Action } from '../actions/Action.js'
|
|
2
2
|
import type { ApiModel } from '../ApiModel.js'
|
|
3
3
|
import type { ExposedEntity } from '../ExposedEntity.js'
|
|
4
|
-
import { AccessRule, type AccessRuleSchema } from './AccessRule.js'
|
|
4
|
+
import { AccessRule, AccessRuleExecutionPhase, type AccessRuleSchema } from './AccessRule.js'
|
|
5
5
|
import { observed, toRaw } from '../../decorators/observed.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -28,7 +28,18 @@ export class MatchEmailDomainAccessRule extends AccessRule implements MatchEmail
|
|
|
28
28
|
@observed({ deep: true }) accessor domains: string[]
|
|
29
29
|
|
|
30
30
|
constructor(parent: ExposedEntity | ApiModel | Action, state: Partial<MatchEmailDomainAccessRuleSchema> = {}) {
|
|
31
|
-
super(
|
|
31
|
+
super(
|
|
32
|
+
parent,
|
|
33
|
+
{
|
|
34
|
+
list: AccessRuleExecutionPhase.PRE_FETCH,
|
|
35
|
+
create: AccessRuleExecutionPhase.PRE_FETCH,
|
|
36
|
+
search: AccessRuleExecutionPhase.PRE_FETCH,
|
|
37
|
+
read: AccessRuleExecutionPhase.PRE_FETCH,
|
|
38
|
+
update: AccessRuleExecutionPhase.PRE_FETCH,
|
|
39
|
+
delete: AccessRuleExecutionPhase.PRE_FETCH,
|
|
40
|
+
},
|
|
41
|
+
state
|
|
42
|
+
)
|
|
32
43
|
this.type = 'matchEmailDomain'
|
|
33
44
|
this.domains = state.domains ? [...state.domains] : []
|
|
34
45
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Action } from '../actions/Action.js'
|
|
2
|
+
import type { ApiModel } from '../ApiModel.js'
|
|
3
|
+
import type { ExposedEntity } from '../ExposedEntity.js'
|
|
4
|
+
import { AccessRule, AccessRuleExecutionPhase, type AccessRuleSchema } from './AccessRule.js'
|
|
5
|
+
import { observed } from '../../decorators/observed.js'
|
|
6
|
+
|
|
7
|
+
export type MatchResourceAttributeOperator =
|
|
8
|
+
| 'equal'
|
|
9
|
+
| 'notEqual'
|
|
10
|
+
| 'startsWith'
|
|
11
|
+
| 'endsWith'
|
|
12
|
+
| 'contains'
|
|
13
|
+
| 'greaterThan'
|
|
14
|
+
| 'lessThan'
|
|
15
|
+
| 'greaterThanOrEqual'
|
|
16
|
+
| 'lessThanOrEqual'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Grants access based on a static value within the resource itself.
|
|
20
|
+
* Example: Allow Read behavior if status == 'published'.
|
|
21
|
+
*/
|
|
22
|
+
export interface MatchResourceAttributeAccessRuleSchema extends AccessRuleSchema {
|
|
23
|
+
type: 'matchResourceAttribute'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The name of the attribute on the resource to check.
|
|
27
|
+
*/
|
|
28
|
+
attribute: string
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The static value to match against the resource's attribute.
|
|
32
|
+
*/
|
|
33
|
+
value: string | number | boolean
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The operator to use when comparing the resource's attribute to the given value.
|
|
37
|
+
* Defaults to 'equal'.
|
|
38
|
+
*/
|
|
39
|
+
operator?: MatchResourceAttributeOperator
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Grants access based on a static value within the resource itself.
|
|
44
|
+
* Example: Allow Read behavior if status == 'published'.
|
|
45
|
+
*/
|
|
46
|
+
export class MatchResourceAttributeAccessRule extends AccessRule implements MatchResourceAttributeAccessRuleSchema {
|
|
47
|
+
override readonly type: 'matchResourceAttribute'
|
|
48
|
+
|
|
49
|
+
@observed() accessor attribute: string
|
|
50
|
+
@observed() accessor value: string | number | boolean
|
|
51
|
+
@observed() accessor operator: MatchResourceAttributeOperator
|
|
52
|
+
|
|
53
|
+
constructor(parent: ExposedEntity | ApiModel | Action, state: Partial<MatchResourceAttributeAccessRuleSchema> = {}) {
|
|
54
|
+
super(
|
|
55
|
+
parent,
|
|
56
|
+
{
|
|
57
|
+
list: AccessRuleExecutionPhase.FETCH,
|
|
58
|
+
search: AccessRuleExecutionPhase.FETCH,
|
|
59
|
+
read: AccessRuleExecutionPhase.POST_FETCH,
|
|
60
|
+
update: AccessRuleExecutionPhase.POST_FETCH,
|
|
61
|
+
delete: AccessRuleExecutionPhase.POST_FETCH,
|
|
62
|
+
},
|
|
63
|
+
state
|
|
64
|
+
)
|
|
65
|
+
this.type = 'matchResourceAttribute'
|
|
66
|
+
this.attribute = state.attribute ?? ''
|
|
67
|
+
this.value = state.value ?? ''
|
|
68
|
+
this.operator = state.operator ?? 'equal'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
override toJSON(): MatchResourceAttributeAccessRuleSchema {
|
|
72
|
+
const json: MatchResourceAttributeAccessRuleSchema = {
|
|
73
|
+
...(super.toJSON() as MatchResourceAttributeAccessRuleSchema),
|
|
74
|
+
attribute: this.attribute,
|
|
75
|
+
value: this.value,
|
|
76
|
+
}
|
|
77
|
+
if (this.operator) {
|
|
78
|
+
json.operator = this.operator
|
|
79
|
+
}
|
|
80
|
+
return json
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Action } from '../actions/Action.js'
|
|
2
2
|
import type { ApiModel } from '../ApiModel.js'
|
|
3
3
|
import type { ExposedEntity } from '../ExposedEntity.js'
|
|
4
|
-
import { AccessRule, type AccessRuleSchema } from './AccessRule.js'
|
|
4
|
+
import { AccessRule, AccessRuleExecutionPhase, type AccessRuleSchema } from './AccessRule.js'
|
|
5
5
|
import { observed } from '../../decorators/observed.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -42,7 +42,17 @@ export class MatchResourceOwnerAccessRule extends AccessRule implements MatchRes
|
|
|
42
42
|
@observed() accessor target: 'property' | 'user-entity'
|
|
43
43
|
|
|
44
44
|
constructor(parent: ExposedEntity | ApiModel | Action, state: Partial<MatchResourceOwnerAccessRuleSchema> = {}) {
|
|
45
|
-
super(
|
|
45
|
+
super(
|
|
46
|
+
parent,
|
|
47
|
+
{
|
|
48
|
+
list: AccessRuleExecutionPhase.FETCH,
|
|
49
|
+
search: AccessRuleExecutionPhase.FETCH,
|
|
50
|
+
read: AccessRuleExecutionPhase.POST_FETCH,
|
|
51
|
+
update: AccessRuleExecutionPhase.POST_FETCH,
|
|
52
|
+
delete: AccessRuleExecutionPhase.POST_FETCH,
|
|
53
|
+
},
|
|
54
|
+
state
|
|
55
|
+
)
|
|
46
56
|
this.type = 'matchResourceOwner'
|
|
47
57
|
this.property = state.property
|
|
48
58
|
this.target = state.target ?? 'property'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Action } from '../actions/Action.js'
|
|
2
2
|
import type { ApiModel } from '../ApiModel.js'
|
|
3
3
|
import type { ExposedEntity } from '../ExposedEntity.js'
|
|
4
|
-
import { AccessRule, type AccessRuleSchema } from './AccessRule.js'
|
|
4
|
+
import { AccessRule, AccessRuleExecutionPhase, type AccessRuleSchema } from './AccessRule.js'
|
|
5
5
|
import { observed } from '../../decorators/observed.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -31,7 +31,18 @@ export class MatchUserPropertyAccessRule extends AccessRule implements MatchUser
|
|
|
31
31
|
@observed() accessor value: string
|
|
32
32
|
|
|
33
33
|
constructor(parent: ExposedEntity | ApiModel | Action, state: Partial<MatchUserPropertyAccessRuleSchema> = {}) {
|
|
34
|
-
super(
|
|
34
|
+
super(
|
|
35
|
+
parent,
|
|
36
|
+
{
|
|
37
|
+
list: AccessRuleExecutionPhase.PRE_FETCH,
|
|
38
|
+
create: AccessRuleExecutionPhase.PRE_FETCH,
|
|
39
|
+
search: AccessRuleExecutionPhase.PRE_FETCH,
|
|
40
|
+
read: AccessRuleExecutionPhase.PRE_FETCH,
|
|
41
|
+
update: AccessRuleExecutionPhase.PRE_FETCH,
|
|
42
|
+
delete: AccessRuleExecutionPhase.PRE_FETCH,
|
|
43
|
+
},
|
|
44
|
+
state
|
|
45
|
+
)
|
|
35
46
|
this.type = 'matchUserProperty'
|
|
36
47
|
this.property = state.property ?? ''
|
|
37
48
|
this.value = state.value ?? ''
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Action } from '../actions/Action.js'
|
|
2
2
|
import type { ApiModel } from '../ApiModel.js'
|
|
3
3
|
import type { ExposedEntity } from '../ExposedEntity.js'
|
|
4
|
-
import { AccessRule, type AccessRuleSchema } from './AccessRule.js'
|
|
4
|
+
import { AccessRule, AccessRuleExecutionPhase, type AccessRuleSchema } from './AccessRule.js'
|
|
5
5
|
import { observed, toRaw } from '../../decorators/observed.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -32,7 +32,18 @@ export class MatchUserRoleAccessRule extends AccessRule implements MatchUserRole
|
|
|
32
32
|
@observed({ deep: true }) accessor role: string[]
|
|
33
33
|
|
|
34
34
|
constructor(parent: ExposedEntity | ApiModel | Action, state: Partial<MatchUserRoleAccessRuleSchema> = {}) {
|
|
35
|
-
super(
|
|
35
|
+
super(
|
|
36
|
+
parent,
|
|
37
|
+
{
|
|
38
|
+
list: AccessRuleExecutionPhase.PRE_FETCH,
|
|
39
|
+
create: AccessRuleExecutionPhase.PRE_FETCH,
|
|
40
|
+
search: AccessRuleExecutionPhase.PRE_FETCH,
|
|
41
|
+
read: AccessRuleExecutionPhase.PRE_FETCH,
|
|
42
|
+
update: AccessRuleExecutionPhase.PRE_FETCH,
|
|
43
|
+
delete: AccessRuleExecutionPhase.PRE_FETCH,
|
|
44
|
+
},
|
|
45
|
+
state
|
|
46
|
+
)
|
|
36
47
|
this.type = 'matchUserRole'
|
|
37
48
|
this.role = state.role ? [...state.role] : []
|
|
38
49
|
}
|