-
Notifications
You must be signed in to change notification settings - Fork 29
/
access.service.ts
144 lines (116 loc) · 4.67 KB
/
access.service.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import { Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';
import { Ability, AnyAbility, subject } from '@casl/ability';
import { AnyObject, Subject } from '@casl/ability/dist/types/types';
import { flatten } from 'flat';
import { AuthorizableRequest } from './interfaces/request.interface';
import { AbilityFactory } from './factories/ability.factory';
import { AbilityMetadata } from './interfaces/ability-metadata.interface';
import { UserProxy } from './proxies/user.proxy';
import { CaslConfig } from './casl.config';
import { AuthorizableUser } from './interfaces/authorizable-user.interface';
import { RequestProxy } from './proxies/request.proxy';
import { ConditionsProxy } from './proxies/conditions.proxy';
@Injectable()
export class AccessService {
constructor(private abilityFactory: AbilityFactory) {}
public getAbility<User extends AuthorizableUser<string, unknown> = AuthorizableUser>(user: User): AnyAbility {
return this.abilityFactory.createForUser(user);
}
public hasAbility<User extends AuthorizableUser<string, unknown> = AuthorizableUser>(
user: User,
action: string,
subject: Subject,
field?: string,
): boolean {
// No user - no access
if (!user) {
return false;
}
// User exists but no ability metadata - deny access
if (!action || !subject) {
return false;
}
const { superuserRole } = CaslConfig.getRootOptions();
const userAbilities = this.abilityFactory.createForUser(user);
// Always allow access for superuser
if (superuserRole && user.roles?.includes(superuserRole)) {
return true;
}
return userAbilities.can(action, subject, field);
}
public assertAbility<User extends AuthorizableUser<string, unknown> = AuthorizableUser>(
user: User,
action: string,
subject: Subject,
field?: string,
): void {
if (!this.hasAbility(user, action, subject, field)) {
const userAbilities = this.abilityFactory.createForUser(user, Ability);
const relatedRules = userAbilities.rulesFor(action, typeof subject === 'object' ? subject.constructor : subject);
if (relatedRules.some((rule) => rule.conditions)) {
throw new NotFoundException();
}
throw new UnauthorizedException();
}
}
public async canActivateAbility<Subject = AnyObject>(
request: AuthorizableRequest,
ability?: AbilityMetadata<Subject>,
): Promise<boolean> {
const { getUserFromRequest, superuserRole } = CaslConfig.getRootOptions();
const userProxy = new UserProxy(request, getUserFromRequest);
const req = new RequestProxy(request);
// Attempt to get user from request
const user = userProxy.getFromRequest();
// No user - no access
if (!user) {
return false;
}
// User exists but no ability metadata - deny access
if (!ability) {
return false;
}
// Always allow access for superuser
if (superuserRole && user.roles?.includes(superuserRole)) {
return true;
}
let userAbilities = this.abilityFactory.createForUser(user, Ability);
const relevantRules = userAbilities.rulesFor(ability.action, ability.subject);
req.setConditions(new ConditionsProxy(userAbilities, ability.action, ability.subject));
// If no relevant rules with conditions or no subject hook exists check against subject class
if (!relevantRules.every((rule) => rule.conditions) || !ability.subjectHook) {
return userAbilities.can(ability.action, ability.subject);
}
// Otherwise try to obtain subject
const subjectInstance = await req.getSubjectHook().run(request);
req.setSubject(subjectInstance);
if (!subjectInstance) {
return userAbilities.can(ability.action, ability.subject);
}
const finalUser = await userProxy.get();
if (finalUser && finalUser !== userProxy.getFromRequest()) {
userAbilities = this.abilityFactory.createForUser(finalUser);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const actualSubject = subject(ability.subject as any, subjectInstance);
const cannotActivateSomeField = this.isThereAnyFieldRestriction(
request.body,
ability.action,
actualSubject,
finalUser,
);
if (cannotActivateSomeField) return false;
// and match agains subject instance
return userAbilities.can(ability.action, actualSubject);
}
private isThereAnyFieldRestriction(
body: Record<string, string>,
action: string,
subject: AnyObject,
user?: AuthorizableUser<string, string>,
) {
if (!user) return true;
const subjectFields = Object.keys(flatten(body || {}));
return subjectFields.some((field) => !this.hasAbility(user, action, subject, field));
}
}