@open-rlb/nestjs-amqp 2.0.3 → 2.0.5
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 +4 -2
- package/amqp-lib/config/rabbitmq.config.d.ts +6 -0
- package/modules/acl/const.d.ts +1 -4
- package/modules/acl/const.js +1 -4
- package/modules/acl/const.js.map +1 -1
- package/modules/acl/models.d.ts +5 -7
- package/modules/acl/repository/acl-action.repository.d.ts +1 -5
- package/modules/acl/repository/acl-action.repository.js.map +1 -1
- package/modules/acl/repository/acl-grant.repository.d.ts +0 -1
- package/modules/acl/repository/acl-grant.repository.js.map +1 -1
- package/modules/acl/repository/acl-role.repository.d.ts +1 -5
- package/modules/acl/repository/acl-role.repository.js.map +1 -1
- package/modules/acl/services/acl-management.service.d.ts +6 -7
- package/modules/acl/services/acl-management.service.js +44 -61
- package/modules/acl/services/acl-management.service.js.map +1 -1
- package/modules/acl/services/acl.service.d.ts +1 -3
- package/modules/acl/services/acl.service.js +5 -47
- package/modules/acl/services/acl.service.js.map +1 -1
- package/modules/broker/broker.module.d.ts +2 -4
- package/modules/broker/broker.module.js +23 -5
- package/modules/broker/broker.module.js.map +1 -1
- package/modules/broker/config/route-discovery.config.d.ts +2 -0
- package/modules/broker/const.d.ts +1 -0
- package/modules/broker/const.js +2 -1
- package/modules/broker/const.js.map +1 -1
- package/modules/broker/services/broker.service.js +1 -1
- package/modules/broker/services/broker.service.js.map +1 -1
- package/modules/broker/services/route-discovery-publisher.service.js +7 -5
- package/modules/broker/services/route-discovery-publisher.service.js.map +1 -1
- package/modules/gateway-admin/config/gateway-admin.config.d.ts +4 -0
- package/modules/gateway-admin/const.d.ts +1 -1
- package/modules/gateway-admin/const.js +1 -1
- package/modules/gateway-admin/const.js.map +1 -1
- package/modules/gateway-admin/gateway-admin.module.d.ts +7 -1
- package/modules/gateway-admin/gateway-admin.module.js +13 -0
- package/modules/gateway-admin/gateway-admin.module.js.map +1 -1
- package/modules/gateway-admin/repository/auth-provider.repository.d.ts +3 -4
- package/modules/gateway-admin/repository/auth-provider.repository.js.map +1 -1
- package/modules/gateway-admin/services/gateway-auth.service.d.ts +3 -4
- package/modules/gateway-admin/services/gateway-auth.service.js +15 -23
- package/modules/gateway-admin/services/gateway-auth.service.js.map +1 -1
- package/modules/gateway-admin/services/gateway-metrics.service.d.ts +3 -0
- package/modules/gateway-admin/services/gateway-metrics.service.js +9 -0
- package/modules/gateway-admin/services/gateway-metrics.service.js.map +1 -1
- package/modules/gateway-admin/services/route-sync.service.d.ts +3 -1
- package/modules/gateway-admin/services/route-sync.service.js +14 -8
- package/modules/gateway-admin/services/route-sync.service.js.map +1 -1
- package/modules/proxy/services/http-handler.service.d.ts +3 -0
- package/modules/proxy/services/http-handler.service.js +28 -4
- package/modules/proxy/services/http-handler.service.js.map +1 -1
- package/package.json +5 -1
- package/schematics/nest-add/files/acl/src/cache/in-memory-acl-store.ts +54 -0
- package/schematics/nest-add/files/acl/src/modules/database/repository/acl.repository.ts +74 -0
- package/schematics/nest-add/files/db-core/src/modules/database/repository/in-memory-collection.ts +122 -0
- package/schematics/nest-add/files/gateway-admin/src/modules/database/repository/gateway.repository.ts +151 -0
- package/schematics/nest-add/files/gateway-admin/src/modules/database/repository/route-sync.repository.ts +15 -0
- package/schematics/nest-add/files/skills/rlb-amqp/SKILL.md +30 -5
- package/schematics/nest-add/files/skills/rlb-amqp/references/config-schema.md +182 -93
- package/schematics/nest-add/files/skills/rlb-amqp/references/gotchas.md +120 -79
- package/schematics/nest-add/files/skills/rlb-amqp-acl/SKILL.md +185 -0
- package/schematics/nest-add/files/skills/rlb-amqp-add-action/SKILL.md +49 -2
- package/schematics/nest-add/files/skills/rlb-amqp-add-route/SKILL.md +82 -19
- package/schematics/nest-add/files/skills/rlb-amqp-add-ws-event/SKILL.md +40 -18
- package/schematics/nest-add/files/skills/rlb-amqp-gateway-admin/SKILL.md +233 -0
- package/schematics/nest-add/files/skills/rlb-amqp-scaffold/SKILL.md +172 -42
- package/schematics/nest-add/index.js +612 -142
- package/schematics/nest-add/index.js.map +1 -1
- package/schematics/nest-add/index.ts +673 -241
- package/schematics/nest-add/init.schema.d.ts +10 -1
- package/schematics/nest-add/init.schema.ts +29 -3
- package/schematics/nest-add/schema.json +37 -8
|
@@ -1,104 +1,145 @@
|
|
|
1
1
|
# Gotchas — bug-prone cases checklist
|
|
2
2
|
|
|
3
|
-
Scan this before adding/changing a topic, queue, exchange, action, route, auth provider
|
|
4
|
-
or
|
|
3
|
+
Scan this before adding/changing a topic, queue, exchange, action, route, auth provider,
|
|
4
|
+
WS event, or route-discovery wiring. Each item is a real failure mode in this codebase.
|
|
5
|
+
Ported from `docs/gotchas.md` (re-verified against post-2.0.5 code).
|
|
5
6
|
|
|
6
7
|
## Decorators & handlers
|
|
7
8
|
1. **No destructuring in `@BrokerAction` parameters.** Param→message mapping parses the
|
|
8
|
-
function source with a regex (`getParamNames`). `fn({a,b})` misaligns indices
|
|
9
|
-
params.
|
|
9
|
+
function source with a regex (`getParamNames`). `fn({a,b})` misaligns indices → params
|
|
10
|
+
arrive `undefined`. Use flat params + an explicit `@BrokerParam` name on each.
|
|
10
11
|
2. **Avoid default parameter values.** Only a basic `= value` strip exists
|
|
11
12
|
(`removeDefaultsFromParams`); complex defaults misalign mapping. Always pass an explicit
|
|
12
13
|
`name` to `@BrokerParam`.
|
|
13
|
-
3. **`(topic, action)` must be unique.** All `@BrokerAction` of a topic share ONE
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
3. **`(topic, action)` must be unique.** All `@BrokerAction` of a topic share ONE consumer/queue,
|
|
15
|
+
dispatched by `action`. A duplicate `(topic, action)` overwrites the previous one silently —
|
|
16
|
+
no error, the old handler just stops being called.
|
|
16
17
|
4. **Forwarded headers are UPPERCASE + prefixed.** Read `@BrokerParam('header',
|
|
17
|
-
'X-GTW-AUTH-USERID')`, not `'userId'`.
|
|
18
|
+
'X-GTW-AUTH-USERID')`, not `'userId'`. The exact name = provider `headerPrefix` + `uidClaim`.
|
|
19
|
+
5. **`handle`/`broadcast` handlers must return `void`.** A return value logs
|
|
20
|
+
`Subscribe handlers should only return void`. Only `rpc` handlers return data.
|
|
21
|
+
6. **One method, multiple `@BrokerAction`s:** any `@BrokerHTTP` / `@BrokerAuth` on that method
|
|
22
|
+
MUST name its `action` to pair deterministically — decorator order is never used.
|
|
18
23
|
|
|
19
24
|
## Topic ↔ queue ↔ exchange wiring
|
|
20
|
-
|
|
25
|
+
7. **The topic `name` must match everywhere**: `@BrokerAction`, `topics[].name`,
|
|
21
26
|
`requestData`/`publishMessage`, `gateway.paths[].topic` / `events[]`. Typo →
|
|
22
27
|
`Topic X not found in configuration`.
|
|
23
|
-
|
|
28
|
+
8. **`mode: rpc`/`handle` need `topics[].queue` in `broker.queues[]`**, and that queue's
|
|
24
29
|
`exchange` in `broker.exchanges[]`. In `handle` a missing queue throws NPE at boot
|
|
25
30
|
(`queue.exchange`).
|
|
26
|
-
|
|
27
|
-
`Queue ... has no routing key`.
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
9. **Exchange `type: topic` → queue MUST have `routingKey`**, else boot throws
|
|
32
|
+
`Queue ... has no routing key`. (The samples use a `direct` exchange with matching keys.)
|
|
33
|
+
10. **The gateway can only forward to topics it declares.** Route auto-discovery teaches the
|
|
34
|
+
gateway the HTTP route, NOT how to reach the microservice's broker topic. That topic (+ its
|
|
35
|
+
queue/exchange) must ALSO exist in the **gateway's own** `broker` config, or the forwarded
|
|
36
|
+
request fails with `Topic ... not found in configuration`.
|
|
37
|
+
|
|
38
|
+
## connection_name (broadcast / WebSocket / route-discovery)
|
|
39
|
+
11. **`broadcast` + WebSocket require `connection_name`** (`clientProperties.connection_name`,
|
|
40
|
+
or `broker.routeDiscovery.serviceName` which fills it when unset), else throw at boot.
|
|
41
|
+
12. **Every instance needs a DISTINCT `connection_name`.** Sharing it makes RabbitMQ treat the
|
|
42
|
+
per-instance queues as one consumer group and **round-robin** broadcast/WS messages — reloads
|
|
43
|
+
land on "every other" instance, WS clients miss events delivered to the other one.
|
|
30
44
|
|
|
31
45
|
## RPC / timeout / errors
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
13. **Wrong `replyQueues` key → silent timeout.** `requestData` resolves `replyTo` from
|
|
47
|
+
`broker.replyQueues[exchange]`; absent → RabbitMQ direct-reply-to. Wrong exchange key → no
|
|
48
|
+
reply routed back → the call just times out.
|
|
49
|
+
14. **Handler exceptions don't crash the consumer.** Returned as `{success:false,error}`;
|
|
50
|
+
`requestData` re-throws on the caller side. Gateway HTTP status derives from `error.name` —
|
|
51
|
+
give errors a meaningful `name` (`BadRequestError`, `NotFoundError`, `ConflictError`,
|
|
52
|
+
`ForbiddenError`, `UnauthorizedError`); anything unrecognized → 500.
|
|
53
|
+
15. **Default RPC timeout 10s** (`broker.defaultRpcTimeout`). Override per route (`paths[].timeout`)
|
|
54
|
+
or per `requestData` call for slow RPCs.
|
|
39
55
|
|
|
40
56
|
## Gateway HTTP
|
|
41
|
-
|
|
42
|
-
is `undefined
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
16. **Boolean RPCs return `200` with `true`/`false`, not `204`.** A *defined* result — including
|
|
58
|
+
falsy `false`/`0`/`""` — is real content, sent as `200` + JSON. Only `null`/`undefined`
|
|
59
|
+
collapses to `204`. So `GET /acl/check?...` answers `200 false` for "no" — don't treat any
|
|
60
|
+
2xx as "allowed", read the body. (The old "always 204" bug is fixed.)
|
|
61
|
+
17. **`parseRaw: true` needs `NestFactory.create(AppModule, { rawBody: true })`** or `$raw` is
|
|
62
|
+
`undefined`.
|
|
63
|
+
18. **Route params win over body/query** (merged in last). Watch key collisions (`:id` vs `body.id`).
|
|
64
|
+
19. **Uploads live in `$files`** (multer `.any()`); buffers are converted to **binary strings** —
|
|
65
|
+
re-encode carefully on the consumer (`Buffer.from(str, 'binary')`).
|
|
66
|
+
20. **`/health` is a tiny liveness probe.** Action `gw-health` → `{ status: 'ok' }` (a real 200),
|
|
67
|
+
NOT a metrics dump. Use `/admin/metrics*` (`gw-metrics-*`) for metrics.
|
|
47
68
|
|
|
48
69
|
## Auth / ACL
|
|
49
|
-
|
|
50
|
-
`
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
`acl-can-user-do-gtw` → `canUserDoGtw(roles, userId)` (gateway
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
`
|
|
70
|
+
21. **`roles` require `auth` on the same path/event.** No `auth` → no identity → fails closed
|
|
71
|
+
(every request `403`, logged at boot). Always pair `roles: [...]` with `auth: <provider>`.
|
|
72
|
+
22. **`roles` require an `IAclRoleService`** registered via `RLB_GTW_ACL_ROLE_SERVICE` in
|
|
73
|
+
`ProxyModule.forRootAsync({ providers: [...] })`. Missing → deny (403). The gateway check is
|
|
74
|
+
**role-based, OR, resource-agnostic** (`canUserDoGtw(roles, userId)`): `roles` lists ROLE NAMES,
|
|
75
|
+
the user passes holding AT LEAST ONE. The provider only needs `uidClaim` (+ `headerPrefix`).
|
|
76
|
+
23. **Two ACL check actions on `rlb-acl`** (both cached, both HTTP GET → `200` true/false):
|
|
77
|
+
`acl-can-user-do-gtw` → `canUserDoGtw(roles, userId)` (gateway filter, OR, resource-agnostic,
|
|
78
|
+
`GET /acl/check`) and `acl-can-user-do` → `canUserDo(roles, userId, resource)` (**ms-side**;
|
|
79
|
+
a global grant OR a grant on that resource passes, `GET /acl/check-resource`).
|
|
80
|
+
24. **Actions, roles & auth-providers are NAME-KEYED. PUT upserts; there is NO POST.** The `name`
|
|
81
|
+
IS the key (no id). `PUT` creates-or-updates, `GET` lists, `GET .../get?name=` reads one,
|
|
82
|
+
`DELETE` removes by `name`. The old id-based ACL CRUD and `POST`-create endpoints are GONE.
|
|
83
|
+
(Gateway-admin **paths** are the exception — they keep id-keyed CRUD and a POST create.)
|
|
84
|
+
25. **`acl-grant` / `acl-revoke` both REQUIRE `userId` + `roles`** (optional `resourceId` +
|
|
85
|
+
`companyId`). `grant` MERGES roles into the single `(userId, resourceId)` record (idempotent).
|
|
86
|
+
`revoke` REMOVES exactly those roles and **deletes the record once it has no roles left**.
|
|
87
|
+
`revoke` without `roles` throws `400 roles are required` — to wipe a grant, revoke all its roles.
|
|
88
|
+
26. **`companyId` is grouping metadata only.** It replaced `resourceBusinessId` and plays NO part
|
|
89
|
+
in authorization — it only groups resources in `acl-list-resources-by-user`. Targeting is by
|
|
90
|
+
`(userId, resourceId)` only. Both grant/revoke validate every role exists (unknown → `400`).
|
|
91
|
+
27. **Removed actions:** `acl-list-by-user` and `acl-verify-access` no longer exist. Use
|
|
92
|
+
`acl-can-user-do` for resource-scoped checks and `acl-list-resources-by-user` to list resources.
|
|
93
|
+
28. **Auth & gateway config go to `ProxyModule`** (`authOptions` / `gatewayOptions`), not
|
|
94
|
+
`BrokerModule`. `BrokerModule` owns only `options` / `topics` / `appOptions`.
|
|
62
95
|
|
|
63
|
-
##
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
`
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
custom handshake headers.
|
|
96
|
+
## Auth providers (hardening)
|
|
97
|
+
29. **JWKS verifies TLS by default.** `httpsAllowUnauthorized: true` only for self-signed dev issuers.
|
|
98
|
+
30. **`algorithms` is REQUIRED for `jwt`/`jwks`.** Omitting denies verification (algorithm-confusion
|
|
99
|
+
guard). For `jwks` only asymmetric algs are allowed (`RS*`/`ES*`/`PS*`); `HS*`/`none` rejected.
|
|
100
|
+
31. **Define `jwtMap` or NO claims are forwarded.** Without it the token is still accepted
|
|
101
|
+
(`success:true`) but no identity headers go downstream — fail-safe, not a leak. Declare it to
|
|
102
|
+
emit `X-GTW-AUTH-USERID` and friends.
|
|
103
|
+
32. **`str-compare`/`basic` PASS THROUGH when their secret is unset — by design.** A `str-compare`
|
|
104
|
+
with no `secret`, or a `basic` with no `clientSecret`, authenticates EVERY request (effectively
|
|
105
|
+
open/disabled). Set the secret to enforce it.
|
|
106
|
+
33. **Credential `mechanism` must be `PLAIN` | `EXTERNAL` | `AMQPLAIN`** (case-insensitive). Unknown
|
|
107
|
+
value leaves SASL `response` unset → AMQP auth fails.
|
|
76
108
|
|
|
77
|
-
##
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
`
|
|
109
|
+
## WebSocket
|
|
110
|
+
34. **Auth is per-event, not global.** `events[].auth` names the provider that verifies the
|
|
111
|
+
connection token AND maps its claims for THAT event (memoized per provider at subscribe). `scopeClaim`
|
|
112
|
+
references the MAPPED claim (prefixed, e.g. `X-GTW-AUTH-USERID`), `payloadKey` is the payload
|
|
113
|
+
field. **`scopeClaim` without `payloadKey` denies everything** (safe default). `requireAuth:false`
|
|
114
|
+
makes `auth` optional (anon allowed; claims mapped if a token is present). `gateway.ws` carries
|
|
115
|
+
only connection-level limits/heartbeat — no auth fields.
|
|
116
|
+
35. **Don't bind a WS event to a fixed durable queue.** The lib creates a per-instance exclusive
|
|
117
|
+
ephemeral queue for fan-out; a shared/durable queue makes instances compete and clients on one
|
|
118
|
+
instance miss messages delivered to another.
|
|
119
|
+
36. **Token rides in the subprotocol**: `new WebSocket(url, [token])` (browsers can't set handshake
|
|
120
|
+
headers). The session is bounded by the JWT `exp` — closed with `1008` on expiry, nothing
|
|
121
|
+
delivered afterward. Long-lived clients need token refresh + reconnect.
|
|
122
|
+
37. **Set `gateway.ws.allowedOrigins`** to reject cross-site handshakes; omitted → all Origins
|
|
123
|
+
accepted (logged at boot). `maxMessageBytes` (default 16384) drops oversized client frames.
|
|
82
124
|
|
|
83
|
-
##
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
value leaves `response` unset → auth fails.
|
|
88
|
-
23. **`algorithms` is REQUIRED for `jwt`/`jwks`.** If omitted, verification is denied
|
|
89
|
-
(algorithm-confusion guard). For `jwks` only asymmetric algs are allowed (RS*/ES*/PS*);
|
|
90
|
-
`HS*`/`none` are rejected.
|
|
91
|
-
24. **`str-compare`/`basic` PASS THROUGH when their secret is unset.** A `str-compare`
|
|
92
|
-
without `secret` or a `basic` without `clientSecret` treats every request as authenticated
|
|
93
|
-
(provider effectively open/disabled — by design). Set the secret to actually enforce it.
|
|
94
|
-
25. **Define `jwtMap`.** Without it NO claims are forwarded (the token is still accepted,
|
|
95
|
-
`success:true`): the gateway fails safe instead of leaking the whole payload. Declare it
|
|
96
|
-
to forward identity headers (e.g. `X-GTW-AUTH-USERID`).
|
|
125
|
+
## Publish / events
|
|
126
|
+
38. **`publishMessage` is `async` — `await` it** for the publisher-confirm guarantee and to catch
|
|
127
|
+
failures. Un-awaited = fire-and-forget without guarantee. (For an `event`-mode route the gateway
|
|
128
|
+
awaits the confirm before returning the 2xx — the success status is not optimistic.)
|
|
97
129
|
|
|
98
|
-
##
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
130
|
+
## Reload & route auto-discovery
|
|
131
|
+
39. **The only reload action is `gw-reload`** (`GW_RELOAD_ACTION`). The control-topic subscriber
|
|
132
|
+
rebuilds routes ONLY for `gw-reload`; every other message on the control topic is ignored.
|
|
133
|
+
Seed then reload: `POST /admin/paths` → `POST /admin/reload` (publishes `gw-reload` on
|
|
134
|
+
`rlb-gateway-control`). No restart. Concurrent reloads are coalesced into one extra pass.
|
|
135
|
+
40. **Route-discovery config is SPLIT; exchange/queue MUST match on both sides.**
|
|
136
|
+
- **Publisher (microservice):** `broker.routeDiscovery { serviceName, publishOnBoot, exchange?, queue? }`.
|
|
137
|
+
`serviceName` is required to publish and also fills `connection_name` when unset.
|
|
138
|
+
- **Consumer (gateway):** `GatewayAdminModule` `routeDiscovery { exchange?, queue? }` (NEST code,
|
|
139
|
+
no `serviceName` — the gateway only receives).
|
|
140
|
+
Both default to `exchange: rlb-route-discovery` / `queue: rlb-route-sync`. Override only to
|
|
141
|
+
namespace per env — but set the SAME values on BOTH sides or manifests never reach the gateway.
|
|
142
|
+
41. **Topic NAMES `rlb-acl` / `rlb-gateway-admin` / `rlb-gateway-control` and all action strings
|
|
143
|
+
are decorator-bound and NOT configurable.** Only exchange/queue/routingKey and the
|
|
144
|
+
route-discovery exchange/queue are. The route-sync handler never throws (logs + acks, no poison
|
|
145
|
+
loop); an empty manifest soft-disables a service's existing routes (and logs a warning).
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rlb-amqp-acl
|
|
3
|
+
description: Manage role-based 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 roles, granting/revoking a user's roles, listing a user's resources, or answering authorization/permission questions (roles, grants, acl-check).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Manage ACL (@open-rlb/nestjs-amqp)
|
|
7
|
+
|
|
8
|
+
Read first when you need depth:
|
|
9
|
+
- `docs/acl.md` (model, wiring, URL table)
|
|
10
|
+
- `libs/rlb-nestjs-amqp/src/modules/acl/const.ts` (`ACL_ACTIONS`, `ACL_TOPIC`)
|
|
11
|
+
- `sample/config-sample/acl.yaml` (annotated broker + gateway reference)
|
|
12
|
+
- `sample/config-sample/gateway-in-memory/src/app.module.ts` (forRoot wiring)
|
|
13
|
+
|
|
14
|
+
Use when: managing **actions/roles/grants**, wiring `AclModule`, role-gating routes
|
|
15
|
+
(`roles: [...]`), or answering "can user do X".
|
|
16
|
+
|
|
17
|
+
## Model (3 entities)
|
|
18
|
+
|
|
19
|
+
- **Action** — atomic capability (`read-doc`). Name-keyed.
|
|
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** match on **roles, never action strings**.
|
|
23
|
+
|
|
24
|
+
## Decorator-bound (NOT configurable)
|
|
25
|
+
|
|
26
|
+
Topic NAME `rlb-acl` (`ACL_TOPIC`) and every action string are bound in the library —
|
|
27
|
+
reference them literally. The queue / exchange / routingKey that carry the topic ARE yours.
|
|
28
|
+
|
|
29
|
+
`ACL_ACTIONS`: `acl-action-list`, `acl-action-get`, `acl-action-update`,
|
|
30
|
+
`acl-action-delete`, `acl-role-list`, `acl-role-get`, `acl-role-update`,
|
|
31
|
+
`acl-role-delete`, `acl-grant`, `acl-revoke`, `acl-can-user-do-gtw`,
|
|
32
|
+
`acl-can-user-do`, `acl-list-resources-by-user`, `acl-invalidate`.
|
|
33
|
+
|
|
34
|
+
> **Removed in 2.0.5:** `acl-list-by-user`, `acl-verify-access`, `acl-create` /
|
|
35
|
+
> id-based ACL CRUD. Entities are name-keyed: **PUT upserts, no POST.**
|
|
36
|
+
|
|
37
|
+
## Actions & roles — name-keyed CRUD
|
|
38
|
+
|
|
39
|
+
No id, no POST. `PUT` upserts by `name` (idempotent), `GET` lists (`?page=&limit=`),
|
|
40
|
+
`GET …/get?name=` reads one, `DELETE` removes by `name`. Role upsert: every referenced
|
|
41
|
+
action must already exist (else **400**).
|
|
42
|
+
|
|
43
|
+
## Grants — dual grant/revoke
|
|
44
|
+
|
|
45
|
+
One record per `(userId, resourceId)`. Both ops **require `userId` + `roles`**;
|
|
46
|
+
`resourceId` + `companyId` are **optional**.
|
|
47
|
+
|
|
48
|
+
- `acl-grant` — merges roles into the pair (creates if absent; idempotent).
|
|
49
|
+
- `acl-revoke` — removes roles; deletes the record once empty.
|
|
50
|
+
- Both validate every role exists (unknown role → **400**) and invalidate the user's cache.
|
|
51
|
+
- `companyId` (replaced `resourceBusinessId`) is **grouping metadata only** — it groups
|
|
52
|
+
`acl-list-resources-by-user` output and plays **no part** in authorization.
|
|
53
|
+
|
|
54
|
+
## Checks — GET → 200 with `true`/`false`
|
|
55
|
+
|
|
56
|
+
`false` is real content; only `null`/`undefined` collapses to 204. Both return `false`
|
|
57
|
+
(never throw) on missing input or error.
|
|
58
|
+
|
|
59
|
+
- `acl-can-user-do-gtw` — resource-**agnostic**, the gateway's primary filter. `true` if
|
|
60
|
+
the user holds **≥1** requested role. Query: `?userId=&roles=user&roles=admin`.
|
|
61
|
+
- `acl-can-user-do` — resource-**scoped**: `true` if a **global** grant OR a grant bound
|
|
62
|
+
to that exact `resource` gives a matching role. Query: `?userId=&roles=admin&resource=doc-1`.
|
|
63
|
+
Normally called over the broker by the owning microservice.
|
|
64
|
+
- `acl-list-resources-by-user` — **auth-gated** (needs `auth`, no roles): reads `userId`
|
|
65
|
+
from the forwarded `X-GTW-AUTH-USERID` header; lists accessible resources grouped by
|
|
66
|
+
`companyId` with resolved actions.
|
|
67
|
+
|
|
68
|
+
## Nest wiring
|
|
69
|
+
|
|
70
|
+
Backend — `AclModule.forRoot([bindings], { cache })`. Bind the abstract repo tokens to
|
|
71
|
+
your concrete impls + optional L2 store; second arg carries TTLs. Module is **global**,
|
|
72
|
+
exports `AclService` + `AclCacheService`.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import {
|
|
76
|
+
AclModule, AclActionRepository, AclRoleRepository, AclGrantRepository,
|
|
77
|
+
RLB_ACL_CACHE_STORE,
|
|
78
|
+
} from '@open-rlb/nestjs-amqp';
|
|
79
|
+
|
|
80
|
+
AclModule.forRoot(
|
|
81
|
+
[
|
|
82
|
+
{ provide: AclActionRepository, useClass: MyAclActionRepository },
|
|
83
|
+
{ provide: AclRoleRepository, useClass: MyAclRoleRepository },
|
|
84
|
+
{ provide: AclGrantRepository, useClass: MyAclGrantRepository },
|
|
85
|
+
{ provide: RLB_ACL_CACHE_STORE, useClass: MyRedisAclCacheStore }, // OPTIONAL L2 (omit → RAM-only)
|
|
86
|
+
],
|
|
87
|
+
{ cache: { ramTtlMs: 30_000, l2TtlSec: 600 } }, // L1 RAM (default 30000) / L2 (default 600s)
|
|
88
|
+
);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Gateway side — let route `roles: [...]` filters run **in-process** (no broker hop) by
|
|
92
|
+
binding the gateway token to the same `AclService`:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { ProxyModule, AclService, RLB_GTW_ACL_ROLE_SERVICE } from '@open-rlb/nestjs-amqp';
|
|
96
|
+
|
|
97
|
+
ProxyModule.forRoot({
|
|
98
|
+
providers: [{ provide: RLB_GTW_ACL_ROLE_SERVICE, useExisting: AclService }],
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Same process → `useExisting`. Separate services → gateway RPCs `acl-can-user-do-gtw` on
|
|
103
|
+
`rlb-acl` instead. A route's `roles` are ROLE NAMES; the user passes with **≥1**.
|
|
104
|
+
|
|
105
|
+
## YAML — topic + queue (names fixed, transport yours)
|
|
106
|
+
|
|
107
|
+
```yaml
|
|
108
|
+
broker:
|
|
109
|
+
queues:
|
|
110
|
+
- name: rlb-acl # consumed by the ACL backend handlers
|
|
111
|
+
exchange: rlb
|
|
112
|
+
routingKey: rlb-acl
|
|
113
|
+
createQueueIfNotExists: true
|
|
114
|
+
options: { durable: true }
|
|
115
|
+
topics:
|
|
116
|
+
- name: rlb-acl # ACL_TOPIC — must match exactly
|
|
117
|
+
mode: rpc
|
|
118
|
+
queue: rlb-acl
|
|
119
|
+
exchange: rlb
|
|
120
|
+
routingKey: rlb-acl
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Gateway paths[] — full ACL table
|
|
124
|
+
|
|
125
|
+
Every path forwards to topic `rlb-acl`, `mode: rpc`. `name` is a free label; `action` is
|
|
126
|
+
the fixed library string.
|
|
127
|
+
|
|
128
|
+
| name | method | path | dataSource | action |
|
|
129
|
+
|---|---|---|---|---|
|
|
130
|
+
| acl-action-list | GET | /acl/actions | query | acl-action-list |
|
|
131
|
+
| acl-action-get | GET | /acl/actions/get | query | acl-action-get |
|
|
132
|
+
| acl-action-upsert | PUT | /acl/actions | body | acl-action-update |
|
|
133
|
+
| acl-action-delete | DELETE | /acl/actions | body | acl-action-delete |
|
|
134
|
+
| acl-role-list | GET | /acl/roles | query | acl-role-list |
|
|
135
|
+
| acl-role-get | GET | /acl/roles/get | query | acl-role-get |
|
|
136
|
+
| acl-role-upsert | PUT | /acl/roles | body | acl-role-update |
|
|
137
|
+
| acl-role-delete | DELETE | /acl/roles | body | acl-role-delete |
|
|
138
|
+
| acl-grant | POST | /acl/grants | body | acl-grant |
|
|
139
|
+
| acl-revoke | DELETE | /acl/grants | body | acl-revoke |
|
|
140
|
+
| acl-check-gtw | GET | /acl/check | query | acl-can-user-do-gtw |
|
|
141
|
+
| acl-check-resource | GET | /acl/check-resource | query | acl-can-user-do |
|
|
142
|
+
| acl-list-resources-by-user | GET | /acl/resources | query | acl-list-resources-by-user (+ `auth:`) |
|
|
143
|
+
|
|
144
|
+
```yaml
|
|
145
|
+
gateway:
|
|
146
|
+
mode: gateway
|
|
147
|
+
paths:
|
|
148
|
+
- name: acl-role-upsert # PUT upserts by name. body: { name, actions, description? }
|
|
149
|
+
method: PUT
|
|
150
|
+
path: /acl/roles
|
|
151
|
+
dataSource: body
|
|
152
|
+
topic: rlb-acl
|
|
153
|
+
action: acl-role-update
|
|
154
|
+
mode: rpc
|
|
155
|
+
- name: acl-grant # body: { userId, roles, resourceId?, companyId?, friendlyName? }
|
|
156
|
+
method: POST
|
|
157
|
+
path: /acl/grants
|
|
158
|
+
dataSource: body
|
|
159
|
+
topic: rlb-acl
|
|
160
|
+
action: acl-grant
|
|
161
|
+
mode: rpc
|
|
162
|
+
- name: acl-check-gtw # ?userId=&roles=user&roles=admin → 200 true/false
|
|
163
|
+
method: GET
|
|
164
|
+
path: /acl/check
|
|
165
|
+
dataSource: query
|
|
166
|
+
topic: rlb-acl
|
|
167
|
+
action: acl-can-user-do-gtw
|
|
168
|
+
mode: rpc
|
|
169
|
+
- name: acl-list-resources-by-user # auth-gated; userId from X-GTW-AUTH-USERID
|
|
170
|
+
method: GET
|
|
171
|
+
path: /acl/resources
|
|
172
|
+
dataSource: query
|
|
173
|
+
topic: rlb-acl
|
|
174
|
+
action: acl-list-resources-by-user
|
|
175
|
+
mode: rpc
|
|
176
|
+
auth: gateway-jwks
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Verify
|
|
180
|
+
|
|
181
|
+
- topic `rlb-acl` + its queue declared on the consuming service; gateway paths use the
|
|
182
|
+
literal `action` strings above.
|
|
183
|
+
- role-gated routes (`roles: [...]`) → `RLB_GTW_ACL_ROLE_SERVICE` bound to an
|
|
184
|
+
`IAclRoleService` (`AclService`). Auth-provider needs `uidClaim` (+ `headerPrefix`).
|
|
185
|
+
- a check returning `false` is a **200**, not an error.
|
|
@@ -12,13 +12,17 @@ First, read the shared reference (schema + gotchas):
|
|
|
12
12
|
- `.claude/skills/rlb-amqp/references/config-schema.md`
|
|
13
13
|
- `.claude/skills/rlb-amqp/references/gotchas.md`
|
|
14
14
|
|
|
15
|
+
Authoritative source of truth: `docs/broker.md` (decorators + modes) and the runnable
|
|
16
|
+
`sample/config-sample/calculator.ms` (its `src/app.service.ts` + `config/config.yaml`).
|
|
17
|
+
|
|
15
18
|
Then locate the project's `config.yaml` (commonly `config/config.yaml`) and the service file.
|
|
16
19
|
|
|
17
20
|
## Inputs to determine (ask only if not inferable)
|
|
18
21
|
|
|
19
22
|
- **topic** (logical name), **action** (string), **mode** intended: `rpc` (default) or `event`.
|
|
20
23
|
- Payload fields the method needs, and any forwarded headers (e.g. `X-GTW-AUTH-USERID`).
|
|
21
|
-
- Whether to also expose it over HTTP
|
|
24
|
+
- Whether to also expose it over HTTP — inline via `@BrokerHTTP` (auto-publish to a gateway,
|
|
25
|
+
see Step 1b) or via a `gateway.paths[]` entry (`rlb-amqp-add-route` skill), or over WS.
|
|
22
26
|
|
|
23
27
|
## Step 1 — the handler method
|
|
24
28
|
|
|
@@ -31,7 +35,7 @@ import { BrokerAction, BrokerParam } from '@open-rlb/nestjs-amqp';
|
|
|
31
35
|
|
|
32
36
|
@Injectable()
|
|
33
37
|
export class <Domain>ActionService {
|
|
34
|
-
@BrokerAction('<topic>', '<action>', 'rpc')
|
|
38
|
+
@BrokerAction('<topic>', '<action>', 'rpc') // type? = 'rpc' (default) | 'event'
|
|
35
39
|
async <method>(
|
|
36
40
|
@BrokerParam('body', '<field>') field: string,
|
|
37
41
|
@BrokerParam('header', 'X-GTW-AUTH-USERID') userId: string,
|
|
@@ -46,6 +50,48 @@ Ensure the service is a provider in a module that NestJS loads (the
|
|
|
46
50
|
`MetadataScannerService` auto-discovers it at boot). Throwing an error whose `name` maps to
|
|
47
51
|
an HTTP status (e.g. `NotFoundError`) yields the right gateway status.
|
|
48
52
|
|
|
53
|
+
### `@BrokerAction(topic, action, type?)`
|
|
54
|
+
|
|
55
|
+
- `topic` must match a `topics:` entry (an `rpc` topic). `action` is the dispatch key.
|
|
56
|
+
- **`(topic, action)` must be unique** across the whole app — all actions of a topic share
|
|
57
|
+
ONE consumer/queue, dispatched by `action` (gotcha 3).
|
|
58
|
+
- A single method may carry **multiple** `@BrokerAction`s. When it does, any `@BrokerHTTP` /
|
|
59
|
+
`@BrokerAuth` on that method **must name its `action`** to pair deterministically (decorator
|
|
60
|
+
order is never used).
|
|
61
|
+
|
|
62
|
+
### `@BrokerParam(source, name?, pipe?)` — one source per argument
|
|
63
|
+
|
|
64
|
+
No object destructuring; declare a separate param per field. A param with no `@BrokerParam`
|
|
65
|
+
defaults to `source: 'body'` keyed by its own name. Optional `pipe` is a `PipeTransform`.
|
|
66
|
+
|
|
67
|
+
| `source` | Resolves to |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `body` | `payload[name ?? paramName]` — a single body field. |
|
|
70
|
+
| `body-full` | the entire `payload` object. |
|
|
71
|
+
| `header` | `headers[name ?? paramName]` — one AMQP/forwarded header (UPPERCASE+prefixed, gotcha 4). |
|
|
72
|
+
| `tag` | the consumer tag of the delivery. |
|
|
73
|
+
| `action` | the dispatched action string. |
|
|
74
|
+
| `topic` | the topic name. |
|
|
75
|
+
|
|
76
|
+
## Step 1b (optional) — `@BrokerHTTP` for route auto-publish
|
|
77
|
+
|
|
78
|
+
To expose the same handler over HTTP **without** hand-editing the gateway, stack
|
|
79
|
+
`@BrokerHTTP("METHOD", "/path", dataSource, options?)` on top of the `@BrokerAction`. On boot
|
|
80
|
+
the microservice publishes its `@BrokerHTTP` routes as a manifest to the gateway (route
|
|
81
|
+
auto-discovery), which persists + registers them. This requires `broker.routeDiscovery`
|
|
82
|
+
(see `rlb-amqp` reference / `docs/gateway-admin.md`); the gateway must also declare this
|
|
83
|
+
service's topic so it can forward calls.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
@BrokerAction('calculator', 'sum')
|
|
87
|
+
@BrokerHTTP('POST', '/calculator/sum', 'body') // dataSource: body | query | params | ...
|
|
88
|
+
async sum(@BrokerParam('body', 'values') values: number[]) {
|
|
89
|
+
return values.reduce((a, v) => a + v, 0);
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
(Pattern taken verbatim from `sample/config-sample/calculator.ms/src/app.service.ts`.)
|
|
94
|
+
|
|
49
95
|
## Step 2 — YAML sync (the critical part)
|
|
50
96
|
|
|
51
97
|
Reconcile `config.yaml` so the topic resolves. Add ONLY what's missing (idempotent):
|
|
@@ -85,6 +131,7 @@ single consumer dispatches by `action`.
|
|
|
85
131
|
- topic-type exchange ⇒ queue has `routingKey` (gotcha 7)
|
|
86
132
|
- `(topic, action)` unique (gotcha 3)
|
|
87
133
|
- header params read the prefixed/uppercased name (gotcha 4)
|
|
134
|
+
- if multiple `@BrokerAction` on one method ⇒ each `@BrokerHTTP`/`@BrokerAuth` names its `action`
|
|
88
135
|
|
|
89
136
|
## Step 4 — build
|
|
90
137
|
|
|
@@ -7,22 +7,46 @@ description: Expose a broker action over HTTP through the @open-rlb/nestjs-amqp
|
|
|
7
7
|
|
|
8
8
|
Read first:
|
|
9
9
|
- `.claude/skills/rlb-amqp/references/config-schema.md` (the `gateway.paths[]` section)
|
|
10
|
-
- `.claude/skills/rlb-amqp/references/gotchas.md` (HTTP + auth items
|
|
10
|
+
- `.claude/skills/rlb-amqp/references/gotchas.md` (HTTP + auth items)
|
|
11
|
+
- `docs/gateway.md` is the authority for the `PathDefinition` fields and status mapping.
|
|
11
12
|
|
|
12
13
|
The target `topic`+`action` should already have a handler (otherwise also run
|
|
13
14
|
`rlb-amqp-add-action`). The route only needs the topic to exist in `topics[]`.
|
|
15
|
+
Canonical example: `sample/config-sample/gateway-in-memory/config/config.yaml`.
|
|
14
16
|
|
|
15
17
|
## Decide
|
|
16
18
|
|
|
17
|
-
- **mode**: `rpc` (return the handler's
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
`roles: [...]` for ACL.
|
|
19
|
+
- **mode**: `rpc` (return the handler's reply) or `event` (publish-and-confirm, no reply).
|
|
20
|
+
- **dataSource**: how the payload is assembled — `req.params` are ALWAYS merged in, plus:
|
|
21
|
+
`body` | `query` | `params` | `body-query` (body wins) | `query-body` (query wins).
|
|
22
|
+
- **auth**: an `auth-provider` name (validates the request, maps claims to `X-GTW-AUTH-*`
|
|
23
|
+
headers). `allowAnonymous: true` skips the gate. `roles: [...]` adds a role check.
|
|
23
24
|
- Extras: `timeout` (rpc), `successStatusCode`, `binary`, `redirect`, `parseRaw`, static
|
|
24
25
|
`headers`, `forwardHeaders`.
|
|
25
26
|
|
|
27
|
+
## PathDefinition fields (all of them)
|
|
28
|
+
|
|
29
|
+
| Field | Notes |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| `name` | Unique; used in logs + metrics. |
|
|
32
|
+
| `method` | `GET` `POST` `PUT` `DELETE` `PATCH` … |
|
|
33
|
+
| `path` | Express route, may carry `:params` (e.g. `/users/:id`). |
|
|
34
|
+
| `topic` / `action` | Broker destination. Action strings are decorator-bound on the backend. |
|
|
35
|
+
| `mode` | `rpc` \| `event`. |
|
|
36
|
+
| `dataSource` | `body` \| `query` \| `params` \| `body-query` \| `query-body`. |
|
|
37
|
+
| `auth` | Auth-provider name; validates + maps claims. |
|
|
38
|
+
| `allowAnonymous` | `true` → gate skipped (token still mapped if present & valid). |
|
|
39
|
+
| `roles` | Role NAMES; caller passes with AT LEAST ONE. Requires `auth`. |
|
|
40
|
+
| `timeout` | RPC timeout (ms), `rpc` only. |
|
|
41
|
+
| `binary` | Treat a raw (non-JSON) RPC reply as base64 → binary body. |
|
|
42
|
+
| `parseRaw` | Adds the raw request body as `$raw` (needs `rawBody: true`). |
|
|
43
|
+
| `successStatusCode` | Override success status (default 200 rpc / 202 event / 204 empty). |
|
|
44
|
+
| `redirect` | On an `rpc` route, redirect with this status using the reply as the location. |
|
|
45
|
+
| `headers` | Static response headers `{ k: v }`. |
|
|
46
|
+
| `forwardHeaders` | `{ dest: srcHeader }` — copy request headers downstream (prefixed by `gateway.headerPrefix`). |
|
|
47
|
+
|
|
48
|
+
Uploaded multipart files (any field) are attached as `$files` (buffers → binary strings).
|
|
49
|
+
|
|
26
50
|
## YAML fragment
|
|
27
51
|
|
|
28
52
|
```yaml
|
|
@@ -36,28 +60,67 @@ gateway:
|
|
|
36
60
|
action: <action>
|
|
37
61
|
mode: rpc # or event
|
|
38
62
|
auth: gateway-jwks # optional
|
|
39
|
-
roles: [resource.write] # optional → needs
|
|
63
|
+
roles: [resource.write] # optional → needs RLB_GTW_ACL_ROLE_SERVICE
|
|
40
64
|
timeout: 7000 # rpc only
|
|
41
65
|
successStatusCode: 201
|
|
42
66
|
```
|
|
43
67
|
|
|
68
|
+
## The 3-case auth gate
|
|
69
|
+
|
|
70
|
+
For every request the gateway runs `processAuthData` (best-effort), then:
|
|
71
|
+
|
|
72
|
+
1. **`allowAnonymous: true`** → gate SKIPPED. A valid token still gets its claims mapped &
|
|
73
|
+
forwarded; a missing/invalid token is NOT blocked.
|
|
74
|
+
2. **`auth` set, no `roles`** → authentication only. Provider must validate (else `401`);
|
|
75
|
+
on success the `X-GTW-AUTH-*` headers are forwarded downstream.
|
|
76
|
+
3. **`auth` + `roles`** → authn + role authz. After a valid token the gateway reads the user
|
|
77
|
+
id from the provider's `uidClaim` and calls `IAclRoleService.canUserDoGtw(roles, userId)`
|
|
78
|
+
in-process. Passes with at least one role, else `403`.
|
|
79
|
+
|
|
80
|
+
> `roles` WITHOUT `auth` is a misconfiguration: no identity → fails closed (every request
|
|
81
|
+
> `403`, logged loudly at boot). The resource-scoped check (`acl-can-user-do`) is NOT run by
|
|
82
|
+
> the gateway — it lives on the target microservice.
|
|
83
|
+
|
|
84
|
+
## Status mapping
|
|
85
|
+
|
|
86
|
+
**`rpc`** reply → status:
|
|
87
|
+
|
|
88
|
+
| Reply | Status |
|
|
89
|
+
| --- | --- |
|
|
90
|
+
| Defined value (incl. falsy `false` / `0` / `''`) | `200` + body |
|
|
91
|
+
| `null` / `undefined` | `204 No Content` |
|
|
92
|
+
|
|
93
|
+
> ONLY `null`/`undefined` collapses to `204`. A defined falsy result is real content, so a
|
|
94
|
+
> boolean check route answers `200` with body `false` — not an empty `204`.
|
|
95
|
+
|
|
96
|
+
**`rpc`** error → status (by error `name`):
|
|
97
|
+
|
|
98
|
+
| Error name | Status |
|
|
99
|
+
| --- | --- |
|
|
100
|
+
| `BadRequestError`, `InvalidParamsErrror` | `400` |
|
|
101
|
+
| `UnauthorizedError` | `401` |
|
|
102
|
+
| `ForbiddenError` | `403` |
|
|
103
|
+
| `NotFoundError` | `404` |
|
|
104
|
+
| `ConflictError` | `409` |
|
|
105
|
+
| (any other) | `500` |
|
|
106
|
+
|
|
107
|
+
**`event`** route: successful publish → `successStatusCode || 202`; publish failure → `503`.
|
|
108
|
+
|
|
44
109
|
## Required wiring to flag
|
|
45
110
|
|
|
46
|
-
- If `parseRaw: true` →
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
`
|
|
50
|
-
|
|
51
|
-
passes with AT LEAST ONE. The auth-provider only needs `uidClaim` (+ `headerPrefix`) to
|
|
52
|
-
extract the userId (gotcha 15). For a resource-scoped decision, the target ms calls
|
|
53
|
-
`canUserDo(roles, userId, resourceId)` (RPC `acl-can-user-do`) itself.
|
|
111
|
+
- If `parseRaw: true` → bootstrap with `NestFactory.create(AppModule, { rawBody: true })`.
|
|
112
|
+
- If `roles` is used → an `IAclRoleService` must be registered via `RLB_GTW_ACL_ROLE_SERVICE`
|
|
113
|
+
in `ProxyModule.forRootAsync({ providers: [{ provide: RLB_GTW_ACL_ROLE_SERVICE, useExisting: AclService }] })`.
|
|
114
|
+
If a path declares `roles` and the service is NOT registered → request DENIED (`403`) +
|
|
115
|
+
error logged. The auth-provider needs a `uidClaim` (+ `headerPrefix`) to resolve the userId.
|
|
54
116
|
- Forwarded auth claims reach the handler as prefixed/uppercased headers
|
|
55
|
-
(e.g. `X-GTW-AUTH-USERID`) — read them with `@BrokerParam('header', ...)`.
|
|
117
|
+
(e.g. `X-GTW-AUTH-USERID`) — read them with `@BrokerParam('header', ...)`. Request headers
|
|
118
|
+
can never override mapped claim headers (anti-spoofing).
|
|
56
119
|
|
|
57
120
|
## Verify
|
|
58
121
|
|
|
59
|
-
- topic exists in `topics[]` and resolves
|
|
60
|
-
- route-param vs body/query key collisions are intentional (
|
|
122
|
+
- topic exists in `topics[]` and resolves.
|
|
123
|
+
- route-param vs body/query key collisions are intentional (params always merge in).
|
|
61
124
|
- `npm run build`, then optionally curl the route once the broker is up.
|
|
62
125
|
|
|
63
126
|
Output the YAML fragment (with parent path), plus any bootstrap/ACL action the user still
|