Skip to content

Commit 8146290

Browse files
committed
Refactor Privileges to remove the need to pass session
1 parent 66d32b6 commit 8146290

File tree

9 files changed

+368
-489
lines changed

9 files changed

+368
-489
lines changed

src/components/authorization/policy/executor/all-permissions-view.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ import {
1616
type ChildSingleAction,
1717
type PropAction,
1818
} from '../actions';
19-
import { type UserEdgePrivileges } from './user-edge-privileges';
20-
import { type UserResourcePrivileges } from './user-resource-privileges';
19+
import { type EdgePrivileges } from './edge-privileges';
20+
import { type ResourcePrivileges } from './resource-privileges';
2121

2222
export type AllPermissionsView<TResourceStatic extends ResourceShape<any>> =
2323
Record<
@@ -34,7 +34,7 @@ export const createAllPermissionsView = <
3434
TResourceStatic extends ResourceShape<any>,
3535
>(
3636
resource: EnhancedResource<TResourceStatic>,
37-
privileges: UserResourcePrivileges<TResourceStatic>,
37+
privileges: ResourcePrivileges<TResourceStatic>,
3838
) =>
3939
createLazyRecord<AllPermissionsView<TResourceStatic>>({
4040
getKeys: () => [...resource.securedPropsPlusExtra, ...resource.childKeys],
@@ -67,7 +67,7 @@ export const createAllPermissionsOfEdgeView = <
6767
TAction extends string,
6868
>(
6969
resource: EnhancedResource<TResourceStatic>,
70-
privileges: UserEdgePrivileges<TResourceStatic, TKey, TAction>,
70+
privileges: EdgePrivileges<TResourceStatic, TKey, TAction>,
7171
) =>
7272
createLazyRecord<Record<TAction, boolean>>({
7373
getKeys: () => [],
Lines changed: 105 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
1-
import { EnhancedResource, type ResourceShape, type Session } from '~/common';
1+
import { LazyGetter as Once } from 'lazy-get-decorator';
2+
import {
3+
EnhancedResource,
4+
type ResourceShape,
5+
UnauthorizedException,
6+
} from '~/common';
27
import { type ResourceObjectContext } from '../object.type';
3-
import { type PolicyExecutor } from './policy-executor';
4-
import { UserEdgePrivileges } from './user-edge-privileges';
8+
import {
9+
type AllPermissionsOfEdgeView,
10+
createAllPermissionsOfEdgeView,
11+
} from './all-permissions-view';
12+
import {
13+
type FilterOptions,
14+
type PolicyExecutor,
15+
type ResolveParams,
16+
} from './policy-executor';
517

618
export class EdgePrivileges<
719
TResourceStatic extends ResourceShape<any>,
@@ -12,18 +24,105 @@ export class EdgePrivileges<
1224
constructor(
1325
resource: TResourceStatic | EnhancedResource<TResourceStatic>,
1426
readonly key: TKey,
27+
private readonly object: ResourceObjectContext<TResourceStatic> | undefined,
1528
private readonly policyExecutor: PolicyExecutor,
1629
) {
1730
this.resource = EnhancedResource.of(resource);
1831
}
1932

20-
forUser(session: Session, object?: ResourceObjectContext<TResourceStatic>) {
21-
return new UserEdgePrivileges<TResourceStatic, TKey, TAction>(
33+
/** @deprecated */
34+
get session() {
35+
return this.policyExecutor.sessionHost.current;
36+
}
37+
38+
/** @deprecated Use {@link forContext} instead */
39+
forUser(_session: unknown, object?: ResourceObjectContext<TResourceStatic>) {
40+
return object ? this.forContext(object) : this;
41+
}
42+
43+
get context() {
44+
return this.object;
45+
}
46+
47+
forContext(object: ResourceObjectContext<TResourceStatic>) {
48+
if (object === this.object) {
49+
return this;
50+
}
51+
return new EdgePrivileges(
2252
this.resource,
2353
this.key,
2454
object,
25-
session,
2655
this.policyExecutor,
2756
);
2857
}
58+
59+
can(action: TAction) {
60+
const perm = this.policyExecutor.resolve({
61+
action,
62+
resource: this.resource,
63+
prop: this.key,
64+
});
65+
return perm === true || perm === false
66+
? perm
67+
: perm.isAllowed({
68+
object: this.object,
69+
resource: this.resource,
70+
session: this.policyExecutor.sessionHost.current,
71+
});
72+
}
73+
74+
verifyCan(action: TAction) {
75+
if (this.can(action)) {
76+
return;
77+
}
78+
throw UnauthorizedException.fromPrivileges(
79+
action,
80+
this.object,
81+
this.resource,
82+
this.key,
83+
);
84+
}
85+
86+
/**
87+
* An alternative view that gives an object with all the permissions for
88+
* actions.
89+
* @example
90+
* const privileges = Privileges.forEdge(User, 'email');
91+
* if (privileges.all.read) {
92+
* // can read
93+
* }
94+
*/
95+
@Once()
96+
get all(): AllPermissionsOfEdgeView<TAction> {
97+
return createAllPermissionsOfEdgeView(this.resource, this);
98+
}
99+
100+
/**
101+
* Applies a filter to the `node` so that only readable nodes continue based on our polices.
102+
* This requires `node` & `project` to be defined where this cypher snippet
103+
* is inserted.
104+
*/
105+
filterToReadable(options?: FilterOptions) {
106+
return this.dbFilter({
107+
action: 'read',
108+
...options,
109+
});
110+
}
111+
112+
dbFilter(options: FilterOptions & Pick<ResolveParams, 'action'>) {
113+
return this.policyExecutor.cypherFilter({
114+
...options,
115+
resource: this.resource,
116+
prop: this.key,
117+
});
118+
}
29119
}
120+
121+
/**
122+
* @deprecated Use {@link EdgePrivileges} instead.
123+
*/
124+
export type UserEdgePrivileges<
125+
TResourceStatic extends ResourceShape<any>,
126+
TKey extends string,
127+
TAction extends string,
128+
> = EdgePrivileges<TResourceStatic, TKey, TAction>;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export interface FilterOptions {
4040
@Injectable()
4141
export class PolicyExecutor {
4242
constructor(
43-
private readonly sessionHost: SessionHost,
43+
readonly sessionHost: SessionHost,
4444
private readonly policyFactory: PolicyFactory,
4545
@Inject(forwardRef(() => ConditionOptimizer))
4646
private readonly conditionOptimizer: ConditionOptimizer & {},

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

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import {
55
EnhancedResource,
66
type ResourceShape,
77
type SecuredPropsPlusExtraKey,
8-
type Session,
98
} from '~/common';
9+
import { SessionHost } from '../../../authentication/session.host';
10+
import type { Power } from '../../dto';
11+
import { MissingPowerException } from '../../missing-power.exception';
1012
import {
1113
type ChildListAction,
1214
type ChildSingleAction,
@@ -16,22 +18,25 @@ import { type ResourceObjectContext } from '../object.type';
1618
import { EdgePrivileges } from './edge-privileges';
1719
import { PolicyExecutor } from './policy-executor';
1820
import { ResourcePrivileges } from './resource-privileges';
19-
import { UserPrivileges } from './user-privileges';
20-
import { UserResourcePrivileges } from './user-resource-privileges';
2121

2222
@Injectable()
2323
export class Privileges {
24-
constructor(private readonly policyExecutor: PolicyExecutor) {}
24+
constructor(
25+
private readonly policyExecutor: PolicyExecutor,
26+
private readonly sessionHost: SessionHost,
27+
) {}
2528

26-
forUser(session: Session) {
27-
return new UserPrivileges(session, this.policyExecutor);
29+
/** @deprecated */
30+
forUser(_session: unknown) {
31+
return this;
2832
}
2933

3034
forResource<TResourceStatic extends ResourceShape<any>>(
3135
resource: TResourceStatic | EnhancedResource<TResourceStatic>,
3236
) {
3337
return new ResourcePrivileges<TResourceStatic>(
3438
EnhancedResource.of(resource),
39+
undefined,
3540
this.policyExecutor,
3641
);
3742
}
@@ -67,6 +72,7 @@ export class Privileges {
6772
return new EdgePrivileges(
6873
EnhancedResource.of(resource),
6974
key,
75+
undefined,
7076
this.policyExecutor,
7177
);
7278
}
@@ -75,15 +81,46 @@ export class Privileges {
7581
* Returns the privileges given the appropriate user & resource context.
7682
*/
7783
for<TResourceStatic extends ResourceShape<any>>(
78-
session: Session,
7984
resource: TResourceStatic | EnhancedResource<TResourceStatic>,
8085
object?: ResourceObjectContext<TResourceStatic>,
86+
): ResourcePrivileges<TResourceStatic>;
87+
/** @deprecated */
88+
for<TResourceStatic extends ResourceShape<any>>(
89+
_session: unknown,
90+
resource: TResourceStatic | EnhancedResource<TResourceStatic>,
91+
object?: ResourceObjectContext<TResourceStatic>,
92+
): ResourcePrivileges<TResourceStatic>;
93+
for<TResourceStatic extends ResourceShape<any>>(
94+
sessionOrRes: any,
95+
resOrCtx: any,
96+
ctx?: any,
8197
) {
82-
return new UserResourcePrivileges<TResourceStatic>(
83-
resource,
84-
object,
85-
session,
98+
const hasSession = sessionOrRes.token && sessionOrRes.anonymous != null;
99+
return new ResourcePrivileges<TResourceStatic>(
100+
hasSession ? resOrCtx : sessionOrRes,
101+
hasSession ? ctx : resOrCtx,
86102
this.policyExecutor,
87103
);
88104
}
105+
106+
/**
107+
* I think this should be replaced in-app code with `.for(X).verifyCan('create')`
108+
*/
109+
verifyPower(power: Power) {
110+
const session = this.sessionHost.current;
111+
if (!this.powers.has(power)) {
112+
throw new MissingPowerException(
113+
power,
114+
`User ${
115+
session.anonymous ? 'anon' : session.userId
116+
} does not have the requested power: ${power}`,
117+
);
118+
}
119+
}
120+
121+
get powers(): Set<Power> {
122+
const session = this.sessionHost.current;
123+
const policies = this.policyExecutor.getPolicies(session);
124+
return new Set(policies.flatMap((policy) => [...policy.powers]));
125+
}
89126
}

0 commit comments

Comments
 (0)