Skip to content

Commit 9f806c0

Browse files
authored
Merge pull request #7 from getlarge/feat-enable-custom-unauthorized-error-in-guards
feat: enable custom unauthorized error in guards
2 parents 91173df + 22846c6 commit 9f806c0

File tree

12 files changed

+74
-43
lines changed

12 files changed

+74
-43
lines changed

.github/workflows/ci.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ jobs:
5353
# requires kratos and keto to be running
5454
- run: npx nx affected -t lint,test,build --parallel=3
5555

56+
- uses: actions/upload-artifact@v4
57+
with:
58+
name: coverage
59+
path: coverage
60+
overwrite: true
61+
retention-days: 1
62+
5663
sonarcloud:
5764
name: SonarCloud
5865
runs-on: ubuntu-latest
@@ -66,6 +73,11 @@ jobs:
6673
- uses: martinbeentjes/[email protected]
6774
id: package-version
6875

76+
- uses: actions/download-artifact@v4
77+
with:
78+
name: coverage
79+
path: coverage
80+
6981
- name: SonarCloud Scan
7082
uses: SonarSource/sonarcloud-github-action@master
7183
env:

jest.preset.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
const nxPreset = require('@nx/jest/preset').default;
22

3-
module.exports = { ...nxPreset };
3+
module.exports = { ...nxPreset, coverageReporters: ['json', 'lcov'] };

packages/base-client-wrapper/project.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
"executor": "@nx/jest:jest",
3131
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
3232
"options": {
33-
"jestConfig": "packages/base-client-wrapper/jest.config.ts"
33+
"jestConfig": "packages/base-client-wrapper/jest.config.ts",
34+
"codeCoverage": true
3435
}
3536
}
3637
},

packages/keto-client-wrapper/project.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
"executor": "@nx/jest:jest",
3737
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
3838
"options": {
39-
"jestConfig": "packages/keto-client-wrapper/jest.config.ts"
39+
"jestConfig": "packages/keto-client-wrapper/jest.config.ts",
40+
"codeCoverage": true
4041
}
4142
},
4243
"docker-push": {

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

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
import {
66
CanActivate,
77
ExecutionContext,
8+
ForbiddenException,
89
Injectable,
910
mixin,
1011
Type,
@@ -17,10 +18,17 @@ import { OryPermissionsService } from './ory-permissions';
1718
export interface OryAuthorizationGuardOptions {
1819
errorFactory?: (error: Error) => Error;
1920
postCheck?: (relationTuple: RelationTuple, isPermitted: boolean) => void;
21+
unauthorizedFactory: (ctx: ExecutionContext, error: unknown) => Error;
2022
}
2123

24+
const defaultOptions: OryAuthorizationGuardOptions = {
25+
unauthorizedFactory: () => {
26+
return new ForbiddenException();
27+
},
28+
};
29+
2230
export const OryAuthorizationGuard = (
23-
options: OryAuthorizationGuardOptions = {}
31+
options: Partial<OryAuthorizationGuardOptions> = {}
2432
): Type<CanActivate> => {
2533
@Injectable()
2634
class AuthorizationGuard implements CanActivate {
@@ -35,22 +43,31 @@ export const OryAuthorizationGuard = (
3543
if (!factories?.length) {
3644
return true;
3745
}
46+
const { postCheck, unauthorizedFactory } = {
47+
...defaultOptions,
48+
...options,
49+
};
3850
for (const { relationTupleFactory } of factories) {
3951
const relationTuple = relationTupleFactory(context);
4052
const result = createPermissionCheckQuery(relationTuple);
4153
if (result.hasError()) {
42-
if (options.errorFactory) {
43-
throw options.errorFactory(result.error);
44-
}
45-
return false;
54+
throw unauthorizedFactory(context, result.error);
55+
}
56+
let isPermitted = false;
57+
try {
58+
const { data } = await this.oryService.checkPermission(result.value);
59+
isPermitted = data.allowed;
60+
} catch (error) {
61+
throw unauthorizedFactory(context, error);
4662
}
47-
const { data } = await this.oryService.checkPermission(result.value);
48-
const isPermitted = data.allowed;
49-
if (options.postCheck) {
50-
options.postCheck(relationTuple, isPermitted);
63+
if (postCheck) {
64+
postCheck(relationTuple, isPermitted);
5165
}
5266
if (!isPermitted) {
53-
return false;
67+
throw unauthorizedFactory(
68+
context,
69+
new Error(`Unauthorized access for ${relationTuple.toString()}`)
70+
);
5471
}
5572
}
5673
return true;

packages/keto-client-wrapper/test/keto-client-wrapper.spec.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,7 @@ describe('Keto client wrapper E2E', () => {
111111
'x-current-user-id': subjectObject,
112112
});
113113
expect(body).toEqual({
114-
error: 'Forbidden',
115-
message: 'Forbidden resource',
114+
message: 'Forbidden',
116115
statusCode: 403,
117116
});
118117
});

packages/keto-relations-parser/project.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
"executor": "@nx/jest:jest",
3636
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
3737
"options": {
38-
"jestConfig": "packages/keto-relations-parser/jest.config.ts"
38+
"jestConfig": "packages/keto-relations-parser/jest.config.ts",
39+
"codeCoverage": true
3940
}
4041
}
4142
},

packages/keto-relations-parser/src/lib/relation-tuple-parser.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { error, Result, value } from 'defekt';
22

33
import { RelationTupleSyntaxError } from './errors/relation-tuple-syntax.error';
44
import { UnknownError } from './errors/unknown.error';
5-
import { RelationTuple } from './relation-tuple';
5+
import { type IRelationTuple, RelationTuple } from './relation-tuple';
66

77
type Namespace = string;
88
type TupleObject = string;
@@ -102,12 +102,7 @@ export function parseRelationTuple(
102102
] = match.map((str) => str?.trim() ?? '');
103103

104104
try {
105-
const result: RelationTuple = {
106-
namespace: namespace,
107-
object: object,
108-
relation: relation,
109-
subjectIdOrSet: '',
110-
};
105+
const result = new RelationTuple(namespace, object, relation, '');
111106

112107
if (subjectRelation) {
113108
result.subjectIdOrSet = {
@@ -142,7 +137,7 @@ export function parseRelationTuple(
142137
* @returns
143138
*/
144139
export const relationTupleToString = (
145-
tuple: Partial<RelationTuple>
140+
tuple: Partial<IRelationTuple>
146141
): RelationTupleString => {
147142
const base: `${string}:${string}#${string}` = `${tuple.namespace}:${tuple.object}#${tuple.relation}`;
148143
if (!tuple.subjectIdOrSet) {

packages/kratos-client-wrapper/project.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@
4343
"executor": "@nx/jest:jest",
4444
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
4545
"options": {
46-
"jestConfig": "packages/kratos-client-wrapper/jest.config.ts"
46+
"jestConfig": "packages/kratos-client-wrapper/jest.config.ts",
47+
"codeCoverage": true
4748
}
4849
},
4950
"docker-push": {

packages/kratos-client-wrapper/src/lib/ory-authentication.guard.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import {
22
CanActivate,
33
ExecutionContext,
44
Injectable,
5-
Logger,
65
mixin,
76
Type,
7+
UnauthorizedException,
88
} from '@nestjs/common';
99
import type { Session } from '@ory/client';
1010

@@ -18,6 +18,7 @@ export interface OryAuthenticationGuardOptions {
1818
ctx: ExecutionContext,
1919
session: Session
2020
) => void | Promise<void>;
21+
unauthorizedFactory: (ctx: ExecutionContext, error: unknown) => Error;
2122
}
2223

2324
const defaultOptions: OryAuthenticationGuardOptions = {
@@ -30,15 +31,16 @@ const defaultOptions: OryAuthenticationGuardOptions = {
3031
.getRequest()
3132
?.headers?.authorization?.replace('Bearer ', ''),
3233
cookieResolver: (ctx) => ctx.switchToHttp().getRequest()?.headers?.cookie,
34+
unauthorizedFactory() {
35+
return new UnauthorizedException();
36+
},
3337
};
3438

3539
export const OryAuthenticationGuard = (
3640
options: Partial<OryAuthenticationGuardOptions> = defaultOptions
3741
): Type<CanActivate> => {
3842
@Injectable()
3943
class AuthenticationGuard implements CanActivate {
40-
readonly logger = new Logger(AuthenticationGuard.name);
41-
4244
constructor(readonly oryService: OryFrontendService) {}
4345

4446
async canActivate(context: ExecutionContext): Promise<boolean> {
@@ -47,29 +49,32 @@ export const OryAuthenticationGuard = (
4749
sessionTokenResolver,
4850
isValidSession,
4951
postValidationHook,
52+
unauthorizedFactory,
5053
} = {
5154
...defaultOptions,
5255
...options,
5356
};
5457

58+
let session: Session;
5559
try {
5660
const cookie = cookieResolver(context);
5761
const xSessionToken = sessionTokenResolver(context);
58-
const { data: session } = await this.oryService.toSession({
62+
const { data } = await this.oryService.toSession({
5963
cookie,
6064
xSessionToken,
6165
});
62-
if (!isValidSession(session)) {
63-
return false;
64-
}
65-
if (typeof postValidationHook === 'function') {
66-
await postValidationHook(context, session);
67-
}
68-
return true;
66+
session = data;
6967
} catch (error) {
70-
this.logger.error(error);
71-
return false;
68+
throw unauthorizedFactory(context, error);
69+
}
70+
71+
if (!isValidSession(session)) {
72+
throw unauthorizedFactory(context, new Error('Invalid session'));
73+
}
74+
if (typeof postValidationHook === 'function') {
75+
await postValidationHook(context, session);
7276
}
77+
return true;
7378
}
7479
}
7580
return mixin(AuthenticationGuard);

0 commit comments

Comments
 (0)