Skip to content

Commit a701b07

Browse files
committed
Enhance permission resolution to allow partial condition resolves
This will allow complex condition expressions to be simplified based on a resolution of certain conditions within the expression. This allows us to emit condition string for each transition by resolving only that one condition. i.e. `(Member AND Transition { Approve }) OR Sens Low` becomes `Member OR Sens Low` `(Member OR Transition { Approve }) AND Sens Low` becomes `Sens Low`
1 parent 191b9f0 commit a701b07

File tree

3 files changed

+95
-19
lines changed

3 files changed

+95
-19
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { AggregateConditions } from './aggregate.condition';
2+
import { Condition } from './condition.interface';
3+
4+
export const visitCondition = (
5+
condition: Condition,
6+
iteratee: (node: Condition) => void,
7+
): void => {
8+
iteratee(condition);
9+
if (condition instanceof AggregateConditions) {
10+
for (const c of condition.conditions) {
11+
visitCondition(c, iteratee);
12+
}
13+
}
14+
};

src/components/authorization/policy/executor/policy-executor.ts

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,16 @@ import { QueryFragment } from '~/core/database/query';
66
import { withoutScope } from '../../dto';
77
import { RoleCondition } from '../../policies/conditions/role.condition';
88
import { Permission } from '../builder/perm-granter';
9-
import { all, any, CalculatedCondition, OrConditions } from '../conditions';
9+
import {
10+
AggregateConditions,
11+
all,
12+
AndConditions,
13+
any,
14+
CalculatedCondition,
15+
Condition,
16+
OrConditions,
17+
} from '../conditions';
18+
import { visitCondition } from '../conditions/condition-visitor';
1019
import { PolicyFactory } from '../policy.factory';
1120
import { ConditionOptimizer } from './condition-optimizer';
1221

@@ -17,6 +26,10 @@ export interface ResolveParams {
1726
prop?: string;
1827
calculatedAsCondition?: boolean;
1928
optimizeConditions?: boolean;
29+
/**
30+
* A function to partially resolve conditions.
31+
*/
32+
conditionResolver?: (condition: Condition) => boolean | undefined;
2033
}
2134

2235
export interface FilterOptions {
@@ -38,6 +51,7 @@ export class PolicyExecutor {
3851
prop,
3952
calculatedAsCondition,
4053
optimizeConditions = false,
54+
conditionResolver,
4155
}: ResolveParams): Permission {
4256
if (action !== 'read') {
4357
if (prop) {
@@ -80,12 +94,20 @@ export class PolicyExecutor {
8094
if (conditions.length === 0) {
8195
return false;
8296
}
83-
const merged = OrConditions.fromAll(conditions, {
97+
let condition = OrConditions.fromAll(conditions, {
8498
optimize: optimizeConditions,
8599
});
86-
return optimizeConditions
87-
? this.conditionOptimizer.optimize(merged)
88-
: merged;
100+
if (conditionResolver) {
101+
const resolved = this.partialResolve(condition, conditionResolver);
102+
if (typeof resolved === 'boolean') {
103+
return resolved;
104+
}
105+
condition = resolved;
106+
}
107+
if (optimizeConditions) {
108+
condition = this.conditionOptimizer.optimize(condition);
109+
}
110+
return condition;
89111
}
90112

91113
forEdgeDB({
@@ -178,6 +200,54 @@ export class PolicyExecutor {
178200
};
179201
}
180202

203+
private partialResolve(
204+
condition: Condition,
205+
resolver: (condition: Condition) => boolean | undefined,
206+
): Permission {
207+
const partialResolutions = new Map<Condition, boolean>();
208+
visitCondition(condition, (cc) => {
209+
const result = resolver(cc);
210+
if (result != null) {
211+
partialResolutions.set(cc, result);
212+
}
213+
});
214+
215+
const walkAndReform = (c: Condition): Permission => {
216+
if (partialResolutions.has(c)) {
217+
return partialResolutions.get(c)!;
218+
}
219+
220+
if (!(c instanceof AggregateConditions)) {
221+
return c;
222+
}
223+
224+
let changed = false;
225+
const children = c.conditions.map((sub) => {
226+
const next = walkAndReform(sub);
227+
if (next !== sub) {
228+
changed = true;
229+
}
230+
return next;
231+
});
232+
// Only change aggregate identity if children have changed
233+
if (!changed) {
234+
return c;
235+
}
236+
if (c instanceof AndConditions) {
237+
return children.some((perm) => perm === false)
238+
? false
239+
: all(...children.filter((perm): perm is Condition => perm !== true));
240+
} else {
241+
return children.some((perm) => perm === true)
242+
? true
243+
: any(
244+
...children.filter((perm): perm is Condition => perm !== false),
245+
);
246+
}
247+
};
248+
return walkAndReform(condition);
249+
}
250+
181251
@CachedByArg({ weak: true })
182252
getPolicies(session: Session) {
183253
const policies = this.policyFactory.getPolicies().filter((policy) => {

src/components/workflow/permission.serializer.ts

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { setOf } from '@seedcompany/common';
22
import { DateTime } from 'luxon';
33
import { ID, Role, Session } from '~/common';
44
import { Privileges, UserResourcePrivileges } from '../authorization';
5-
import { Permission } from '../authorization/policy/builder/perm-granter';
65
import { Condition } from '../authorization/policy/conditions';
76
import { Workflow } from './define-workflow';
87
import { SerializedWorkflowTransitionPermission as SerializedTransitionPermission } from './dto/serialized-workflow.dto';
@@ -47,22 +46,15 @@ const resolve = (
4746
p: UserResourcePrivileges<any>,
4847
action: string,
4948
transitionKey: ID,
50-
): Permission => {
51-
const c = p.resolve({
49+
) =>
50+
p.resolve({
5251
action,
53-
calculatedAsCondition: true,
5452
optimizeConditions: true,
53+
conditionResolver: (condition) =>
54+
condition instanceof TransitionCondition
55+
? condition.allowedTransitionKeys.has(transitionKey)
56+
: undefined,
5557
});
5658

57-
if (typeof c === 'boolean') {
58-
return c;
59-
}
60-
// Cheaply resolve transition condition.
61-
if (c instanceof TransitionCondition) {
62-
return c.allowedTransitionKeys.has(transitionKey);
63-
}
64-
65-
return c;
66-
};
6759
const renderCondition = (c: boolean | Condition) =>
6860
typeof c === 'boolean' ? undefined : Condition.id(c);

0 commit comments

Comments
 (0)