Skip to content

Commit 22d788c

Browse files
committed
feat: update OryAuthorizationGuard to use batchCheckPermission
1 parent c7981c6 commit 22d788c

File tree

1 file changed

+125
-43
lines changed

1 file changed

+125
-43
lines changed

packages/keto-client-wrapper/src/lib/ory-authorization.guard.ts

Lines changed: 125 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {
2-
createPermissionCheckQuery,
2+
createRelationship,
33
parseRelationTuple,
44
} from '@getlarge/keto-relations-parser';
55
import {
@@ -11,6 +11,7 @@ import {
1111
Type,
1212
} from '@nestjs/common';
1313
import { Reflector } from '@nestjs/core';
14+
import { Relationship } from '@ory/client';
1415
import type { Observable } from 'rxjs';
1516

1617
import {
@@ -19,19 +20,33 @@ import {
1920
} from './ory-permission-checks.decorator';
2021
import { OryPermissionsService } from './ory-permissions';
2122

23+
type EvaluationResult = {
24+
results: {
25+
[tuple: string]: {
26+
allowed: boolean;
27+
parentType: 'AND' | 'OR' | null;
28+
};
29+
};
30+
allowed: boolean;
31+
};
32+
2233
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;
2836
unauthorizedFactory: (
2937
this: IAuthorizationGuard,
3038
ctx: ExecutionContext,
3139
error: unknown
3240
) => Error;
3341
}
3442

43+
export type NestedCondition = {
44+
type: 'AND' | 'OR';
45+
conditions: (boolean | NestedCondition)[];
46+
};
47+
48+
export type EnhancedNestedCondition = boolean | NestedCondition;
49+
3550
export abstract class IAuthorizationGuard implements CanActivate {
3651
abstract options: OryAuthorizationGuardOptions;
3752
abstract canActivate(
@@ -41,16 +56,14 @@ export abstract class IAuthorizationGuard implements CanActivate {
4156
abstract evaluateConditions(
4257
factory: EnhancedRelationTupleFactory,
4358
context: ExecutionContext
44-
): Promise<{
45-
allowed: boolean;
46-
relationTuple: string | string[];
47-
}>;
59+
): Promise<EvaluationResult>;
4860
}
4961

5062
const defaultOptions: OryAuthorizationGuardOptions = {
5163
unauthorizedFactory: () => {
5264
return new ForbiddenException();
5365
},
66+
maxDepth: 3,
5467
};
5568

5669
export const OryAuthorizationGuard = (
@@ -70,46 +83,118 @@ export const OryAuthorizationGuard = (
7083
};
7184
}
7285

73-
async evaluateConditions(
86+
private flattenConditions(
7487
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;
8292

93+
if (typeof factory === 'string' || typeof factory === 'function') {
8394
const relationTuple =
8495
typeof factory === 'string' ? factory : factory(context);
85-
const result = createPermissionCheckQuery(
96+
const result = createRelationship(
8697
parseRelationTuple(relationTuple).unwrapOrThrow()
8798
);
8899

89100
if (result.hasError()) {
90101
throw unauthorizedFactory.bind(this)(context, result.error);
91102
}
92103

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+
];
99111
}
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)
102115
);
103-
const results = evaluatedConditions.flatMap(({ allowed }) => allowed);
104-
const allowed =
105-
factory.type === 'AND' ? results.every(Boolean) : results.some(Boolean);
116+
}
106117

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+
}
113198
}
114199

115200
async canActivate(context: ExecutionContext): Promise<boolean> {
@@ -120,18 +205,15 @@ export const OryAuthorizationGuard = (
120205
}
121206
const { postCheck, unauthorizedFactory } = this.options;
122207
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);
127209

128210
if (postCheck) {
129-
postCheck.bind(this)(relationTuple, allowed);
211+
postCheck.bind(this)(evaluation);
130212
}
131-
if (!allowed) {
213+
if (!evaluation.allowed) {
132214
throw unauthorizedFactory.bind(this)(
133215
context,
134-
new Error(`Unauthorized access for ${relationTuple}`)
216+
new Error(`Unauthorized access`)
135217
);
136218
}
137219
}

0 commit comments

Comments
 (0)