@owlmeans/server-iam 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/append.d.ts +19 -0
- package/build/append.d.ts.map +1 -0
- package/build/append.js +24 -0
- package/build/append.js.map +1 -0
- package/build/gate.d.ts +27 -0
- package/build/gate.d.ts.map +1 -0
- package/build/gate.js +76 -0
- package/build/gate.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -0
- package/build/index.js.map +1 -1
- package/package.json +8 -3
- package/src/append.ts +31 -0
- package/src/gate.ts +100 -0
- package/src/index.ts +3 -0
- package/tests/gate.spec.ts +75 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ServerConfig, ServerContext } from '@owlmeans/server-context';
|
|
2
|
+
import type { WithSharedConfig } from '@owlmeans/oidc';
|
|
3
|
+
type IamServerConfig = ServerConfig & WithSharedConfig;
|
|
4
|
+
type IamServerContext<C extends IamServerConfig = IamServerConfig> = ServerContext<C>;
|
|
5
|
+
/**
|
|
6
|
+
* One-call OIDC RP setup for a server context.
|
|
7
|
+
*
|
|
8
|
+
* Replaces the explicit wiring pattern in the target template:
|
|
9
|
+
* context.registerService(makeOidcClientService())
|
|
10
|
+
* context.registerService(makeOidcWrappingService())
|
|
11
|
+
* context.registerService(makeOidcGate())
|
|
12
|
+
* appendOidcGuard(context)
|
|
13
|
+
*
|
|
14
|
+
* The OIDC provider config (discovery URL, clientId, secret) is read from
|
|
15
|
+
* context.cfg.oidc.providers[] — populate it before calling appendIam().
|
|
16
|
+
*/
|
|
17
|
+
export declare const appendIam: <C extends IamServerConfig, T extends IamServerContext<C>>(context: T) => T;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=append.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"append.d.ts","sourceRoot":"","sources":["../src/append.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAC3E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAItD,KAAK,eAAe,GAAG,YAAY,GAAG,gBAAgB,CAAA;AACtD,KAAK,gBAAgB,CAAC,CAAC,SAAS,eAAe,GAAG,eAAe,IAAI,aAAa,CAAC,CAAC,CAAC,CAAA;AAErF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,SAAS,GAAI,CAAC,SAAS,eAAe,EAAE,CAAC,SAAS,gBAAgB,CAAC,CAAC,CAAC,EAChF,SAAS,CAAC,KACT,CAQF,CAAA"}
|
package/build/append.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { appendOidcGuard, makeOidcClientService, makeOidcWrappingService } from '@owlmeans/server-oidc-rp';
|
|
2
|
+
import { makeIamGate } from './gate.js';
|
|
3
|
+
/**
|
|
4
|
+
* One-call OIDC RP setup for a server context.
|
|
5
|
+
*
|
|
6
|
+
* Replaces the explicit wiring pattern in the target template:
|
|
7
|
+
* context.registerService(makeOidcClientService())
|
|
8
|
+
* context.registerService(makeOidcWrappingService())
|
|
9
|
+
* context.registerService(makeOidcGate())
|
|
10
|
+
* appendOidcGuard(context)
|
|
11
|
+
*
|
|
12
|
+
* The OIDC provider config (discovery URL, clientId, secret) is read from
|
|
13
|
+
* context.cfg.oidc.providers[] — populate it before calling appendIam().
|
|
14
|
+
*/
|
|
15
|
+
export const appendIam = (context) => {
|
|
16
|
+
context.registerService(makeOidcClientService());
|
|
17
|
+
context.registerService(makeOidcWrappingService());
|
|
18
|
+
// IAM gate under the OIDC_GATE alias: claims-first (integrated mode),
|
|
19
|
+
// UMA2 fallback byte-equivalent to makeOidcGate (keycloak mode)
|
|
20
|
+
context.registerService(makeIamGate());
|
|
21
|
+
appendOidcGuard(context);
|
|
22
|
+
return context;
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=append.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"append.js","sourceRoot":"","sources":["../src/append.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAA;AAC1G,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAKvC;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CACvB,OAAU,EACP,EAAE;IACL,OAAO,CAAC,eAAe,CAAC,qBAAqB,EAAE,CAAC,CAAA;IAChD,OAAO,CAAC,eAAe,CAAC,uBAAuB,EAAE,CAAC,CAAA;IAClD,sEAAsE;IACtE,gEAAgE;IAChE,OAAO,CAAC,eAAe,CAAC,WAAW,EAAE,CAAC,CAAA;IACtC,eAAe,CAAO,OAAO,CAAC,CAAA;IAC9B,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA"}
|
package/build/gate.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { GateService } from '@owlmeans/entrypoint';
|
|
2
|
+
/**
|
|
3
|
+
* Gate-param syntax:
|
|
4
|
+
* <permission> — unscoped (project-wide) check, e.g. 'article--modify'
|
|
5
|
+
* <permission>@<requestParam> — resource-scoped: the resource id is resolved from
|
|
6
|
+
* req.params[requestParam] ?? req.query[requestParam]
|
|
7
|
+
* and must be listed in the grant's resources[]
|
|
8
|
+
* '{entity}' substitution applies to the permission part in both forms.
|
|
9
|
+
*/
|
|
10
|
+
export declare const RESOURCE_PARAM_SEPARATOR = "@";
|
|
11
|
+
export interface ParsedGateParam {
|
|
12
|
+
permission: string;
|
|
13
|
+
resourceParam?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const parseGateParam: (param: string) => ParsedGateParam;
|
|
16
|
+
/**
|
|
17
|
+
* IAM gate: claims-first with UMA2 fallback. Registered under the OIDC_GATE alias by
|
|
18
|
+
* appendIam() so target code never knows which IAM backend is active.
|
|
19
|
+
*
|
|
20
|
+
* 1. When the Auth carries a valid PermissionSet[] claim (integrated IAM mode),
|
|
21
|
+
* params are asserted locally against it — both unscoped and resource-scoped forms.
|
|
22
|
+
* 2. Otherwise (Keycloak mode — its tokens never produce a conforming claim) the
|
|
23
|
+
* @-suffixes are stripped and the check delegates to the UMA2 gate model from
|
|
24
|
+
* @owlmeans/server-oidc-rp, byte-equivalent to makeOidcGate.
|
|
25
|
+
*/
|
|
26
|
+
export declare const makeIamGate: (alias?: string) => GateService;
|
|
27
|
+
//# sourceMappingURL=gate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAmB,MAAM,sBAAsB,CAAA;AAQxE;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,MAAM,CAAA;AAE3C,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,eAAO,MAAM,cAAc,GAAI,OAAO,MAAM,KAAG,eAO9C,CAAA;AAUD;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,GAAI,QAAO,MAAkB,KAAG,WAgDvD,CAAA"}
|
package/build/gate.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createLazyService } from '@owlmeans/context';
|
|
2
|
+
import { OIDC_GATE } from '@owlmeans/oidc';
|
|
3
|
+
import { AuthForbidden } from '@owlmeans/auth';
|
|
4
|
+
import { createGateModel, extractPermissionSets } from '@owlmeans/server-oidc-rp';
|
|
5
|
+
import { hasPermission } from '@owlmeans/iam';
|
|
6
|
+
/**
|
|
7
|
+
* Gate-param syntax:
|
|
8
|
+
* <permission> — unscoped (project-wide) check, e.g. 'article--modify'
|
|
9
|
+
* <permission>@<requestParam> — resource-scoped: the resource id is resolved from
|
|
10
|
+
* req.params[requestParam] ?? req.query[requestParam]
|
|
11
|
+
* and must be listed in the grant's resources[]
|
|
12
|
+
* '{entity}' substitution applies to the permission part in both forms.
|
|
13
|
+
*/
|
|
14
|
+
export const RESOURCE_PARAM_SEPARATOR = '@';
|
|
15
|
+
export const parseGateParam = (param) => {
|
|
16
|
+
const idx = param.indexOf(RESOURCE_PARAM_SEPARATOR);
|
|
17
|
+
if (idx < 0) {
|
|
18
|
+
return { permission: param };
|
|
19
|
+
}
|
|
20
|
+
return { permission: param.slice(0, idx), resourceParam: param.slice(idx + 1) };
|
|
21
|
+
};
|
|
22
|
+
const resolveResourceId = (req, resourceParam) => {
|
|
23
|
+
const params = req.params;
|
|
24
|
+
const query = req.query;
|
|
25
|
+
const value = params?.[resourceParam] ?? query?.[resourceParam];
|
|
26
|
+
return value != null ? `${value}` : undefined;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* IAM gate: claims-first with UMA2 fallback. Registered under the OIDC_GATE alias by
|
|
30
|
+
* appendIam() so target code never knows which IAM backend is active.
|
|
31
|
+
*
|
|
32
|
+
* 1. When the Auth carries a valid PermissionSet[] claim (integrated IAM mode),
|
|
33
|
+
* params are asserted locally against it — both unscoped and resource-scoped forms.
|
|
34
|
+
* 2. Otherwise (Keycloak mode — its tokens never produce a conforming claim) the
|
|
35
|
+
* @-suffixes are stripped and the check delegates to the UMA2 gate model from
|
|
36
|
+
* @owlmeans/server-oidc-rp, byte-equivalent to makeOidcGate.
|
|
37
|
+
*/
|
|
38
|
+
export const makeIamGate = (alias = OIDC_GATE) => {
|
|
39
|
+
const service = createLazyService(alias, {
|
|
40
|
+
assert: async (req, _, params) => {
|
|
41
|
+
await service.ready();
|
|
42
|
+
const ctx = service.assertCtx();
|
|
43
|
+
if (req.auth == null) {
|
|
44
|
+
throw new AuthForbidden('auth');
|
|
45
|
+
}
|
|
46
|
+
const auth = req.auth;
|
|
47
|
+
const sets = extractPermissionSets(auth.permissions);
|
|
48
|
+
if (sets != null) {
|
|
49
|
+
const granted = params.some(param => {
|
|
50
|
+
const { permission, resourceParam } = parseGateParam(param);
|
|
51
|
+
const fixed = permission.replaceAll('{entity}', auth.entityId ?? '-');
|
|
52
|
+
if (resourceParam != null) {
|
|
53
|
+
const resourceId = resolveResourceId(req, resourceParam);
|
|
54
|
+
if (resourceId == null) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
return hasPermission(auth, fixed, { resourceId });
|
|
58
|
+
}
|
|
59
|
+
return hasPermission(auth, fixed);
|
|
60
|
+
});
|
|
61
|
+
if (!granted) {
|
|
62
|
+
throw new AuthForbidden('permission');
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const stripped = params.map(param => parseGateParam(param).permission);
|
|
67
|
+
const model = createGateModel(ctx);
|
|
68
|
+
const permissions = await model.loadPermissions(auth, stripped);
|
|
69
|
+
if (permissions.length < 1) {
|
|
70
|
+
throw new AuthForbidden('permission');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
return service;
|
|
75
|
+
};
|
|
76
|
+
//# sourceMappingURL=gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate.js","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"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"}
|
package/build/index.d.ts
CHANGED
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,eAAe,CAAA;AAClC,cAAc,aAAa,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,eAAe,CAAA;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAC7C,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA"}
|
package/build/index.js
CHANGED
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAC7C,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/server-iam",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -22,8 +22,13 @@
|
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@owlmeans/
|
|
26
|
-
"@owlmeans/
|
|
25
|
+
"@owlmeans/auth": "^0.1.7",
|
|
26
|
+
"@owlmeans/context": "^0.1.7",
|
|
27
|
+
"@owlmeans/entrypoint": "^0.1.7",
|
|
28
|
+
"@owlmeans/iam": "^0.1.7",
|
|
29
|
+
"@owlmeans/oidc": "^0.1.7",
|
|
30
|
+
"@owlmeans/server-context": "^0.1.7",
|
|
31
|
+
"@owlmeans/server-oidc-rp": "^0.1.7"
|
|
27
32
|
},
|
|
28
33
|
"devDependencies": {
|
|
29
34
|
"@owlmeans/dep-config": "workspace:*",
|
package/src/append.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
|
|
2
|
+
import type { WithSharedConfig } from '@owlmeans/oidc'
|
|
3
|
+
import { appendOidcGuard, makeOidcClientService, makeOidcWrappingService } from '@owlmeans/server-oidc-rp'
|
|
4
|
+
import { makeIamGate } from './gate.js'
|
|
5
|
+
|
|
6
|
+
type IamServerConfig = ServerConfig & WithSharedConfig
|
|
7
|
+
type IamServerContext<C extends IamServerConfig = IamServerConfig> = ServerContext<C>
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* One-call OIDC RP setup for a server context.
|
|
11
|
+
*
|
|
12
|
+
* Replaces the explicit wiring pattern in the target template:
|
|
13
|
+
* context.registerService(makeOidcClientService())
|
|
14
|
+
* context.registerService(makeOidcWrappingService())
|
|
15
|
+
* context.registerService(makeOidcGate())
|
|
16
|
+
* appendOidcGuard(context)
|
|
17
|
+
*
|
|
18
|
+
* The OIDC provider config (discovery URL, clientId, secret) is read from
|
|
19
|
+
* context.cfg.oidc.providers[] — populate it before calling appendIam().
|
|
20
|
+
*/
|
|
21
|
+
export const appendIam = <C extends IamServerConfig, T extends IamServerContext<C>>(
|
|
22
|
+
context: T
|
|
23
|
+
): T => {
|
|
24
|
+
context.registerService(makeOidcClientService())
|
|
25
|
+
context.registerService(makeOidcWrappingService())
|
|
26
|
+
// IAM gate under the OIDC_GATE alias: claims-first (integrated mode),
|
|
27
|
+
// UMA2 fallback byte-equivalent to makeOidcGate (keycloak mode)
|
|
28
|
+
context.registerService(makeIamGate())
|
|
29
|
+
appendOidcGuard<C, T>(context)
|
|
30
|
+
return context
|
|
31
|
+
}
|
package/src/gate.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createLazyService } from '@owlmeans/context'
|
|
2
|
+
import type { GateService, AbstractRequest } from '@owlmeans/entrypoint'
|
|
3
|
+
import { OIDC_GATE } from '@owlmeans/oidc'
|
|
4
|
+
import { AuthForbidden } from '@owlmeans/auth'
|
|
5
|
+
import type { Auth } from '@owlmeans/auth'
|
|
6
|
+
import { createGateModel, extractPermissionSets } from '@owlmeans/server-oidc-rp'
|
|
7
|
+
import type { Config, Context } from '@owlmeans/server-oidc-rp'
|
|
8
|
+
import { hasPermission } from '@owlmeans/iam'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Gate-param syntax:
|
|
12
|
+
* <permission> — unscoped (project-wide) check, e.g. 'article--modify'
|
|
13
|
+
* <permission>@<requestParam> — resource-scoped: the resource id is resolved from
|
|
14
|
+
* req.params[requestParam] ?? req.query[requestParam]
|
|
15
|
+
* and must be listed in the grant's resources[]
|
|
16
|
+
* '{entity}' substitution applies to the permission part in both forms.
|
|
17
|
+
*/
|
|
18
|
+
export const RESOURCE_PARAM_SEPARATOR = '@'
|
|
19
|
+
|
|
20
|
+
export interface ParsedGateParam {
|
|
21
|
+
permission: string
|
|
22
|
+
resourceParam?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const parseGateParam = (param: string): ParsedGateParam => {
|
|
26
|
+
const idx = param.indexOf(RESOURCE_PARAM_SEPARATOR)
|
|
27
|
+
if (idx < 0) {
|
|
28
|
+
return { permission: param }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { permission: param.slice(0, idx), resourceParam: param.slice(idx + 1) }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const resolveResourceId = (req: AbstractRequest, resourceParam: string): string | undefined => {
|
|
35
|
+
const params = req.params as Record<string, unknown> | undefined
|
|
36
|
+
const query = req.query as Record<string, unknown> | undefined
|
|
37
|
+
const value = params?.[resourceParam] ?? query?.[resourceParam]
|
|
38
|
+
|
|
39
|
+
return value != null ? `${value}` : undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* IAM gate: claims-first with UMA2 fallback. Registered under the OIDC_GATE alias by
|
|
44
|
+
* appendIam() so target code never knows which IAM backend is active.
|
|
45
|
+
*
|
|
46
|
+
* 1. When the Auth carries a valid PermissionSet[] claim (integrated IAM mode),
|
|
47
|
+
* params are asserted locally against it — both unscoped and resource-scoped forms.
|
|
48
|
+
* 2. Otherwise (Keycloak mode — its tokens never produce a conforming claim) the
|
|
49
|
+
* @-suffixes are stripped and the check delegates to the UMA2 gate model from
|
|
50
|
+
* @owlmeans/server-oidc-rp, byte-equivalent to makeOidcGate.
|
|
51
|
+
*/
|
|
52
|
+
export const makeIamGate = (alias: string = OIDC_GATE): GateService => {
|
|
53
|
+
const service: GateService = createLazyService<GateService>(alias, {
|
|
54
|
+
assert: async (req, _, params) => {
|
|
55
|
+
await service.ready()
|
|
56
|
+
const ctx = service.assertCtx<Config, Context>()
|
|
57
|
+
|
|
58
|
+
if (req.auth == null) {
|
|
59
|
+
throw new AuthForbidden('auth')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const auth = req.auth as Auth
|
|
63
|
+
const sets = extractPermissionSets(auth.permissions)
|
|
64
|
+
|
|
65
|
+
if (sets != null) {
|
|
66
|
+
const granted = params.some(param => {
|
|
67
|
+
const { permission, resourceParam } = parseGateParam(param)
|
|
68
|
+
const fixed = permission.replaceAll('{entity}', auth.entityId ?? '-')
|
|
69
|
+
|
|
70
|
+
if (resourceParam != null) {
|
|
71
|
+
const resourceId = resolveResourceId(req, resourceParam)
|
|
72
|
+
if (resourceId == null) {
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return hasPermission(auth, fixed, { resourceId })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return hasPermission(auth, fixed)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
if (!granted) {
|
|
83
|
+
throw new AuthForbidden('permission')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const stripped = params.map(param => parseGateParam(param).permission)
|
|
90
|
+
const model = createGateModel(ctx)
|
|
91
|
+
const permissions = await model.loadPermissions(auth, stripped)
|
|
92
|
+
|
|
93
|
+
if (permissions.length < 1) {
|
|
94
|
+
throw new AuthForbidden('permission')
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
return service
|
|
100
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { AppType, Layer, makeBasicContext } from '@owlmeans/context'
|
|
3
|
+
import type { BasicConfig } from '@owlmeans/context'
|
|
4
|
+
import type { AbstractRequest, AbstractResponse, GateService } from '@owlmeans/entrypoint'
|
|
5
|
+
import type { Auth } from '@owlmeans/auth'
|
|
6
|
+
import { AuthForbidden, AuthRole } from '@owlmeans/auth'
|
|
7
|
+
import { OIDC_GATE } from '@owlmeans/oidc'
|
|
8
|
+
import { makeIamGate } from '@owlmeans/server-iam'
|
|
9
|
+
|
|
10
|
+
const makeGate = async (): Promise<GateService> => {
|
|
11
|
+
const ctx = makeBasicContext<BasicConfig>({
|
|
12
|
+
ready: false,
|
|
13
|
+
service: 'server-iam-tests',
|
|
14
|
+
layer: Layer.Service,
|
|
15
|
+
type: AppType.Backend,
|
|
16
|
+
})
|
|
17
|
+
ctx.registerService(makeIamGate())
|
|
18
|
+
ctx.configure()
|
|
19
|
+
await ctx.init()
|
|
20
|
+
|
|
21
|
+
return ctx.service<GateService>(OIDC_GATE)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const makeRequest = (params: Record<string, string> = {}): AbstractRequest => ({
|
|
25
|
+
alias: 'test',
|
|
26
|
+
headers: {},
|
|
27
|
+
params,
|
|
28
|
+
query: {},
|
|
29
|
+
body: {},
|
|
30
|
+
path: '/',
|
|
31
|
+
auth: {
|
|
32
|
+
type: 'oidc-wrapped-token',
|
|
33
|
+
token: 'test',
|
|
34
|
+
userId: 'user-1',
|
|
35
|
+
role: AuthRole.User,
|
|
36
|
+
scopes: [],
|
|
37
|
+
entityId: 'entity-1',
|
|
38
|
+
isUser: true,
|
|
39
|
+
createdAt: new Date(),
|
|
40
|
+
permissions: [
|
|
41
|
+
{ scope: 'my-project', permissions: { 'article--modify': true } },
|
|
42
|
+
{
|
|
43
|
+
scope: 'my-project',
|
|
44
|
+
permissions: { 'department--modify': true },
|
|
45
|
+
resources: ['dep-12345678']
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
} satisfies Auth
|
|
49
|
+
}) as unknown as AbstractRequest
|
|
50
|
+
|
|
51
|
+
const res = {} as AbstractResponse<unknown>
|
|
52
|
+
|
|
53
|
+
describe('@owlmeans/server-iam — makeIamGate (claims mode)', () => {
|
|
54
|
+
test('passes an unscoped permission param', async () => {
|
|
55
|
+
const gate = await makeGate()
|
|
56
|
+
await gate.assert(makeRequest(), res, ['article--modify'])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('passes a resource-scoped param when the request resource id is granted', async () => {
|
|
60
|
+
const gate = await makeGate()
|
|
61
|
+
await gate.assert(makeRequest({ depId: 'dep-12345678' }), res, ['department--modify@depId'])
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('params are any-of — one granted param is enough', async () => {
|
|
65
|
+
const gate = await makeGate()
|
|
66
|
+
await gate.assert(makeRequest(), res, ['unknown--permission', 'article--modify'])
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('rejects a resource-scoped param for an ungranted resource id', async () => {
|
|
70
|
+
const gate = await makeGate()
|
|
71
|
+
expect(
|
|
72
|
+
gate.assert(makeRequest({ depId: 'dep-87654321' }), res, ['department--modify@depId'])
|
|
73
|
+
).rejects.toBeInstanceOf(AuthForbidden)
|
|
74
|
+
})
|
|
75
|
+
})
|