1
1
import {
2
- createPermissionCheckQuery ,
2
+ createRelationship ,
3
3
parseRelationTuple ,
4
4
} from '@getlarge/keto-relations-parser' ;
5
5
import {
@@ -11,6 +11,7 @@ import {
11
11
Type ,
12
12
} from '@nestjs/common' ;
13
13
import { Reflector } from '@nestjs/core' ;
14
+ import { Relationship } from '@ory/client' ;
14
15
import type { Observable } from 'rxjs' ;
15
16
16
17
import {
@@ -19,19 +20,33 @@ import {
19
20
} from './ory-permission-checks.decorator' ;
20
21
import { OryPermissionsService } from './ory-permissions' ;
21
22
23
+ type EvaluationResult = {
24
+ results : {
25
+ [ tuple : string ] : {
26
+ allowed : boolean ;
27
+ parentType : 'AND' | 'OR' | null ;
28
+ } ;
29
+ } ;
30
+ allowed : boolean ;
31
+ } ;
32
+
22
33
export interface OryAuthorizationGuardOptions {
23
- postCheck ?: (
24
- this : IAuthorizationGuard ,
25
- relationTuple : string | string [ ] ,
26
- isPermitted : boolean
27
- ) => void ;
34
+ maxDepth : number ;
35
+ postCheck ?: ( this : IAuthorizationGuard , result : EvaluationResult ) => void ;
28
36
unauthorizedFactory : (
29
37
this : IAuthorizationGuard ,
30
38
ctx : ExecutionContext ,
31
39
error : unknown
32
40
) => Error ;
33
41
}
34
42
43
+ export type NestedCondition = {
44
+ type : 'AND' | 'OR' ;
45
+ conditions : ( boolean | NestedCondition ) [ ] ;
46
+ } ;
47
+
48
+ export type EnhancedNestedCondition = boolean | NestedCondition ;
49
+
35
50
export abstract class IAuthorizationGuard implements CanActivate {
36
51
abstract options : OryAuthorizationGuardOptions ;
37
52
abstract canActivate (
@@ -41,16 +56,14 @@ export abstract class IAuthorizationGuard implements CanActivate {
41
56
abstract evaluateConditions (
42
57
factory : EnhancedRelationTupleFactory ,
43
58
context : ExecutionContext
44
- ) : Promise < {
45
- allowed : boolean ;
46
- relationTuple : string | string [ ] ;
47
- } > ;
59
+ ) : Promise < EvaluationResult > ;
48
60
}
49
61
50
62
const defaultOptions : OryAuthorizationGuardOptions = {
51
63
unauthorizedFactory : ( ) => {
52
64
return new ForbiddenException ( ) ;
53
65
} ,
66
+ maxDepth : 3 ,
54
67
} ;
55
68
56
69
export const OryAuthorizationGuard = (
@@ -70,46 +83,118 @@ export const OryAuthorizationGuard = (
70
83
} ;
71
84
}
72
85
73
- async evaluateConditions (
86
+ private flattenConditions (
74
87
factory : EnhancedRelationTupleFactory ,
75
- context : ExecutionContext
76
- ) : Promise < {
77
- allowed : boolean ;
78
- relationTuple : string | string [ ] ;
79
- } > {
80
- if ( typeof factory === 'string' || typeof factory === 'function' ) {
81
- const { unauthorizedFactory } = this . options ;
88
+ context : ExecutionContext ,
89
+ parentType : 'AND' | 'OR' | null = null
90
+ ) : { tuple : Relationship ; relation : string ; type : 'AND' | 'OR' } [ ] {
91
+ const { unauthorizedFactory } = this . options ;
82
92
93
+ if ( typeof factory === 'string' || typeof factory === 'function' ) {
83
94
const relationTuple =
84
95
typeof factory === 'string' ? factory : factory ( context ) ;
85
- const result = createPermissionCheckQuery (
96
+ const result = createRelationship (
86
97
parseRelationTuple ( relationTuple ) . unwrapOrThrow ( )
87
98
) ;
88
99
89
100
if ( result . hasError ( ) ) {
90
101
throw unauthorizedFactory . bind ( this ) ( context , result . error ) ;
91
102
}
92
103
93
- try {
94
- const { data } = await this . oryService . checkPermission ( result . value ) ;
95
- return { allowed : data . allowed , relationTuple } ;
96
- } catch ( error ) {
97
- throw unauthorizedFactory . bind ( this ) ( context , error ) ;
98
- }
104
+ return [
105
+ {
106
+ tuple : result . value ,
107
+ relation : relationTuple ,
108
+ type : parentType || 'AND' ,
109
+ } ,
110
+ ] ;
99
111
}
100
- const evaluatedConditions = await Promise . all (
101
- factory . conditions . map ( ( cond ) => this . evaluateConditions ( cond , context ) )
112
+
113
+ return factory . conditions . flatMap ( ( cond ) =>
114
+ this . flattenConditions ( cond , context , factory . type )
102
115
) ;
103
- const results = evaluatedConditions . flatMap ( ( { allowed } ) => allowed ) ;
104
- const allowed =
105
- factory . type === 'AND' ? results . every ( Boolean ) : results . some ( Boolean ) ;
116
+ }
106
117
107
- return {
108
- allowed,
109
- relationTuple : evaluatedConditions . flatMap (
110
- ( { relationTuple } ) => relationTuple
111
- ) ,
112
- } ;
118
+ private evaluateLogicalStructure (
119
+ condition : EnhancedNestedCondition
120
+ ) : boolean {
121
+ if ( typeof condition === 'boolean' ) {
122
+ return condition ;
123
+ }
124
+ if ( condition . type === 'AND' ) {
125
+ return condition . conditions . every ( ( cond ) => {
126
+ if ( typeof cond === 'boolean' ) {
127
+ return cond ;
128
+ }
129
+ return this . evaluateLogicalStructure ( cond ) ;
130
+ } ) ;
131
+ } else if ( condition . type === 'OR' ) {
132
+ return condition . conditions . some ( ( cond ) => {
133
+ if ( typeof cond === 'boolean' ) {
134
+ return cond ;
135
+ }
136
+ return this . evaluateLogicalStructure ( cond ) ;
137
+ } ) ;
138
+ }
139
+ return false ; // Fallback, should not reach here
140
+ }
141
+
142
+ async evaluateConditions (
143
+ factory : EnhancedRelationTupleFactory ,
144
+ context : ExecutionContext
145
+ ) : Promise < EvaluationResult > {
146
+ const { unauthorizedFactory } = this . options ;
147
+ const flattenedConditions = this . flattenConditions ( factory , context ) ;
148
+ const tuples = flattenedConditions . map ( ( { tuple } ) => tuple ) ;
149
+
150
+ try {
151
+ const { data } = await this . oryService
152
+ . batchCheckPermission ( {
153
+ batchCheckPermissionBody : { tuples } ,
154
+ maxDepth : this . options . maxDepth ,
155
+ } )
156
+ . catch ( ( error ) => {
157
+ console . error ( error ) ;
158
+ throw error ;
159
+ } ) ;
160
+
161
+ const results = data . results ;
162
+ const evaluationResult : EvaluationResult = {
163
+ results : { } ,
164
+ allowed : false ,
165
+ } ;
166
+ let index = 0 ;
167
+ function replaceWithResults (
168
+ condition : EnhancedRelationTupleFactory
169
+ ) : EnhancedNestedCondition {
170
+ if (
171
+ typeof condition === 'string' ||
172
+ typeof condition === 'function'
173
+ ) {
174
+ const { allowed } = results [ index ] ;
175
+ const { relation, type } = flattenedConditions [ index ] ;
176
+ evaluationResult . results [ relation ] = {
177
+ allowed,
178
+ parentType : type ,
179
+ } ;
180
+ index ++ ;
181
+ return allowed ;
182
+ }
183
+
184
+ return {
185
+ type : condition . type ,
186
+ conditions : condition . conditions . map ( replaceWithResults ) ,
187
+ } ;
188
+ }
189
+
190
+ const logicalStructure = replaceWithResults ( factory ) ;
191
+ evaluationResult . allowed =
192
+ this . evaluateLogicalStructure ( logicalStructure ) ;
193
+
194
+ return evaluationResult ;
195
+ } catch ( error ) {
196
+ throw unauthorizedFactory . bind ( this ) ( context , error ) ;
197
+ }
113
198
}
114
199
115
200
async canActivate ( context : ExecutionContext ) : Promise < boolean > {
@@ -120,18 +205,15 @@ export const OryAuthorizationGuard = (
120
205
}
121
206
const { postCheck, unauthorizedFactory } = this . options ;
122
207
for ( const factory of factories ) {
123
- const { allowed, relationTuple } = await this . evaluateConditions (
124
- factory ,
125
- context
126
- ) ;
208
+ const evaluation = await this . evaluateConditions ( factory , context ) ;
127
209
128
210
if ( postCheck ) {
129
- postCheck . bind ( this ) ( relationTuple , allowed ) ;
211
+ postCheck . bind ( this ) ( evaluation ) ;
130
212
}
131
- if ( ! allowed ) {
213
+ if ( ! evaluation . allowed ) {
132
214
throw unauthorizedFactory . bind ( this ) (
133
215
context ,
134
- new Error ( `Unauthorized access for ${ relationTuple } ` )
216
+ new Error ( `Unauthorized access` )
135
217
) ;
136
218
}
137
219
}
0 commit comments