@open-rlb/nestjs-amqp 2.0.4 → 2.0.6
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 +0 -1
- package/modules/acl/const.js +0 -1
- 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-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 +2 -2
- package/modules/acl/services/acl-management.service.js +17 -20
- package/modules/acl/services/acl-management.service.js.map +1 -1
- package/modules/acl/services/acl.service.d.ts +1 -2
- package/modules/acl/services/acl.service.js +5 -21
- 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/decorator-paths.d.ts +1 -0
- package/modules/broker/config/decorator-paths.js +34 -4
- package/modules/broker/config/decorator-paths.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/decorators/broker-action.decorator.d.ts +2 -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/broker.service.js +1 -1
- package/modules/broker/services/broker.service.js.map +1 -1
- package/modules/broker/services/metadata-scanner.service.js +11 -2
- package/modules/broker/services/metadata-scanner.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 +27 -3
- 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 +33 -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 +129 -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 +87 -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 +244 -0
- package/schematics/nest-add/files/skills/rlb-amqp-scaffold/SKILL.md +177 -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,154 @@
|
|
|
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. **Two independent pairings on a method — `action` (http↔action) and `httpName` (auth↔route).**
|
|
22
|
+
Decorator order is never used; pair by name.
|
|
23
|
+
- **`@BrokerHTTP` ↔ `@BrokerAction`** via the `action` option: a method with multiple
|
|
24
|
+
`@BrokerAction`s requires each `@BrokerHTTP` to name its `action`; with one action it defaults.
|
|
25
|
+
- **`@BrokerAuth` ↔ `@BrokerHTTP` route** via `httpName` (= the route's `name`): auth is
|
|
26
|
+
**per ROUTE**, kept DECOUPLED from `@BrokerHTTP` (which carries NO auth). One `@BrokerHTTP`
|
|
27
|
+
auto-pairs its `@BrokerAuth` (no `name`/`httpName` needed); multiple `@BrokerHTTP` require each
|
|
28
|
+
to set a `name` and each `@BrokerAuth` to set the matching `httpName`. An `@BrokerAuth` whose
|
|
29
|
+
`httpName` matches no route is NOT applied and logs a WARNING at microservice startup.
|
|
30
|
+
- A route with no paired `@BrokerAuth` is **PUBLIC**. Two HTTP paths for the SAME action can now
|
|
31
|
+
carry DIFFERENT auth — pair each to its route by `name`.
|
|
18
32
|
|
|
19
33
|
## Topic ↔ queue ↔ exchange wiring
|
|
20
|
-
|
|
34
|
+
7. **The topic `name` must match everywhere**: `@BrokerAction`, `topics[].name`,
|
|
21
35
|
`requestData`/`publishMessage`, `gateway.paths[].topic` / `events[]`. Typo →
|
|
22
36
|
`Topic X not found in configuration`.
|
|
23
|
-
|
|
37
|
+
8. **`mode: rpc`/`handle` need `topics[].queue` in `broker.queues[]`**, and that queue's
|
|
24
38
|
`exchange` in `broker.exchanges[]`. In `handle` a missing queue throws NPE at boot
|
|
25
39
|
(`queue.exchange`).
|
|
26
|
-
|
|
27
|
-
`Queue ... has no routing key`.
|
|
28
|
-
|
|
29
|
-
|
|
40
|
+
9. **Exchange `type: topic` → queue MUST have `routingKey`**, else boot throws
|
|
41
|
+
`Queue ... has no routing key`. (The samples use a `direct` exchange with matching keys.)
|
|
42
|
+
10. **The gateway can only forward to topics it declares.** Route auto-discovery teaches the
|
|
43
|
+
gateway the HTTP route, NOT how to reach the microservice's broker topic. That topic (+ its
|
|
44
|
+
queue/exchange) must ALSO exist in the **gateway's own** `broker` config, or the forwarded
|
|
45
|
+
request fails with `Topic ... not found in configuration`.
|
|
46
|
+
|
|
47
|
+
## connection_name (broadcast / WebSocket / route-discovery)
|
|
48
|
+
11. **`broadcast` + WebSocket require `connection_name`** (`clientProperties.connection_name`,
|
|
49
|
+
or `broker.routeDiscovery.serviceName` which fills it when unset), else throw at boot.
|
|
50
|
+
12. **Every instance needs a DISTINCT `connection_name`.** Sharing it makes RabbitMQ treat the
|
|
51
|
+
per-instance queues as one consumer group and **round-robin** broadcast/WS messages — reloads
|
|
52
|
+
land on "every other" instance, WS clients miss events delivered to the other one.
|
|
30
53
|
|
|
31
54
|
## RPC / timeout / errors
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
55
|
+
13. **Wrong `replyQueues` key → silent timeout.** `requestData` resolves `replyTo` from
|
|
56
|
+
`broker.replyQueues[exchange]`; absent → RabbitMQ direct-reply-to. Wrong exchange key → no
|
|
57
|
+
reply routed back → the call just times out.
|
|
58
|
+
14. **Handler exceptions don't crash the consumer.** Returned as `{success:false,error}`;
|
|
59
|
+
`requestData` re-throws on the caller side. Gateway HTTP status derives from `error.name` —
|
|
60
|
+
give errors a meaningful `name` (`BadRequestError`, `NotFoundError`, `ConflictError`,
|
|
61
|
+
`ForbiddenError`, `UnauthorizedError`); anything unrecognized → 500.
|
|
62
|
+
15. **Default RPC timeout 10s** (`broker.defaultRpcTimeout`). Override per route (`paths[].timeout`)
|
|
63
|
+
or per `requestData` call for slow RPCs.
|
|
39
64
|
|
|
40
65
|
## Gateway HTTP
|
|
41
|
-
|
|
42
|
-
is `undefined
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
66
|
+
16. **Boolean RPCs return `200` with `true`/`false`, not `204`.** A *defined* result — including
|
|
67
|
+
falsy `false`/`0`/`""` — is real content, sent as `200` + JSON. Only `null`/`undefined`
|
|
68
|
+
collapses to `204`. So `GET /acl/check?...` answers `200 false` for "no" — don't treat any
|
|
69
|
+
2xx as "allowed", read the body. (The old "always 204" bug is fixed.)
|
|
70
|
+
17. **`parseRaw: true` needs `NestFactory.create(AppModule, { rawBody: true })`** or `$raw` is
|
|
71
|
+
`undefined`.
|
|
72
|
+
18. **Route params win over body/query** (merged in last). Watch key collisions (`:id` vs `body.id`).
|
|
73
|
+
19. **Uploads live in `$files`** (multer `.any()`); buffers are converted to **binary strings** —
|
|
74
|
+
re-encode carefully on the consumer (`Buffer.from(str, 'binary')`).
|
|
75
|
+
20. **`/health` is a tiny liveness probe.** Action `gw-health` → `{ status: 'ok' }` (a real 200),
|
|
76
|
+
NOT a metrics dump. Use `/admin/metrics*` (`gw-metrics-*`) for metrics.
|
|
47
77
|
|
|
48
78
|
## Auth / ACL
|
|
49
|
-
|
|
50
|
-
`
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
`acl-can-user-do-gtw` → `canUserDoGtw(roles, userId)` (gateway
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
`
|
|
79
|
+
21. **`roles` require `auth` on the same path/event.** No `auth` → no identity → fails closed
|
|
80
|
+
(every request `403`, logged at boot). Always pair `roles: [...]` with `auth: <provider>`.
|
|
81
|
+
22. **`roles` require an `IAclRoleService`** registered via `RLB_GTW_ACL_ROLE_SERVICE` in
|
|
82
|
+
`ProxyModule.forRootAsync({ providers: [...] })`. Missing → deny (403). The gateway check is
|
|
83
|
+
**role-based, OR, resource-agnostic** (`canUserDoGtw(roles, userId)`): `roles` lists ROLE NAMES,
|
|
84
|
+
the user passes holding AT LEAST ONE. The provider only needs `uidClaim` (+ `headerPrefix`).
|
|
85
|
+
23. **Two ACL check actions on `rlb-acl`** (both cached, both HTTP GET → `200` true/false):
|
|
86
|
+
`acl-can-user-do-gtw` → `canUserDoGtw(roles, userId)` (gateway filter, OR, resource-agnostic,
|
|
87
|
+
`GET /acl/check`) and `acl-can-user-do` → `canUserDo(roles, userId, resource)` (**ms-side**;
|
|
88
|
+
a global grant OR a grant on that resource passes, `GET /acl/check-resource`).
|
|
89
|
+
24. **Actions, roles & auth-providers are NAME-KEYED. PUT upserts; there is NO POST.** The `name`
|
|
90
|
+
IS the key (no id). `PUT` creates-or-updates, `GET` lists, `GET .../get?name=` reads one,
|
|
91
|
+
`DELETE` removes by `name`. The old id-based ACL CRUD and `POST`-create endpoints are GONE.
|
|
92
|
+
(Gateway-admin **paths** are the exception — they keep id-keyed CRUD and a POST create.)
|
|
93
|
+
25. **`acl-grant` / `acl-revoke` both REQUIRE `userId` + `roles`** (optional `resourceId` +
|
|
94
|
+
`companyId`). `grant` MERGES roles into the single `(userId, resourceId)` record (idempotent).
|
|
95
|
+
`revoke` REMOVES exactly those roles and **deletes the record once it has no roles left**.
|
|
96
|
+
`revoke` without `roles` throws `400 roles are required` — to wipe a grant, revoke all its roles.
|
|
97
|
+
26. **`companyId` is grouping metadata only.** It replaced `resourceBusinessId` and plays NO part
|
|
98
|
+
in authorization — it only groups resources in `acl-list-resources-by-user`. Targeting is by
|
|
99
|
+
`(userId, resourceId)` only. Both grant/revoke validate every role exists (unknown → `400`).
|
|
100
|
+
27. **Removed actions:** `acl-list-by-user` and `acl-verify-access` no longer exist. Use
|
|
101
|
+
`acl-can-user-do` for resource-scoped checks and `acl-list-resources-by-user` to list resources.
|
|
102
|
+
28. **Auth & gateway config go to `ProxyModule`** (`authOptions` / `gatewayOptions`), not
|
|
103
|
+
`BrokerModule`. `BrokerModule` owns only `options` / `topics` / `appOptions`.
|
|
62
104
|
|
|
63
|
-
##
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
`
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
custom handshake headers.
|
|
105
|
+
## Auth providers (hardening)
|
|
106
|
+
29. **JWKS verifies TLS by default.** `httpsAllowUnauthorized: true` only for self-signed dev issuers.
|
|
107
|
+
30. **`algorithms` is REQUIRED for `jwt`/`jwks`.** Omitting denies verification (algorithm-confusion
|
|
108
|
+
guard). For `jwks` only asymmetric algs are allowed (`RS*`/`ES*`/`PS*`); `HS*`/`none` rejected.
|
|
109
|
+
31. **Define `jwtMap` or NO claims are forwarded.** Without it the token is still accepted
|
|
110
|
+
(`success:true`) but no identity headers go downstream — fail-safe, not a leak. Declare it to
|
|
111
|
+
emit `X-GTW-AUTH-USERID` and friends.
|
|
112
|
+
32. **`str-compare`/`basic` PASS THROUGH when their secret is unset — by design.** A `str-compare`
|
|
113
|
+
with no `secret`, or a `basic` with no `clientSecret`, authenticates EVERY request (effectively
|
|
114
|
+
open/disabled). Set the secret to enforce it.
|
|
115
|
+
33. **Credential `mechanism` must be `PLAIN` | `EXTERNAL` | `AMQPLAIN`** (case-insensitive). Unknown
|
|
116
|
+
value leaves SASL `response` unset → AMQP auth fails.
|
|
76
117
|
|
|
77
|
-
##
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
`
|
|
118
|
+
## WebSocket
|
|
119
|
+
34. **Auth is per-event, not global.** `events[].auth` names the provider that verifies the
|
|
120
|
+
connection token AND maps its claims for THAT event (memoized per provider at subscribe). `scopeClaim`
|
|
121
|
+
references the MAPPED claim (prefixed, e.g. `X-GTW-AUTH-USERID`), `payloadKey` is the payload
|
|
122
|
+
field. **`scopeClaim` without `payloadKey` denies everything** (safe default). `requireAuth:false`
|
|
123
|
+
makes `auth` optional (anon allowed; claims mapped if a token is present). `gateway.ws` carries
|
|
124
|
+
only connection-level limits/heartbeat — no auth fields.
|
|
125
|
+
35. **Don't bind a WS event to a fixed durable queue.** The lib creates a per-instance exclusive
|
|
126
|
+
ephemeral queue for fan-out; a shared/durable queue makes instances compete and clients on one
|
|
127
|
+
instance miss messages delivered to another.
|
|
128
|
+
36. **Token rides in the subprotocol**: `new WebSocket(url, [token])` (browsers can't set handshake
|
|
129
|
+
headers). The session is bounded by the JWT `exp` — closed with `1008` on expiry, nothing
|
|
130
|
+
delivered afterward. Long-lived clients need token refresh + reconnect.
|
|
131
|
+
37. **Set `gateway.ws.allowedOrigins`** to reject cross-site handshakes; omitted → all Origins
|
|
132
|
+
accepted (logged at boot). `maxMessageBytes` (default 16384) drops oversized client frames.
|
|
82
133
|
|
|
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`).
|
|
134
|
+
## Publish / events
|
|
135
|
+
38. **`publishMessage` is `async` — `await` it** for the publisher-confirm guarantee and to catch
|
|
136
|
+
failures. Un-awaited = fire-and-forget without guarantee. (For an `event`-mode route the gateway
|
|
137
|
+
awaits the confirm before returning the 2xx — the success status is not optimistic.)
|
|
97
138
|
|
|
98
|
-
##
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
139
|
+
## Reload & route auto-discovery
|
|
140
|
+
39. **The only reload action is `gw-reload`** (`GW_RELOAD_ACTION`). The control-topic subscriber
|
|
141
|
+
rebuilds routes ONLY for `gw-reload`; every other message on the control topic is ignored.
|
|
142
|
+
Seed then reload: `POST /admin/paths` → `POST /admin/reload` (publishes `gw-reload` on
|
|
143
|
+
`rlb-gateway-control`). No restart. Concurrent reloads are coalesced into one extra pass.
|
|
144
|
+
40. **Route-discovery config is SPLIT; exchange/queue MUST match on both sides.**
|
|
145
|
+
- **Publisher (microservice):** `broker.routeDiscovery { serviceName, publishOnBoot, exchange?, queue? }`.
|
|
146
|
+
`serviceName` is required to publish and also fills `connection_name` when unset.
|
|
147
|
+
- **Consumer (gateway):** `GatewayAdminModule` `routeDiscovery { exchange?, queue? }` (NEST code,
|
|
148
|
+
no `serviceName` — the gateway only receives).
|
|
149
|
+
Both default to `exchange: rlb-route-discovery` / `queue: rlb-route-sync`. Override only to
|
|
150
|
+
namespace per env — but set the SAME values on BOTH sides or manifests never reach the gateway.
|
|
151
|
+
41. **Topic NAMES `rlb-acl` / `rlb-gateway-admin` / `rlb-gateway-control` and all action strings
|
|
152
|
+
are decorator-bound and NOT configurable.** Only exchange/queue/routingKey and the
|
|
153
|
+
route-discovery exchange/queue are. The route-sync handler never throws (logs + acks, no poison
|
|
154
|
+
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,85 @@ 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` on
|
|
59
|
+
that method **must name its `action`** to pair deterministically (decorator order is never
|
|
60
|
+
used). Auth pairs separately — see `@BrokerAuth` below.
|
|
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
|
+
`dataSource` is `'query' | 'body' | 'params'`. The options object carries
|
|
86
|
+
`{ name?, action?, successStatusCode?, timeout?, parseRaw?, binary?, redirect?, headers?,
|
|
87
|
+
forwardHeaders? }` — it does **not** carry auth.
|
|
88
|
+
|
|
89
|
+
Two independent pairings sit on the method, each only needed in the multi case:
|
|
90
|
+
|
|
91
|
+
- **http ↔ action** (`@BrokerHTTP`'s `action`): bind a route to one of several `@BrokerAction`s.
|
|
92
|
+
Required only when the method declares **more than one** `@BrokerAction`; with a single action
|
|
93
|
+
it defaults to that action.
|
|
94
|
+
- **auth ↔ route** (`@BrokerAuth`'s `httpName` ⇄ `@BrokerHTTP`'s `name`): bind an auth rule to one
|
|
95
|
+
of several routes. Required only when the method declares **more than one** `@BrokerHTTP`; with a
|
|
96
|
+
single route the auth auto-pairs (no `name`/`httpName` needed). A route with no paired
|
|
97
|
+
`@BrokerAuth` is **public**.
|
|
98
|
+
|
|
99
|
+
Auth lives in a separate, decoupled decorator —
|
|
100
|
+
`@BrokerAuth(authName, allowAnonymous?, roles?, httpName?)` — never inside `@BrokerHTTP`'s options.
|
|
101
|
+
This lets two HTTP paths for the SAME action carry DIFFERENT auth.
|
|
102
|
+
|
|
103
|
+
Simple case — one route, auth auto-pairs (no names needed):
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
@BrokerAction('calculator', 'sum')
|
|
107
|
+
@BrokerHTTP('POST', '/calculator/sum', 'body') // dataSource: 'query' | 'body' | 'params'
|
|
108
|
+
@BrokerAuth('cust-jwks', true) // auto-pairs to the single route; public if omitted
|
|
109
|
+
async sum(@BrokerParam('body', 'values') values: number[]) {
|
|
110
|
+
return values.reduce((a, v) => a + v, 0);
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Multi case — two routes for ONE action, each name-paired to its own auth:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
@BrokerAction('booking', 'get-booking')
|
|
118
|
+
@BrokerHTTP('GET', '/bookings/:id', 'params', { name: 'get-booking' })
|
|
119
|
+
@BrokerAuth('cust-jwks', true, undefined, 'get-booking') // httpName ⇄ route name
|
|
120
|
+
@BrokerHTTP('GET', '/admin/bookings/:id', 'params', { name: 'admin-get-booking' })
|
|
121
|
+
@BrokerAuth('admin-jwks', undefined, ['admin'], 'admin-get-booking')
|
|
122
|
+
async getBooking(@BrokerParam('params', 'id') id: string) {
|
|
123
|
+
return this.bookings.find(id);
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
An `@BrokerAuth` whose `httpName` matches no route is NOT applied and logs a WARNING at
|
|
128
|
+
microservice startup.
|
|
129
|
+
|
|
130
|
+
(Single-route pattern taken verbatim from `sample/config-sample/calculator.ms/src/app.service.ts`.)
|
|
131
|
+
|
|
49
132
|
## Step 2 — YAML sync (the critical part)
|
|
50
133
|
|
|
51
134
|
Reconcile `config.yaml` so the topic resolves. Add ONLY what's missing (idempotent):
|
|
@@ -85,6 +168,8 @@ single consumer dispatches by `action`.
|
|
|
85
168
|
- topic-type exchange ⇒ queue has `routingKey` (gotcha 7)
|
|
86
169
|
- `(topic, action)` unique (gotcha 3)
|
|
87
170
|
- header params read the prefixed/uppercased name (gotcha 4)
|
|
171
|
+
- if multiple `@BrokerAction` on one method ⇒ each `@BrokerHTTP` names its `action`
|
|
172
|
+
- if multiple `@BrokerHTTP` on one method ⇒ each `@BrokerAuth` sets `httpName` = the route's `name`
|
|
88
173
|
|
|
89
174
|
## Step 4 — build
|
|
90
175
|
|