@owlmeans/server-iam 0.1.6 → 0.1.8

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/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # @owlmeans/server-iam
2
+
3
+ One-call OIDC RP wiring and IAM gate for OwlMeans servers — `appendIam()` and the permission-asserting `makeIamGate`.
4
+
5
+ ## Overview
6
+
7
+ - `appendIam(context)` — registers the OIDC client, wrapping service, and IAM gate in a single call, replacing verbose manual wiring
8
+ - `makeIamGate()` — produces a guard that asserts unscoped or resource-scoped permissions against the IAM backend (claims-first, UMA2 fallback)
9
+ - Re-exports `hasPermission` from `@owlmeans/iam` for inline permission checks
10
+ - Designed for IAM consumers such as the viable target template backend
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ bun add @owlmeans/server-iam
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```typescript
21
+ import { appendIam, makeIamGate } from '@owlmeans/server-iam'
22
+ import { IAM_GATE } from '@owlmeans/server-iam'
23
+
24
+ // Wire IAM into a server context
25
+ appendIam(context)
26
+
27
+ // Gate a module with a permission check
28
+ module.gate(IAM_GATE, { permission: 'manage-users' })
29
+ ```
30
+
31
+ Requires `@owlmeans/oidc` OIDC shared config and a running IAM provider configured via `@owlmeans/server-oidc-rp`.
32
+
33
+ <!-- owlmeans:agent-guidance:start -->
34
+ ## Agent guidance
35
+
36
+ This package ships embedded Claude Code skills and GitHub Copilot instructions under
37
+ `agent-meta/`. After installing your `@owlmeans/*` packages, run the OwlMeans
38
+ agent-skills installer to place them into your project's native locations
39
+ (`.claude/skills/` and `.github/instructions/`):
40
+
41
+ ```sh
42
+ npx @owlmeans/agent-skills
43
+ ```
44
+
45
+ The embedded files are version-matched to this package release. Do not edit them
46
+ directly — they are regenerated on each publish. To contribute guidance edits,
47
+ open a PR against the source monorepo.
48
+ <!-- owlmeans:agent-guidance:end -->
@@ -0,0 +1,63 @@
1
+ ---
2
+ description: "How to use @owlmeans/server-iam — one-call OIDC RP wiring (appendIam) and the IAM gate that asserts unscoped and resource-scoped permissions (claims-first, UMA2 fallback). Use when gating server endpoints in IAM consumers (e.g. the viable target template backend)."
3
+ applyTo: "**/owlmeans.ts, **/gate*.ts, **/auth-guard*.ts"
4
+ ---
5
+ <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
6
+
7
+ # Using `@owlmeans/server-iam`
8
+
9
+ Boilerplate-less server-side consumer of the OwlMeans IAM. One call wires the OIDC RP stack and the
10
+ IAM gate; the consumer never knows which IAM backend (Keycloak or integrated) is active.
11
+
12
+ ## Public API surface
13
+
14
+ | Symbol | Kind | Purpose |
15
+ |--------|------|---------|
16
+ | `appendIam(context)` | fn | Registers `makeOidcClientService()`, `makeOidcWrappingService()`, `makeIamGate()` (under `OIDC_GATE`), and the OIDC guard |
17
+ | `makeIamGate(alias?)` | fn | The IAM `GateService` — claims-first assertion with UMA2 fallback |
18
+ | `parseGateParam(param)` | fn | Splits `'perm@requestParam'` into `{ permission, resourceParam? }` |
19
+ | `RESOURCE_PARAM_SEPARATOR` | const | `'@'` |
20
+ | `hasPermission` | fn | Re-export from `@owlmeans/iam` |
21
+
22
+ ## Gate-param syntax
23
+
24
+ ```
25
+ <permission> → unscoped (project-wide): 'article--modify'
26
+ <permission>@<requestParam> → resource-scoped: 'department--modify@departmentId'
27
+ ```
28
+
29
+ - `{entity}` substitution applies to the permission part in both forms.
30
+ - For the resource-scoped form the gate resolves `req.params[requestParam] ?? req.query[requestParam]`
31
+ at assert time and requires the grant's `resources[]` to contain it (an unscoped grant always passes).
32
+ - Params are **any-of**: one granted param is enough.
33
+
34
+ ```ts
35
+ // module declaration
36
+ gate(OIDC_GATE, ['department--modify@depId'])
37
+
38
+ // or imperative
39
+ await ctx.service<GateService>(OIDC_GATE).assert(req, res, ['article--modify'])
40
+ ```
41
+
42
+ ## How the gate decides
43
+
44
+ 1. `req.auth == null` → `AuthForbidden('auth')`.
45
+ 2. **Claims mode** — when `req.auth.permissions` is a valid `PermissionSet[]` (minted into the id_token
46
+ by the integrated IAM provider and mapped into `Auth` by `@owlmeans/server-oidc-rp`), params are
47
+ asserted locally via `hasPermission`.
48
+ 3. **Fallback** — otherwise `@…` suffixes are stripped and the check delegates to the UMA2 gate model
49
+ from `@owlmeans/server-oidc-rp` (`createGateModel().loadPermissions`) — byte-equivalent to the old
50
+ `makeOidcGate`, which keeps Keycloak-backed deployments unchanged.
51
+
52
+ ## Rules
53
+
54
+ - Register the gate via `appendIam()`; do not also register `makeOidcGate()` — both use the
55
+ `OIDC_GATE` alias and the IAM gate already covers the UMA2 path.
56
+ - Never branch on the IAM backend in consumer code — the gate's claims/fallback split is the only seam.
57
+ - The OIDC provider entry must request the `permissions` scope (`extraScopes`) for claims mode to
58
+ engage; without it the gate transparently uses the fallback.
59
+
60
+ ## Related instructions
61
+
62
+ - `@owlmeans/iam` — abstraction + `hasPermission`; see `iam.instructions.md`
63
+ - `@owlmeans/server-oidc-rp` — claim extraction + UMA2 model; see `server-oidc-rp.instructions.md`
@@ -0,0 +1,16 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "package": "@owlmeans/server-iam",
4
+ "version": "0.1.8",
5
+ "generatedAt": "2026-06-11T16:30:27.191Z",
6
+ "canonicalRepo": "https://github.com/owlmeans/common",
7
+ "entries": [
8
+ {
9
+ "kind": "instruction",
10
+ "name": "server-iam",
11
+ "category": "package-specific",
12
+ "file": "instructions/server-iam.instructions.md",
13
+ "canonicalPath": ".github/instructions/server-iam.instructions.md"
14
+ }
15
+ ]
16
+ }
@@ -1 +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;AAGtD,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,CAMF,CAAA"}
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 CHANGED
@@ -1,4 +1,5 @@
1
- import { appendOidcGuard, makeOidcClientService, makeOidcGate, makeOidcWrappingService } from '@owlmeans/server-oidc-rp';
1
+ import { appendOidcGuard, makeOidcClientService, makeOidcWrappingService } from '@owlmeans/server-oidc-rp';
2
+ import { makeIamGate } from './gate.js';
2
3
  /**
3
4
  * One-call OIDC RP setup for a server context.
4
5
  *
@@ -14,7 +15,9 @@ import { appendOidcGuard, makeOidcClientService, makeOidcGate, makeOidcWrappingS
14
15
  export const appendIam = (context) => {
15
16
  context.registerService(makeOidcClientService());
16
17
  context.registerService(makeOidcWrappingService());
17
- context.registerService(makeOidcGate());
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());
18
21
  appendOidcGuard(context);
19
22
  return context;
20
23
  };
@@ -1 +1 @@
1
- {"version":3,"file":"append.js","sourceRoot":"","sources":["../src/append.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAA;AAKxH;;;;;;;;;;;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,OAAO,CAAC,eAAe,CAAC,YAAY,EAAE,CAAC,CAAA;IACvC,eAAe,CAAO,OAAO,CAAC,CAAA;IAC9B,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA"}
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"}
@@ -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
@@ -1,4 +1,6 @@
1
1
  export type * from '@owlmeans/iam';
2
+ export { hasPermission } from '@owlmeans/iam';
2
3
  export * from './consts.js';
3
4
  export * from './append.js';
5
+ export * from './gate.js';
4
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,eAAe,CAAA;AAClC,cAAc,aAAa,CAAA;AAC3B,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
@@ -1,3 +1,5 @@
1
+ export { hasPermission } from '@owlmeans/iam';
1
2
  export * from './consts.js';
2
3
  export * from './append.js';
4
+ export * from './gate.js';
3
5
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA;AAC3B,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.6",
3
+ "version": "0.1.8",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -22,11 +22,13 @@
22
22
  }
23
23
  },
24
24
  "dependencies": {
25
- "@owlmeans/context": "^0.1.6",
26
- "@owlmeans/iam": "^0.1.6",
27
- "@owlmeans/oidc": "^0.1.6",
28
- "@owlmeans/server-context": "^0.1.6",
29
- "@owlmeans/server-oidc-rp": "^0.1.6"
25
+ "@owlmeans/auth": "^0.1.8",
26
+ "@owlmeans/context": "^0.1.8",
27
+ "@owlmeans/entrypoint": "^0.1.8",
28
+ "@owlmeans/iam": "^0.1.8",
29
+ "@owlmeans/oidc": "^0.1.8",
30
+ "@owlmeans/server-context": "^0.1.8",
31
+ "@owlmeans/server-oidc-rp": "^0.1.8"
30
32
  },
31
33
  "devDependencies": {
32
34
  "@owlmeans/dep-config": "workspace:*",
package/src/append.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
2
2
  import type { WithSharedConfig } from '@owlmeans/oidc'
3
- import { appendOidcGuard, makeOidcClientService, makeOidcGate, makeOidcWrappingService } from '@owlmeans/server-oidc-rp'
3
+ import { appendOidcGuard, makeOidcClientService, makeOidcWrappingService } from '@owlmeans/server-oidc-rp'
4
+ import { makeIamGate } from './gate.js'
4
5
 
5
6
  type IamServerConfig = ServerConfig & WithSharedConfig
6
7
  type IamServerContext<C extends IamServerConfig = IamServerConfig> = ServerContext<C>
@@ -22,7 +23,9 @@ export const appendIam = <C extends IamServerConfig, T extends IamServerContext<
22
23
  ): T => {
23
24
  context.registerService(makeOidcClientService())
24
25
  context.registerService(makeOidcWrappingService())
25
- context.registerService(makeOidcGate())
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())
26
29
  appendOidcGuard<C, T>(context)
27
30
  return context
28
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
@@ -1,3 +1,5 @@
1
1
  export type * from '@owlmeans/iam'
2
+ export { hasPermission } from '@owlmeans/iam'
2
3
  export * from './consts.js'
3
4
  export * from './append.js'
5
+ export * from './gate.js'
@@ -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
+ })