@open-rlb/nestjs-amqp 2.0.8 → 2.0.10
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 +44 -45
- package/modules/acl/auth-match.d.ts +6 -0
- package/modules/acl/auth-match.js +10 -0
- package/modules/acl/auth-match.js.map +1 -0
- package/modules/acl/config/acl.config.d.ts +1 -0
- package/modules/acl/const.d.ts +5 -2
- package/modules/acl/const.js +6 -3
- package/modules/acl/const.js.map +1 -1
- package/modules/acl/index.d.ts +1 -0
- package/modules/acl/index.js +1 -0
- package/modules/acl/index.js.map +1 -1
- package/modules/acl/repository/acl-action.repository.d.ts +1 -0
- package/modules/acl/repository/acl-action.repository.js.map +1 -1
- package/modules/acl/repository/acl-grant.repository.d.ts +1 -0
- package/modules/acl/repository/acl-grant.repository.js.map +1 -1
- package/modules/acl/repository/acl-role.repository.d.ts +1 -0
- package/modules/acl/repository/acl-role.repository.js.map +1 -1
- package/modules/acl/services/acl-management.service.d.ts +10 -3
- package/modules/acl/services/acl-management.service.js +60 -10
- package/modules/acl/services/acl-management.service.js.map +1 -1
- package/modules/acl/services/acl.service.d.ts +3 -4
- package/modules/acl/services/acl.service.js +24 -46
- package/modules/acl/services/acl.service.js.map +1 -1
- package/modules/broker/config/decorator-paths.js +2 -2
- package/modules/broker/config/decorator-paths.js.map +1 -1
- package/modules/broker/decorators/broker-action.decorator.d.ts +1 -1
- package/modules/broker/decorators/broker-action.decorator.js +2 -2
- package/modules/broker/decorators/broker-action.decorator.js.map +1 -1
- package/modules/broker/services/shutdown-state.service.d.ts +1 -0
- package/modules/broker/services/shutdown-state.service.js +12 -12
- package/modules/broker/services/shutdown-state.service.js.map +1 -1
- package/modules/gateway-admin/config/gateway-admin.config.d.ts +2 -0
- package/modules/gateway-admin/const.d.ts +7 -0
- package/modules/gateway-admin/const.js +7 -0
- package/modules/gateway-admin/const.js.map +1 -1
- package/modules/gateway-admin/gateway-admin.module.js +4 -1
- package/modules/gateway-admin/gateway-admin.module.js.map +1 -1
- package/modules/gateway-admin/health.d.ts +14 -0
- package/modules/gateway-admin/health.js +5 -0
- package/modules/gateway-admin/health.js.map +1 -0
- package/modules/gateway-admin/index.d.ts +5 -0
- package/modules/gateway-admin/index.js +5 -0
- package/modules/gateway-admin/index.js.map +1 -1
- package/modules/gateway-admin/models.d.ts +61 -0
- package/modules/gateway-admin/repository/auth-provider.repository.d.ts +1 -0
- package/modules/gateway-admin/repository/auth-provider.repository.js.map +1 -1
- package/modules/gateway-admin/repository/http-metric.repository.d.ts +6 -2
- package/modules/gateway-admin/repository/http-metric.repository.js.map +1 -1
- package/modules/gateway-admin/repository/http-path.repository.d.ts +3 -0
- package/modules/gateway-admin/repository/http-path.repository.js.map +1 -1
- package/modules/gateway-admin/repository/route-sync-log.repository.d.ts +4 -1
- package/modules/gateway-admin/repository/route-sync-log.repository.js.map +1 -1
- package/modules/gateway-admin/services/gateway-auth.service.d.ts +1 -0
- package/modules/gateway-admin/services/gateway-auth.service.js +12 -0
- package/modules/gateway-admin/services/gateway-auth.service.js.map +1 -1
- package/modules/gateway-admin/services/gateway-health.service.d.ts +10 -0
- package/modules/gateway-admin/services/gateway-health.service.js +64 -0
- package/modules/gateway-admin/services/gateway-health.service.js.map +1 -0
- package/modules/gateway-admin/services/gateway-metrics-rollup.service.d.ts +13 -0
- package/modules/gateway-admin/services/gateway-metrics-rollup.service.js +63 -0
- package/modules/gateway-admin/services/gateway-metrics-rollup.service.js.map +1 -0
- package/modules/gateway-admin/services/gateway-metrics.service.d.ts +5 -4
- package/modules/gateway-admin/services/gateway-metrics.service.js +56 -17
- package/modules/gateway-admin/services/gateway-metrics.service.js.map +1 -1
- package/modules/gateway-admin/services/gateway-path.service.d.ts +11 -4
- package/modules/gateway-admin/services/gateway-path.service.js +79 -11
- package/modules/gateway-admin/services/gateway-path.service.js.map +1 -1
- package/modules/gateway-admin/services/gateway-retention.service.d.ts +16 -0
- package/modules/gateway-admin/services/gateway-retention.service.js +71 -0
- package/modules/gateway-admin/services/gateway-retention.service.js.map +1 -0
- package/modules/gateway-admin/services/route-sync.service.js +12 -5
- package/modules/gateway-admin/services/route-sync.service.js.map +1 -1
- package/modules/gateway-admin/util/metrics.d.ts +10 -0
- package/modules/gateway-admin/util/metrics.js +170 -0
- package/modules/gateway-admin/util/metrics.js.map +1 -0
- package/modules/gateway-admin/util/route-diff.d.ts +7 -0
- package/modules/gateway-admin/util/route-diff.js +7 -2
- package/modules/gateway-admin/util/route-diff.js.map +1 -1
- package/modules/gateway-admin/util/route-manifest.d.ts +3 -0
- package/modules/gateway-admin/util/route-manifest.js +36 -1
- package/modules/gateway-admin/util/route-manifest.js.map +1 -1
- package/modules/proxy/config/path-definition.config.d.ts +2 -2
- package/modules/proxy/services/acl.service.d.ts +2 -2
- package/modules/proxy/services/acl.service.js.map +1 -1
- package/modules/proxy/services/http-auth-handler.service.d.ts +6 -4
- package/modules/proxy/services/http-auth-handler.service.js +15 -9
- package/modules/proxy/services/http-auth-handler.service.js.map +1 -1
- package/modules/proxy/services/http-handler.service.d.ts +1 -0
- package/modules/proxy/services/http-handler.service.js +28 -35
- package/modules/proxy/services/http-handler.service.js.map +1 -1
- package/modules/proxy/services/metrics-hook.d.ts +1 -0
- package/modules/proxy/services/websocket.service.js +5 -5
- package/modules/proxy/services/websocket.service.js.map +1 -1
- package/package.json +1 -1
- package/schematics/nest-add/files/acl/src/modules/database/repository/acl.repository.ts +3 -0
- package/schematics/nest-add/files/db-core/src/modules/database/repository/in-memory-collection.ts +29 -0
- package/schematics/nest-add/files/gateway-admin/src/modules/database/repository/gateway.repository.ts +48 -20
- package/schematics/nest-add/files/gateway-admin/src/modules/database/repository/route-sync.repository.ts +24 -1
- package/schematics/nest-add/files/skills/rlb-amqp/SKILL.md +3 -3
- package/schematics/nest-add/files/skills/rlb-amqp/references/config-schema.md +18 -14
- package/schematics/nest-add/files/skills/rlb-amqp/references/gotchas.md +42 -17
- package/schematics/nest-add/files/skills/rlb-amqp-acl/SKILL.md +57 -39
- package/schematics/nest-add/files/skills/rlb-amqp-add-action/SKILL.md +4 -3
- package/schematics/nest-add/files/skills/rlb-amqp-add-route/SKILL.md +20 -14
- package/schematics/nest-add/files/skills/rlb-amqp-add-ws-event/SKILL.md +7 -6
- package/schematics/nest-add/files/skills/rlb-amqp-gateway-admin/SKILL.md +49 -9
- package/schematics/nest-add/files/skills/rlb-amqp-scaffold/SKILL.md +4 -4
- package/schematics/nest-add/index.js +78 -9
- package/schematics/nest-add/index.js.map +1 -1
- package/schematics/nest-add/index.ts +78 -9
|
@@ -134,7 +134,7 @@ auth-providers:
|
|
|
134
134
|
- name: gateway-jwks
|
|
135
135
|
type: jwks # jwt | jwks | basic | str-compare | none
|
|
136
136
|
headerPrefix: "X-GTW-AUTH-" # prefix for mapped claim headers (and <prefix>USERID)
|
|
137
|
-
uidClaim: sub # claim → <prefix>USERID; REQUIRED for
|
|
137
|
+
uidClaim: sub # claim → <prefix>USERID; REQUIRED for action checks
|
|
138
138
|
jwtMap: # 'source:dest' pairs → <prefix><DEST>; WITHOUT it NO claims forwarded
|
|
139
139
|
- sub:userId # → X-GTW-AUTH-USERID
|
|
140
140
|
- email:email # → X-GTW-AUTH-EMAIL
|
|
@@ -159,8 +159,8 @@ Type behaviour:
|
|
|
159
159
|
|
|
160
160
|
Hardening: `algorithms` REQUIRED for `jwt`/`jwks` (omit → denied; algorithm-confusion guard).
|
|
161
161
|
Define `jwtMap` or no identity is forwarded (token still `success:true` — fail-safe, not leak).
|
|
162
|
-
`usernameClaim` is deprecated; `aclTopic`/`aclAction` are removed (gateway
|
|
163
|
-
via `IAclRoleService.
|
|
162
|
+
`usernameClaim` is deprecated; `aclTopic`/`aclAction` are removed (the gateway action check is
|
|
163
|
+
in-process via `IAclRoleService.checkAction(userId, ctx, action)`).
|
|
164
164
|
|
|
165
165
|
> DB-stored auth-providers (name-keyed `gw-auth-*` upserts) layer on top of this static list —
|
|
166
166
|
> see `docs/gateway-admin.md`.
|
|
@@ -211,8 +211,8 @@ control-topic subscriber ignores every other message. Concurrent `reload()`s are
|
|
|
211
211
|
mode: rpc # rpc | event
|
|
212
212
|
timeout: 15000 # rpc only (ms)
|
|
213
213
|
auth: gateway-jwks # auth-provider name
|
|
214
|
-
allowAnonymous: false # true → skip the auth/
|
|
215
|
-
|
|
214
|
+
allowAnonymous: false # true → skip the auth/action gate entirely
|
|
215
|
+
actions: [doc.read, doc.admin] # caller must hold AT LEAST ONE on (companyId, resourceId); needs auth + IAclRoleService
|
|
216
216
|
successStatusCode: 200 # default 200 rpc / 202 event / 204 empty rpc reply
|
|
217
217
|
binary: true # treat a raw (non-JSON) reply as base64 → binary body
|
|
218
218
|
redirect: 302 # rpc only → redirect with this status, using the reply as Location
|
|
@@ -238,9 +238,13 @@ Uploads → `$files` (buffers as binary strings), raw → `$raw`.
|
|
|
238
238
|
ForbiddenError→403, NotFoundError→404, ConflictError→409, else→500.
|
|
239
239
|
**event status:** successful publish → `successStatusCode || 202`; publish failure → `503`.
|
|
240
240
|
|
|
241
|
-
Auth gate (per request): `allowAnonymous:true` → gate skipped; `auth` no `
|
|
242
|
-
(401 if invalid); `auth` + `
|
|
243
|
-
|
|
241
|
+
Auth gate (per request): `allowAnonymous:true` → gate skipped; `auth` no `actions` → authn only
|
|
242
|
+
(401 if invalid); `auth` + `actions` → authn then in-process
|
|
243
|
+
`checkAction(userId, { companyId, resourceId }, actions)` (403 if the caller holds none of
|
|
244
|
+
`actions` on that pair). `actions` without `auth` fails closed (every request 403).
|
|
245
|
+
|
|
246
|
+
The gateway reads the canonical `companyId`/`resourceId` from the request (precedence
|
|
247
|
+
params → query → body) and matches them exactly for the action check.
|
|
244
248
|
|
|
245
249
|
### gateway.events[] (WebSocketEvent — WS / webhook)
|
|
246
250
|
|
|
@@ -251,7 +255,7 @@ no role). `roles` without `auth` fails closed (every request 403).
|
|
|
251
255
|
routingKey: chat.messages
|
|
252
256
|
auth: gateway-jwks # provider that verifies the token + maps claims FOR THIS event (at subscribe)
|
|
253
257
|
requireAuth: true # default true when `auth` is set; false → auth optional (anon allowed)
|
|
254
|
-
|
|
258
|
+
actions: [chat.read] # ACL check via IAclRoleService.checkAction (needs auth); WS gates resource-agnostically
|
|
255
259
|
scopeClaim: userId # per-user isolation: the mapped claim value...
|
|
256
260
|
payloadKey: userId # ...must equal payload[payloadKey]; without payloadKey → denies everything
|
|
257
261
|
# type: http only:
|
|
@@ -282,11 +286,10 @@ ACL (topic `rlb-acl`):
|
|
|
282
286
|
|
|
283
287
|
| Method | Path | Action |
|
|
284
288
|
| --- | --- | --- |
|
|
285
|
-
| GET | `/acl/check` | `acl-
|
|
286
|
-
| GET | `/acl/check-resource` | `acl-can-user-do` (resource-scoped; `200` true/false) |
|
|
289
|
+
| GET | `/acl/check` | `acl-check-action` (`?userId=&action=&companyId?=&resourceId?=`; exact-match; `200` true/false) |
|
|
287
290
|
| GET | `/acl/resources` | `acl-list-resources-by-user` (auth, reads `X-GTW-AUTH-USERID`) |
|
|
288
|
-
| POST | `/acl/grants` | `acl-grant` (`{userId, roles, resourceId?, companyId?}`) |
|
|
289
|
-
| DELETE | `/acl/grants` | `acl-revoke` (same shape; `roles` REQUIRED) |
|
|
291
|
+
| POST | `/acl/grants` | `acl-grant` (`{userId, roles, resourceId?, companyId?}`; caller needs `role-management` on target) |
|
|
292
|
+
| DELETE | `/acl/grants` | `acl-revoke` (same shape; `roles` REQUIRED; caller needs `role-management` on target) |
|
|
290
293
|
| GET / PUT / DELETE | `/acl/actions[/get?name=]` | `acl-action-list`/`-update`/`-delete`/`-get` |
|
|
291
294
|
| GET / PUT / DELETE | `/acl/roles[/get?name=]` | `acl-role-list`/`-update`/`-delete`/`-get` |
|
|
292
295
|
|
|
@@ -301,4 +304,5 @@ Gateway-admin (topic `rlb-gateway-admin`, except reload):
|
|
|
301
304
|
| POST | `/admin/metrics/track` | `gw-metrics-track` (`mode: event`) |
|
|
302
305
|
| POST | `/admin/reload` | `gw-reload` on `rlb-gateway-control` (`mode: event`) |
|
|
303
306
|
|
|
304
|
-
> Removed: `acl-list-by-user`, `acl-verify-access`, `
|
|
307
|
+
> Removed: `acl-list-by-user`, `acl-verify-access`, `acl-can-user-do`, `acl-can-user-do-gtw`
|
|
308
|
+
> (all collapsed into `acl-check-action`), `gw-auth-create`, all id-based ACL CRUD.
|
|
@@ -76,29 +76,54 @@ Ported from `docs/gotchas.md` (re-verified against post-2.0.5 code).
|
|
|
76
76
|
NOT a metrics dump. Use `/admin/metrics*` (`gw-metrics-*`) for metrics.
|
|
77
77
|
|
|
78
78
|
## Auth / ACL
|
|
79
|
-
21. **`
|
|
80
|
-
(every request `403`, logged at boot). Always pair `
|
|
81
|
-
22. **`
|
|
79
|
+
21. **`actions` require `auth` on the same path/event.** No `auth` → no identity → fails closed
|
|
80
|
+
(every request `403`, logged at boot). Always pair `actions: [...]` with `auth: <provider>`.
|
|
81
|
+
22. **`actions` require an `IAclRoleService`** registered via `RLB_GTW_ACL_ROLE_SERVICE` in
|
|
82
82
|
`ProxyModule.forRootAsync({ providers: [...] })`. Missing → deny (403). The gateway check is
|
|
83
|
-
**
|
|
84
|
-
the
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
`
|
|
88
|
-
|
|
83
|
+
**action-based, OR, resource-SCOPED** (`checkAction(userId, ctx, actions)`): `actions` lists
|
|
84
|
+
ACTION NAMES, the caller is authorized if it holds AT LEAST ONE on the request's
|
|
85
|
+
`(companyId, resourceId)`. The provider only needs `uidClaim` (+ `headerPrefix`).
|
|
86
|
+
23. **One ACL check action on `rlb-acl`: `acl-check-action`** (cached, HTTP GET → `200` true/false).
|
|
87
|
+
`checkAction(userId, ctx, action)`, `ctx = { companyId?, resourceId? }`,
|
|
88
|
+
`action = string | string[]` (OR). It resolves action→roles-that-include-it, then matches the
|
|
89
|
+
user's grants. A grant authorizes **iff** `grant.companyId === req.companyId &&
|
|
90
|
+
grant.resourceId === req.resourceId` (undefined/null/`''` = absent). The ONLY carve-out: both
|
|
91
|
+
ids absent on request AND grant. **No wildcard** — a `null` `resourceId` does NOT match
|
|
92
|
+
everything; `companyId` is load-bearing. Replaces the old `acl-can-user-do` /
|
|
93
|
+
`acl-can-user-do-gtw` and the merged `GET /acl/check` + `/acl/check-resource`.
|
|
94
|
+
23a. **Gateway gating is ACTION-based, not role-based.** `gateway.paths[].actions` /
|
|
95
|
+
`events[].actions` name ACTIONS (was `roles`). The gateway resolves `userId` from the auth
|
|
96
|
+
provider, extracts `(companyId, resourceId)` from the request, and authorizes if the caller
|
|
97
|
+
holds one of `actions` on that pair. It reads the canonical `companyId`/`resourceId` from the
|
|
98
|
+
request (precedence params→query→body) and matches them exactly. WS events gate by `actions`
|
|
99
|
+
**resource-agnostically**.
|
|
100
|
+
23b. **`@BrokerAuth`'s 3rd param is now `actions` (was `roles`).** Signature:
|
|
101
|
+
`@BrokerAuth(authName, allowAnonymous?, actions?, httpName?)`. Pass action names there for an
|
|
102
|
+
auto-discovered route's action gate.
|
|
89
103
|
24. **Actions, roles & auth-providers are NAME-KEYED. PUT upserts; there is NO POST.** The `name`
|
|
90
104
|
IS the key (no id). `PUT` creates-or-updates, `GET` lists, `GET .../get?name=` reads one,
|
|
91
105
|
`DELETE` removes by `name`. The old id-based ACL CRUD and `POST`-create endpoints are GONE.
|
|
92
106
|
(Gateway-admin **paths** are the exception — they keep id-keyed CRUD and a POST create.)
|
|
93
107
|
25. **`acl-grant` / `acl-revoke` both REQUIRE `userId` + `roles`** (optional `resourceId` +
|
|
94
|
-
`companyId`).
|
|
95
|
-
`revoke` REMOVES exactly those roles and **deletes the
|
|
96
|
-
`revoke` without `roles` throws `400 roles are required` —
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
108
|
+
`companyId`). The grant record is keyed by `(userId, companyId, resourceId)`. `grant` MERGES
|
|
109
|
+
roles into that triple (idempotent). `revoke` REMOVES exactly those roles and **deletes the
|
|
110
|
+
record once it has no roles left**. `revoke` without `roles` throws `400 roles are required` —
|
|
111
|
+
to wipe a grant, revoke all its roles. **Grants assign ROLES (keep the `roles` param); roles
|
|
112
|
+
contain actions.** Only the gateway/route GATE switched to action names.
|
|
113
|
+
25a. **`acl-grant` / `acl-revoke` are GATED.** The caller (forwarded `X-GTW-AUTH-USERID`) must hold
|
|
114
|
+
the `role-management` action on the TARGET `(companyId, resourceId)`, else `403`. The gate
|
|
115
|
+
action defaults to `role-management`, overridable via `AclModuleOptions.roleManagementAction`.
|
|
116
|
+
**Chicken-and-egg:** no caller can grant the very first `role-management`, so **bootstrap by
|
|
117
|
+
seeding the first `role-management` grant directly in the DB**.
|
|
118
|
+
26. **`companyId` is LOAD-BEARING in authorization.** It replaced `resourceBusinessId` and is
|
|
119
|
+
BOTH part of the grant identity AND matched during `checkAction`: a grant authorizes only when
|
|
120
|
+
`grant.companyId === req.companyId` (and `resourceId` likewise). It also groups
|
|
121
|
+
`acl-list-resources-by-user` output. There is **no wildcard** — a `null`/absent `resourceId`
|
|
122
|
+
only matches a request with that id also absent. Both grant/revoke validate every role exists
|
|
123
|
+
(unknown → `400`).
|
|
124
|
+
27. **Removed actions:** `acl-list-by-user`, `acl-verify-access`, `acl-can-user-do`, and
|
|
125
|
+
`acl-can-user-do-gtw` no longer exist (the last two collapsed into `acl-check-action`). Use
|
|
126
|
+
`acl-check-action` for authorization checks and `acl-list-resources-by-user` to list resources.
|
|
102
127
|
28. **Auth & gateway config go to `ProxyModule`** (`authOptions` / `gatewayOptions`), not
|
|
103
128
|
`BrokerModule`. `BrokerModule` owns only `options` / `topics` / `appOptions`.
|
|
104
129
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: rlb-amqp-acl
|
|
3
|
-
description: Manage
|
|
3
|
+
description: Manage access control (ACL) with @open-rlb/nestjs-amqp — actions, roles, grants/revokes, and "can user do X" checks. Use when wiring AclModule, gating gateway routes by actions, granting/revoking a user's roles, listing a user's resources, or answering authorization/permission questions (actions, roles, grants, acl-check).
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Manage ACL (@open-rlb/nestjs-amqp)
|
|
@@ -11,15 +11,16 @@ Read first when you need depth:
|
|
|
11
11
|
- `sample/config-sample/acl.yaml` (annotated broker + gateway reference)
|
|
12
12
|
- `sample/config-sample/gateway-in-memory/src/app.module.ts` (forRoot wiring)
|
|
13
13
|
|
|
14
|
-
Use when: managing **actions/roles/grants**, wiring `AclModule`,
|
|
15
|
-
(`
|
|
14
|
+
Use when: managing **actions/roles/grants**, wiring `AclModule`, action-gating routes
|
|
15
|
+
(`actions: [...]`), or answering "can user do X".
|
|
16
16
|
|
|
17
17
|
## Model (3 entities)
|
|
18
18
|
|
|
19
19
|
- **Action** — atomic capability (`read-doc`). Name-keyed.
|
|
20
20
|
- **Role** — bundle of action names (`editor = [read-doc, write-doc]`). Name-keyed.
|
|
21
|
-
- **Grant** — binds a `userId` → role names; one record per `(userId, resourceId)`.
|
|
22
|
-
- **Checks**
|
|
21
|
+
- **Grant** — binds a `userId` → role names; one record per `(userId, companyId, resourceId)`.
|
|
22
|
+
- **Checks** resolve the requested **action** → roles-that-include-it, then match the
|
|
23
|
+
user's grants. The route/gate names **actions**; grants still assign **roles**.
|
|
23
24
|
|
|
24
25
|
## Decorator-bound (NOT configurable)
|
|
25
26
|
|
|
@@ -28,8 +29,8 @@ reference them literally. The queue / exchange / routingKey that carry the topic
|
|
|
28
29
|
|
|
29
30
|
`ACL_ACTIONS`: `acl-action-list`, `acl-action-get`, `acl-action-update`,
|
|
30
31
|
`acl-action-delete`, `acl-role-list`, `acl-role-get`, `acl-role-update`,
|
|
31
|
-
`acl-role-delete`, `acl-grant`, `acl-revoke`, `acl-
|
|
32
|
-
`acl-
|
|
32
|
+
`acl-role-delete`, `acl-grant`, `acl-revoke`, `acl-check-action`,
|
|
33
|
+
`acl-list-resources-by-user`, `acl-invalidate`.
|
|
33
34
|
|
|
34
35
|
> **Removed in 2.0.5:** `acl-list-by-user`, `acl-verify-access`, `acl-create` /
|
|
35
36
|
> id-based ACL CRUD. Entities are name-keyed: **PUT upserts, no POST.**
|
|
@@ -38,30 +39,40 @@ reference them literally. The queue / exchange / routingKey that carry the topic
|
|
|
38
39
|
|
|
39
40
|
No id, no POST. `PUT` upserts by `name` (idempotent), `GET` lists (`?page=&limit=`),
|
|
40
41
|
`GET …/get?name=` reads one, `DELETE` removes by `name`. Role upsert: every referenced
|
|
41
|
-
action must already exist (else **400**).
|
|
42
|
+
action must already exist (else **400**). The `*-search` actions (`acl-action-search`,
|
|
43
|
+
`acl-role-search`, `acl-grant-search`) now take `?q=&page=&limit=` and return
|
|
44
|
+
`PaginationModel<T>` (`{ page, limit, total, data }`), not a bare array.
|
|
42
45
|
|
|
43
|
-
## Grants — dual grant/revoke
|
|
46
|
+
## Grants — dual grant/revoke (now GATED)
|
|
44
47
|
|
|
45
|
-
One record per `(userId, resourceId)`. Both ops **require `userId` + `roles`**;
|
|
46
|
-
`resourceId` + `companyId` are **optional
|
|
48
|
+
One record per `(userId, companyId, resourceId)`. Both ops **require `userId` + `roles`**;
|
|
49
|
+
`resourceId` + `companyId` are **optional** but PART of the record identity.
|
|
47
50
|
|
|
48
|
-
- `acl-grant` — merges roles into the
|
|
51
|
+
- `acl-grant` — merges roles into the triple (creates if absent; idempotent).
|
|
49
52
|
- `acl-revoke` — removes roles; deletes the record once empty.
|
|
50
53
|
- Both validate every role exists (unknown role → **400**) and invalidate the user's cache.
|
|
51
|
-
- `companyId` (replaced `resourceBusinessId`) is **
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
`
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
54
|
+
- `companyId` (replaced `resourceBusinessId`) is **load-bearing**: it is part of the grant
|
|
55
|
+
identity AND part of authorization (a grant matches only when its `companyId` equals the
|
|
56
|
+
request's). It also groups `acl-list-resources-by-user` output.
|
|
57
|
+
- **Caller gating:** `acl-grant`/`acl-revoke` require the caller (forwarded
|
|
58
|
+
`X-GTW-AUTH-USERID`) to hold the `role-management` action on the TARGET
|
|
59
|
+
`(companyId, resourceId)`, else **403**. The gate action defaults to `role-management`,
|
|
60
|
+
overridable via `AclModuleOptions.roleManagementAction`. Bootstrap by seeding the first
|
|
61
|
+
`role-management` grant directly in the DB (no caller can grant it otherwise).
|
|
62
|
+
|
|
63
|
+
## Checks — single primitive, GET → 200 with `true`/`false`
|
|
64
|
+
|
|
65
|
+
`false` is real content; only `null`/`undefined` collapses to 204. Returns `false`
|
|
66
|
+
(never throws) on missing input or error.
|
|
67
|
+
|
|
68
|
+
- `acl-check-action` → `checkAction(userId, ctx, action)`, `ctx = { companyId?, resourceId? }`,
|
|
69
|
+
`action = string | string[]` (OR). Resolves the action(s) → roles-that-include-it, then
|
|
70
|
+
matches the user's grants. A grant authorizes **iff** `grant.companyId === req.companyId &&
|
|
71
|
+
grant.resourceId === req.resourceId` (undefined/null/`''` all count as absent). The ONLY
|
|
72
|
+
carve-out: both ids absent on the request AND on the grant. **No wildcard** — a `null`
|
|
73
|
+
`resourceId` no longer matches everything; `companyId` is load-bearing.
|
|
74
|
+
Query: `?userId=&action=read-doc&companyId=acme&resourceId=doc-1`.
|
|
75
|
+
- `acl-list-resources-by-user` — **auth-gated** (needs `auth`, no actions): reads `userId`
|
|
65
76
|
from the forwarded `X-GTW-AUTH-USERID` header; lists accessible resources grouped by
|
|
66
77
|
`companyId` with resolved actions.
|
|
67
78
|
|
|
@@ -88,8 +99,9 @@ AclModule.forRoot(
|
|
|
88
99
|
);
|
|
89
100
|
```
|
|
90
101
|
|
|
91
|
-
Gateway side — let route `
|
|
92
|
-
binding the gateway token to the same `AclService
|
|
102
|
+
Gateway side — let route `actions: [...]` gates run **in-process** (no broker hop) by
|
|
103
|
+
binding the gateway token to the same `AclService` (implements
|
|
104
|
+
`IAclRoleService.checkAction(userId, ctx, action)`):
|
|
93
105
|
|
|
94
106
|
```ts
|
|
95
107
|
import { ProxyModule, AclService, RLB_GTW_ACL_ROLE_SERVICE } from '@open-rlb/nestjs-amqp';
|
|
@@ -99,8 +111,9 @@ ProxyModule.forRoot({
|
|
|
99
111
|
});
|
|
100
112
|
```
|
|
101
113
|
|
|
102
|
-
Same process → `useExisting`. Separate services → gateway RPCs `acl-
|
|
103
|
-
`rlb-acl` instead. A route's `
|
|
114
|
+
Same process → `useExisting`. Separate services → gateway RPCs `acl-check-action` on
|
|
115
|
+
`rlb-acl` instead. A route's `actions` are ACTION NAMES; the caller is authorized if it
|
|
116
|
+
holds **≥1** of them on the request's `(companyId, resourceId)`.
|
|
104
117
|
|
|
105
118
|
## YAML — topic + queue (names fixed, transport yours)
|
|
106
119
|
|
|
@@ -131,14 +144,16 @@ the fixed library string.
|
|
|
131
144
|
| acl-action-get | GET | /acl/actions/get | query | acl-action-get |
|
|
132
145
|
| acl-action-upsert | PUT | /acl/actions | body | acl-action-update |
|
|
133
146
|
| acl-action-delete | DELETE | /acl/actions | body | acl-action-delete |
|
|
147
|
+
| acl-action-search | GET | /acl/actions/search | query | acl-action-search (`?q=&page=&limit=` → `PaginationModel<AclAction>`) |
|
|
134
148
|
| acl-role-list | GET | /acl/roles | query | acl-role-list |
|
|
135
149
|
| acl-role-get | GET | /acl/roles/get | query | acl-role-get |
|
|
136
150
|
| acl-role-upsert | PUT | /acl/roles | body | acl-role-update |
|
|
137
151
|
| acl-role-delete | DELETE | /acl/roles | body | acl-role-delete |
|
|
138
|
-
| acl-
|
|
139
|
-
| acl-
|
|
140
|
-
| acl-
|
|
141
|
-
| acl-
|
|
152
|
+
| acl-role-search | GET | /acl/roles/search | query | acl-role-search (`?q=&page=&limit=` → `PaginationModel<AclRole>`) |
|
|
153
|
+
| acl-grant | POST | /acl/grants | body | acl-grant (gated: caller needs `role-management`) |
|
|
154
|
+
| acl-revoke | DELETE | /acl/grants | body | acl-revoke (gated: caller needs `role-management`) |
|
|
155
|
+
| acl-grant-search | GET | /acl/grants/search | query | acl-grant-search — access search (`?q=&page=&limit=` → `PaginationModel<AclGrant>`) |
|
|
156
|
+
| acl-check | GET | /acl/check | query | acl-check-action |
|
|
142
157
|
| acl-list-resources-by-user | GET | /acl/resources | query | acl-list-resources-by-user (+ `auth:`) |
|
|
143
158
|
|
|
144
159
|
```yaml
|
|
@@ -153,18 +168,18 @@ gateway:
|
|
|
153
168
|
action: acl-role-update
|
|
154
169
|
mode: rpc
|
|
155
170
|
- name: acl-grant # body: { userId, roles, resourceId?, companyId?, friendlyName? }
|
|
156
|
-
method: POST
|
|
171
|
+
method: POST # gated: caller (X-GTW-AUTH-USERID) needs role-management on target
|
|
157
172
|
path: /acl/grants
|
|
158
173
|
dataSource: body
|
|
159
174
|
topic: rlb-acl
|
|
160
175
|
action: acl-grant
|
|
161
176
|
mode: rpc
|
|
162
|
-
- name: acl-check
|
|
177
|
+
- name: acl-check # ?userId=&action=read-doc&companyId=&resourceId= → 200 true/false
|
|
163
178
|
method: GET
|
|
164
179
|
path: /acl/check
|
|
165
180
|
dataSource: query
|
|
166
181
|
topic: rlb-acl
|
|
167
|
-
action: acl-
|
|
182
|
+
action: acl-check-action
|
|
168
183
|
mode: rpc
|
|
169
184
|
- name: acl-list-resources-by-user # auth-gated; userId from X-GTW-AUTH-USERID
|
|
170
185
|
method: GET
|
|
@@ -180,6 +195,9 @@ gateway:
|
|
|
180
195
|
|
|
181
196
|
- topic `rlb-acl` + its queue declared on the consuming service; gateway paths use the
|
|
182
197
|
literal `action` strings above.
|
|
183
|
-
-
|
|
184
|
-
`IAclRoleService` (`AclService`). Auth-provider needs `uidClaim`
|
|
198
|
+
- action-gated routes (`actions: [...]`) → `RLB_GTW_ACL_ROLE_SERVICE` bound to an
|
|
199
|
+
`IAclRoleService` (`AclService`, `checkAction`). Auth-provider needs `uidClaim`
|
|
200
|
+
(+ `headerPrefix`).
|
|
201
|
+
- `acl-grant`/`acl-revoke` are gated — seed the first `role-management` grant directly in
|
|
202
|
+
the DB or every caller gets `403`.
|
|
185
203
|
- a check returning `false` is a **200**, not an error.
|
|
@@ -97,8 +97,9 @@ Two independent pairings sit on the method, each only needed in the multi case:
|
|
|
97
97
|
`@BrokerAuth` is **public**.
|
|
98
98
|
|
|
99
99
|
Auth lives in a separate, decoupled decorator —
|
|
100
|
-
`@BrokerAuth(authName, allowAnonymous?,
|
|
101
|
-
This lets two HTTP paths for the SAME
|
|
100
|
+
`@BrokerAuth(authName, allowAnonymous?, actions?, httpName?)` — never inside `@BrokerHTTP`'s options.
|
|
101
|
+
The 3rd param is `actions` (ACL action names, was `roles`). This lets two HTTP paths for the SAME
|
|
102
|
+
action carry DIFFERENT auth.
|
|
102
103
|
|
|
103
104
|
Simple case — one route, auth auto-pairs (no names needed):
|
|
104
105
|
|
|
@@ -118,7 +119,7 @@ Multi case — two routes for ONE action, each name-paired to its own auth:
|
|
|
118
119
|
@BrokerHTTP('GET', '/bookings/:id', 'params', { name: 'get-booking' })
|
|
119
120
|
@BrokerAuth('cust-jwks', true, undefined, 'get-booking') // httpName ⇄ route name
|
|
120
121
|
@BrokerHTTP('GET', '/admin/bookings/:id', 'params', { name: 'admin-get-booking' })
|
|
121
|
-
@BrokerAuth('admin-jwks', undefined, ['admin'], 'admin-get-booking')
|
|
122
|
+
@BrokerAuth('admin-jwks', undefined, ['booking.admin'], 'admin-get-booking') // 3rd param = ACL actions
|
|
122
123
|
async getBooking(@BrokerParam('params', 'id') id: string) {
|
|
123
124
|
return this.bookings.find(id);
|
|
124
125
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: rlb-amqp-add-route
|
|
3
|
-
description: Expose a broker action over HTTP through the @open-rlb/nestjs-amqp gateway by adding a gateway.paths[] entry. Use when the user wants a new HTTP endpoint/REST route that forwards to a topic/action, choosing rpc (wait reply) vs event (fire-and-forget with confirm), with auth,
|
|
3
|
+
description: Expose a broker action over HTTP through the @open-rlb/nestjs-amqp gateway by adding a gateway.paths[] entry. Use when the user wants a new HTTP endpoint/REST route that forwards to a topic/action, choosing rpc (wait reply) vs event (fire-and-forget with confirm), with auth, actions (ACL gate), dataSource, timeout, file upload or raw body. Generates the YAML path fragment and flags required bootstrap/ACL wiring.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Add an HTTP gateway route (gateway.paths[])
|
|
@@ -20,7 +20,9 @@ Canonical example: `sample/config-sample/gateway-in-memory/config/config.yaml`.
|
|
|
20
20
|
- **dataSource**: how the payload is assembled — `req.params` are ALWAYS merged in, plus:
|
|
21
21
|
`body` | `query` | `params` | `body-query` (body wins) | `query-body` (query wins).
|
|
22
22
|
- **auth**: an `auth-provider` name (validates the request, maps claims to `X-GTW-AUTH-*`
|
|
23
|
-
headers). `allowAnonymous: true` skips the gate. `
|
|
23
|
+
headers). `allowAnonymous: true` skips the gate. `actions: [...]` adds an ACL action check
|
|
24
|
+
scoped to the request's `(companyId, resourceId)` (read from the canonical fields,
|
|
25
|
+
params → query → body).
|
|
24
26
|
- Extras: `timeout` (rpc), `successStatusCode`, `binary`, `redirect`, `parseRaw`, static
|
|
25
27
|
`headers`, `forwardHeaders`.
|
|
26
28
|
|
|
@@ -36,7 +38,7 @@ Canonical example: `sample/config-sample/gateway-in-memory/config/config.yaml`.
|
|
|
36
38
|
| `dataSource` | `body` \| `query` \| `params` \| `body-query` \| `query-body`. |
|
|
37
39
|
| `auth` | Auth-provider name; validates + maps claims. |
|
|
38
40
|
| `allowAnonymous` | `true` → gate skipped (token still mapped if present & valid). |
|
|
39
|
-
| `
|
|
41
|
+
| `actions` | ACTION NAMES; caller passes holding AT LEAST ONE on the request's `(companyId, resourceId)`. Requires `auth`. |
|
|
40
42
|
| `timeout` | RPC timeout (ms), `rpc` only. |
|
|
41
43
|
| `binary` | Treat a raw (non-JSON) RPC reply as base64 → binary body. |
|
|
42
44
|
| `parseRaw` | Adds the raw request body as `$raw` (needs `rawBody: true`). |
|
|
@@ -60,7 +62,7 @@ gateway:
|
|
|
60
62
|
action: <action>
|
|
61
63
|
mode: rpc # or event
|
|
62
64
|
auth: gateway-jwks # optional
|
|
63
|
-
|
|
65
|
+
actions: [resource.write] # optional → needs RLB_GTW_ACL_ROLE_SERVICE; checked on (companyId, resourceId)
|
|
64
66
|
timeout: 7000 # rpc only
|
|
65
67
|
successStatusCode: 201
|
|
66
68
|
```
|
|
@@ -71,15 +73,18 @@ For every request the gateway runs `processAuthData` (best-effort), then:
|
|
|
71
73
|
|
|
72
74
|
1. **`allowAnonymous: true`** → gate SKIPPED. A valid token still gets its claims mapped &
|
|
73
75
|
forwarded; a missing/invalid token is NOT blocked.
|
|
74
|
-
2. **`auth` set, no `
|
|
76
|
+
2. **`auth` set, no `actions`** → authentication only. Provider must validate (else `401`);
|
|
75
77
|
on success the `X-GTW-AUTH-*` headers are forwarded downstream.
|
|
76
|
-
3. **`auth` + `
|
|
77
|
-
id from the provider's `uidClaim
|
|
78
|
-
|
|
78
|
+
3. **`auth` + `actions`** → authn + action auth. After a valid token the gateway reads the
|
|
79
|
+
user id from the provider's `uidClaim`, extracts `(companyId, resourceId)` from the request
|
|
80
|
+
(canonical fields, params → query → body), and calls
|
|
81
|
+
`IAclRoleService.checkAction(userId, { companyId, resourceId }, actions)` in-process. Passes
|
|
82
|
+
if the caller holds at least one of `actions` on that pair, else `403`. The check is
|
|
83
|
+
**exact-match on `(companyId, resourceId)` — there is no wildcard**, and `companyId` is
|
|
84
|
+
load-bearing.
|
|
79
85
|
|
|
80
|
-
> `
|
|
81
|
-
> `403`, logged loudly at boot).
|
|
82
|
-
> the gateway — it lives on the target microservice.
|
|
86
|
+
> `actions` WITHOUT `auth` is a misconfiguration: no identity → fails closed (every request
|
|
87
|
+
> `403`, logged loudly at boot).
|
|
83
88
|
|
|
84
89
|
## Status mapping
|
|
85
90
|
|
|
@@ -109,9 +114,10 @@ For every request the gateway runs `processAuthData` (best-effort), then:
|
|
|
109
114
|
## Required wiring to flag
|
|
110
115
|
|
|
111
116
|
- If `parseRaw: true` → bootstrap with `NestFactory.create(AppModule, { rawBody: true })`.
|
|
112
|
-
- If `
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
- If `actions` is used → an `IAclRoleService` (`checkAction`) must be registered via
|
|
118
|
+
`RLB_GTW_ACL_ROLE_SERVICE` in
|
|
119
|
+
`ProxyModule.forRootAsync({ providers: [{ provide: RLB_GTW_ACL_ROLE_SERVICE, useExisting: AclService }] })`.
|
|
120
|
+
If a path declares `actions` and the service is NOT registered → request DENIED (`403`) +
|
|
115
121
|
error logged. The auth-provider needs a `uidClaim` (+ `headerPrefix`) to resolve the userId.
|
|
116
122
|
- Forwarded auth claims reach the handler as prefixed/uppercased headers
|
|
117
123
|
(e.g. `X-GTW-AUTH-USERID`) — read them with `@BrokerParam('header', ...)`. Request headers
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: rlb-amqp-add-ws-event
|
|
3
|
-
description: Add a secure WebSocket event (or HTTP webhook) to the @open-rlb/nestjs-amqp gateway by adding a gateway.events[] entry. Use when the user wants to push broker messages to connected WebSocket clients or to a webhook, with authentication (token in subprotocol), per-event
|
|
3
|
+
description: Add a secure WebSocket event (or HTTP webhook) to the @open-rlb/nestjs-amqp gateway by adding a gateway.events[] entry. Use when the user wants to push broker messages to connected WebSocket clients or to a webhook, with authentication (token in subprotocol), per-event actions/ACL, and per-user scoping to avoid leaking other users' data. Generates the YAML event fragment plus the exchange/queue and ws options, and flags the security wiring.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Add a WebSocket / webhook event (gateway.events[])
|
|
@@ -27,14 +27,15 @@ each message out to the connected clients of EVERY gateway instance. Secure it b
|
|
|
27
27
|
required to subscribe.
|
|
28
28
|
- `requireAuth: false` → makes `auth` optional (anonymous allowed; claims mapped if a token
|
|
29
29
|
is present — handy with `scopeClaim`). Defaults to `true` when `auth` is set.
|
|
30
|
-
- `
|
|
30
|
+
- `actions: [...]` → ACL action check via `IAclRoleService.checkAction` (needs `IAclRoleService`);
|
|
31
|
+
requires `auth` for the identity. WS events gate **resource-agnostically** (both ids absent).
|
|
31
32
|
- `scopeClaim` + `payloadKey` → per-user isolation: a client only receives messages where
|
|
32
33
|
`payload[payloadKey] === claims[scopeClaim]`. `scopeClaim` is the MAPPED claim
|
|
33
34
|
(with `headerPrefix`, e.g. `X-GTW-AUTH-USERID`). Without `payloadKey` it denies all
|
|
34
35
|
(gotcha 16). With `auth` but no `scopeClaim`/`payloadKey`, every authorized subscriber
|
|
35
36
|
gets ALL messages (warned at boot).
|
|
36
37
|
|
|
37
|
-
> Auth/
|
|
38
|
+
> Auth/actions/scope are declared PER-EVENT. `gateway.ws` only holds connection-level limits,
|
|
38
39
|
> heartbeat, origin allowlist and message-size cap (no auth fields). Different events may use
|
|
39
40
|
> different providers.
|
|
40
41
|
|
|
@@ -57,7 +58,7 @@ gateway:
|
|
|
57
58
|
routingKey: orders.#
|
|
58
59
|
auth: gateway-jwks # verifies token + maps claims for this event
|
|
59
60
|
requireAuth: true # default true when auth is set; false → optional
|
|
60
|
-
|
|
61
|
+
actions: [orders.read] # optional → needs IAclRoleService (checkAction); resource-agnostic for WS
|
|
61
62
|
scopeClaim: X-GTW-AUTH-USERID # optional per-user scoping (MAPPED claim)
|
|
62
63
|
payloadKey: userId # message field compared to scopeClaim
|
|
63
64
|
|
|
@@ -89,8 +90,8 @@ broker:
|
|
|
89
90
|
`sample/config-sample/gateway-in-memory/src/main.ts`).
|
|
90
91
|
- `events[].auth` must reference a `jwt`/`jwks` provider; subscribing without a valid token
|
|
91
92
|
yields `{ topic:'onError', data:{ event, error:'unauthorized' } }` (unless `requireAuth:false`).
|
|
92
|
-
A failed
|
|
93
|
-
- `
|
|
93
|
+
A failed action check yields `error:'forbidden'`.
|
|
94
|
+
- `actions` → `IAclRoleService` (`checkAction`) via `RLB_GTW_ACL_ROLE_SERVICE` in
|
|
94
95
|
`ProxyModule.forRootAsync({ providers: [...] })` (gotcha 15).
|
|
95
96
|
- Do NOT add a fixed durable queue for the event — the lib creates a per-instance exclusive
|
|
96
97
|
ephemeral auto-delete queue for fan-out (gotcha 17).
|
|
@@ -43,7 +43,21 @@ GatewayAdminModule.forRoot([
|
|
|
43
43
|
|
|
44
44
|
Use `forRootAsync` to resolve the **consumer-side** `routeDiscovery { exchange, queue }`
|
|
45
45
|
from config (see Route auto-discovery). Exports `GatewayPathService`,
|
|
46
|
-
`GatewayAuthService`, `GatewayMetricsService
|
|
46
|
+
`GatewayAuthService`, `GatewayMetricsService`, `GatewayMetricsRollupService`,
|
|
47
|
+
`GatewayHealthService`, `GatewayRetentionService`.
|
|
48
|
+
|
|
49
|
+
**Options also carry `retentionDays`** (default `90` ≈ 3 months; `0`/negative disables) **and
|
|
50
|
+
`rollupRetentionDays`** (default `365` ≈ 1 year; `0`/negative disables rollups). A daily
|
|
51
|
+
`GatewayRetentionService` job prunes the route journal + raw metric points (`retentionDays`, via
|
|
52
|
+
`prune`/`prunePoints`) and the hourly rollups (`rollupRetentionDays`, via `pruneRollups`). An hourly
|
|
53
|
+
`GatewayMetricsRollupService` downsamples the previous hour's raw points into persisted
|
|
54
|
+
`HttpMetricRollup` rows (`recordRollups`) so long-term trends survive raw-point pruning;
|
|
55
|
+
`gw-metrics-rollups` reads them back via `rollupSeries`.
|
|
56
|
+
|
|
57
|
+
**New repo contract methods consumers must implement:** every `*Repository.search(q?, page?, limit?)`
|
|
58
|
+
now returns `Promise<PaginationModel<T>>` (not a bare array); `RouteSyncLogRepository.query(filter,
|
|
59
|
+
page?, limit?)` backs `gw-route-log-search`; `RouteSyncLogRepository.prune` + `HttpMetricRepository.prunePoints`
|
|
60
|
+
back retention.
|
|
47
61
|
|
|
48
62
|
## Broker topic + queue (required)
|
|
49
63
|
|
|
@@ -88,10 +102,13 @@ auth/ACL resources. `create` rejects a `(method, path)` collision (409).
|
|
|
88
102
|
| --- | --- | --- | --- | --- |
|
|
89
103
|
| POST | `/admin/paths` | `gw-path-create` | body | needs `name,method,path,topic`; 409 on collision |
|
|
90
104
|
| GET | `/admin/paths` | `gw-path-list` | query | `?page=&limit=` |
|
|
105
|
+
| GET | `/admin/paths/search` | `gw-path-search` | query | free-text → `PaginationModel<StoredHttpPath>`; `?q=&page=&limit=` |
|
|
91
106
|
| GET | `/admin/paths/export` | `gw-path-export` | query | enabled, ordered; used by `loadConfig.paths` |
|
|
92
107
|
| PUT | `/admin/paths` | `gw-path-update` | body | needs `id`; re-checks collisions |
|
|
93
108
|
| GET | `/admin/paths/get` | `gw-path-get` | query | `?id=` |
|
|
94
109
|
| DELETE | `/admin/paths` | `gw-path-delete` | body | `{ id }` |
|
|
110
|
+
| GET | `/admin/route-log` | `gw-route-log-list` | query | route journal (who changed what); `?limit=` |
|
|
111
|
+
| GET | `/admin/route-log/search` | `gw-route-log-search` | query | filtered + paginated journal → `PaginationModel<RouteSyncLogEntry>`; `?actor=&service=&event=&routeKey=&from=&to=&page=&limit=` |
|
|
95
112
|
|
|
96
113
|
## Auth-provider management — `gw-auth-*` (name-keyed PUT-upsert)
|
|
97
114
|
|
|
@@ -104,6 +121,7 @@ DB-stored providers, ON TOP of the static `auth-providers[]` in YAML.
|
|
|
104
121
|
| PUT | `/admin/auth` | `gw-auth-update` | body | upsert by name; `{ name, type, ... }` |
|
|
105
122
|
| GET | `/admin/auth/get` | `gw-auth-get` | query | `?name=` |
|
|
106
123
|
| DELETE | `/admin/auth` | `gw-auth-delete` | body | `{ name }` |
|
|
124
|
+
| GET | `/admin/auth/search` | `gw-auth-search` | query | free-text → `PaginationModel<StoredAuthProvider>`; `?q=&page=&limit=` |
|
|
107
125
|
|
|
108
126
|
`gw-auth-export` (dump all enabled) also exists; not in the sample YAML.
|
|
109
127
|
|
|
@@ -111,12 +129,30 @@ DB-stored providers, ON TOP of the static `auth-providers[]` in YAML.
|
|
|
111
129
|
|
|
112
130
|
| Method | Path | action | mode | dataSource | Returns |
|
|
113
131
|
| --- | --- | --- | --- | --- | --- |
|
|
114
|
-
| GET | `/admin/metrics`
|
|
115
|
-
| GET | `/admin/metrics/series`
|
|
116
|
-
| GET | `/admin/metrics/points`
|
|
117
|
-
|
|
|
132
|
+
| GET | `/admin/metrics` | `gw-metrics-get` | rpc | query | counters/route (`count,errorCount,avgDurationMs,errorRate,lastErrorCode`); `?route=` |
|
|
133
|
+
| GET | `/admin/metrics/series` | `gw-metrics-series` | rpc | query | enriched buckets w/ `p50/p95/p99`+`byStatus`: `?bucketMs=60000&from=&to=&method=&route=&name=` |
|
|
134
|
+
| GET | `/admin/metrics/points` | `gw-metrics-points` | rpc | query | raw points (incl. error `code`) newest-first: `?method=&route=&from=&to=&limit=` |
|
|
135
|
+
| GET | `/admin/metrics/summary` | `gw-metrics-summary` | rpc | query | overview `MetricSummary` (totals,errorRate,p50/95/99,byStatus,top-N): `?from=&to=&method=&route=&name=&topN=10` |
|
|
136
|
+
| GET | `/admin/metrics/prometheus` | `gw-metrics-prometheus` | rpc | query | Prometheus text exposition of counters (`text/plain`) |
|
|
137
|
+
| GET | `/admin/metrics/rollups` | `gw-metrics-rollups` | rpc | query | hourly rollups (`HttpMetricRollup[]`, survive retention): `?from=&to=&method=&route=` |
|
|
138
|
+
| POST | `/admin/metrics/track` | `gw-metrics-track` | event | body | fire-and-forget sink (wired via `gateway.metrics`) |
|
|
139
|
+
|
|
140
|
+
`series`/`summary` are computed **app-side from raw points** (latency percentiles are exact); the
|
|
141
|
+
counters stay O(1) per route. `prometheus` needs the route's `headers: { Content-Type: text/plain }`.
|
|
142
|
+
|
|
143
|
+
`gw-health` is a **readiness** probe (NOT a metrics dump). Returns
|
|
144
|
+
`{ status: 'up'|'down', broker: { status, detail? }, dependencies: { <name>: { status, detail? } } }`;
|
|
145
|
+
`status` is `'down'` if the broker OR any dependency is down. The broker (AmqpConnection) is checked
|
|
146
|
+
built-in; DB/redis/external checks are **consumer-supplied** via `RLB_GW_HEALTH_INDICATORS` (array of
|
|
147
|
+
`GatewayHealthIndicator { name; check(): Promise<{status:'up'|'down', detail?}> }`, both exported from
|
|
148
|
+
`@open-rlb/nestjs-amqp`). The HTTP response is **always 200** (the gateway forwards an rpc result,
|
|
149
|
+
can't set 503) — readiness must inspect `status`.
|
|
118
150
|
|
|
119
|
-
|
|
151
|
+
```ts
|
|
152
|
+
{ provide: RLB_GW_HEALTH_INDICATORS, useValue: [
|
|
153
|
+
{ name: 'database', check: async () => ({ status: 'up' }) },
|
|
154
|
+
] satisfies GatewayHealthIndicator[] }
|
|
155
|
+
```
|
|
120
156
|
|
|
121
157
|
```yaml
|
|
122
158
|
- name: health
|
|
@@ -203,7 +239,7 @@ routes over the same action can publish with different auth — a route with no
|
|
|
203
239
|
|
|
204
240
|
```ts
|
|
205
241
|
@BrokerHTTP('GET', '/admin/bookings/:id', 'params', { name: 'admin-get-booking' })
|
|
206
|
-
@BrokerAuth('admin-jwks', undefined, ['admin'], 'admin-get-booking') // pairs by httpName
|
|
242
|
+
@BrokerAuth('admin-jwks', undefined, ['booking.admin'], 'admin-get-booking') // 3rd param = ACL actions; pairs by httpName
|
|
207
243
|
```
|
|
208
244
|
|
|
209
245
|
### Consumer (gateway ← microservice) — `GatewayAdminModule` `routeDiscovery`
|
|
@@ -212,8 +248,12 @@ Wired by `GatewayAdminModule` (NOT YAML). Asserts the fanout exchange, subscribe
|
|
|
212
248
|
to the durable queue (competing consumers), then per manifest: diffs vs DB scoped
|
|
213
249
|
to the publishing service, applies only changes (insert/update; soft-disable stale
|
|
214
250
|
to `enabled:false`; skip collisions — existing owner keeps `(method,path)`),
|
|
215
|
-
journals every event via `RouteSyncLogRepository
|
|
216
|
-
|
|
251
|
+
journals every event via `RouteSyncLogRepository` (each row has `actor:'system'`;
|
|
252
|
+
`skipped` rows mark user-modified routes left untouched; `updated` rows carry a
|
|
253
|
+
`changes` per-field diff like `actions: [+x, -y]`), and publishes `gw-reload` when
|
|
254
|
+
anything changed. Never throws (acks; no poison loop). Stored routes carry `source`
|
|
255
|
+
(`'microservice'|'user'`) and `modified`; user CRUD (`gw-path-*`) is audited too —
|
|
256
|
+
`actor`=`X-GTW-AUTH-USERID` (else `'unknown'`), event `created`/`updated`/`deleted`.
|
|
217
257
|
|
|
218
258
|
```ts
|
|
219
259
|
GatewayAdminModule.forRootAsync({
|
|
@@ -277,10 +277,10 @@ export class AppService {
|
|
|
277
277
|
}
|
|
278
278
|
```
|
|
279
279
|
|
|
280
|
-
Add auth with `@BrokerAuth(authName, allowAnonymous?,
|
|
281
|
-
`@BrokerHTTP
|
|
282
|
-
each `@BrokerHTTP` sets a `name` and each `@BrokerAuth`
|
|
283
|
-
with no `@BrokerAuth` is public.
|
|
280
|
+
Add auth with `@BrokerAuth(authName, allowAnonymous?, actions?, httpName?)` — decoupled from
|
|
281
|
+
`@BrokerHTTP` (3rd param is ACL action names). With one `@BrokerHTTP` it auto-pairs (no
|
|
282
|
+
`httpName` needed); with multiple, each `@BrokerHTTP` sets a `name` and each `@BrokerAuth`
|
|
283
|
+
targets it via `httpName`. A route with no `@BrokerAuth` is public.
|
|
284
284
|
|
|
285
285
|
## Verify
|
|
286
286
|
- topic/queue/exchange names line up across `broker`/`topics`/paths (gotchas 5–7);
|