@o3co/auth.policy-verifier.builtins 0.3.1 → 0.4.0
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.ja.md +70 -14
- package/README.md +75 -15
- package/dist/collectors/PayloadScopeCollector.d.mts +5 -0
- package/dist/collectors/PayloadScopeCollector.d.mts.map +1 -1
- package/dist/collectors/PayloadScopeCollector.mjs +8 -1
- package/dist/collectors/PayloadSubjectIdCollector.d.mts +9 -0
- package/dist/collectors/PayloadSubjectIdCollector.d.mts.map +1 -1
- package/dist/collectors/PayloadSubjectIdCollector.mjs +16 -4
- package/dist/collectors/RequestContextAttributeCollector.d.mts +47 -0
- package/dist/collectors/RequestContextAttributeCollector.d.mts.map +1 -0
- package/dist/collectors/RequestContextAttributeCollector.mjs +100 -0
- package/dist/collectors/StaticPermissionCollector.d.mts +5 -0
- package/dist/collectors/StaticPermissionCollector.d.mts.map +1 -1
- package/dist/collectors/StaticPermissionCollector.mjs +7 -0
- package/dist/collectors/StaticRoleCollector.d.mts +5 -0
- package/dist/collectors/StaticRoleCollector.d.mts.map +1 -1
- package/dist/collectors/StaticRoleCollector.mjs +7 -0
- package/dist/index.d.mts +3 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +4 -1
- package/dist/module.d.mts +5 -0
- package/dist/module.d.mts.map +1 -1
- package/dist/module.mjs +10 -1
- package/dist/resource/DotNotationResourceParser.d.mts +48 -0
- package/dist/resource/DotNotationResourceParser.d.mts.map +1 -1
- package/dist/resource/DotNotationResourceParser.mjs +94 -10
- package/dist/rules/AttrLiteralCompare.d.mts +2 -2
- package/dist/rules/AttrLiteralCompare.d.mts.map +1 -1
- package/dist/rules/AttrLiteralCompare.mjs +2 -0
- package/dist/rules/AttrLiteralEqual.d.mts +2 -2
- package/dist/rules/AttrLiteralEqual.d.mts.map +1 -1
- package/dist/rules/AttrLiteralEqual.mjs +2 -0
- package/dist/rules/AttrLiteralIn.d.mts +2 -2
- package/dist/rules/AttrLiteralIn.d.mts.map +1 -1
- package/dist/rules/AttrLiteralIn.mjs +2 -0
- package/dist/rules/AttrLiteralNotEqual.d.mts +2 -2
- package/dist/rules/AttrLiteralNotEqual.d.mts.map +1 -1
- package/dist/rules/AttrLiteralNotEqual.mjs +2 -0
- package/dist/rules/AttrLiteralNotIn.d.mts +2 -2
- package/dist/rules/AttrLiteralNotIn.d.mts.map +1 -1
- package/dist/rules/AttrLiteralNotIn.mjs +2 -0
- package/dist/rules/AttrMatchRule.d.mts.map +1 -1
- package/dist/rules/AttrMatchRule.mjs +2 -0
- package/dist/rules/AttrPairCompare.d.mts +2 -2
- package/dist/rules/AttrPairCompare.d.mts.map +1 -1
- package/dist/rules/AttrPairCompare.mjs +2 -0
- package/dist/rules/AttrPairEqual.d.mts +2 -2
- package/dist/rules/AttrPairEqual.d.mts.map +1 -1
- package/dist/rules/AttrPairEqual.mjs +2 -0
- package/dist/rules/AttrPairNotEqual.d.mts +2 -2
- package/dist/rules/AttrPairNotEqual.d.mts.map +1 -1
- package/dist/rules/AttrPairNotEqual.mjs +2 -0
- package/dist/rules/HasPermission.d.mts +30 -2
- package/dist/rules/HasPermission.d.mts.map +1 -1
- package/dist/rules/HasPermission.mjs +30 -2
- package/dist/rules/HasScope.d.mts +45 -4
- package/dist/rules/HasScope.d.mts.map +1 -1
- package/dist/rules/HasScope.mjs +46 -9
- package/dist/rules/_sharedValidation.d.mts +12 -9
- package/dist/rules/_sharedValidation.d.mts.map +1 -1
- package/dist/rules/_sharedValidation.mjs +46 -9
- package/dist/rules/collectors/ResourceActionPermissionRuleCollector.d.mts +6 -0
- package/dist/rules/collectors/ResourceActionPermissionRuleCollector.d.mts.map +1 -1
- package/dist/rules/collectors/ResourceActionPermissionRuleCollector.mjs +8 -0
- package/dist/rules/collectors/ResourceActionScopeRuleCollector.d.mts +52 -4
- package/dist/rules/collectors/ResourceActionScopeRuleCollector.d.mts.map +1 -1
- package/dist/rules/collectors/ResourceActionScopeRuleCollector.mjs +54 -6
- package/package.json +8 -5
- package/dist/__tests__/collectors/PayloadScopeCollector.test.d.mts +0 -2
- package/dist/__tests__/collectors/PayloadScopeCollector.test.d.mts.map +0 -1
- package/dist/__tests__/collectors/PayloadScopeCollector.test.mjs +0 -24
- package/dist/__tests__/collectors/PayloadSubjectIdCollector.test.d.mts +0 -2
- package/dist/__tests__/collectors/PayloadSubjectIdCollector.test.d.mts.map +0 -1
- package/dist/__tests__/collectors/PayloadSubjectIdCollector.test.mjs +0 -33
- package/dist/__tests__/collectors/StaticPermissionCollector.test.d.mts +0 -2
- package/dist/__tests__/collectors/StaticPermissionCollector.test.d.mts.map +0 -1
- package/dist/__tests__/collectors/StaticPermissionCollector.test.mjs +0 -22
- package/dist/__tests__/collectors/StaticRoleCollector.test.d.mts +0 -2
- package/dist/__tests__/collectors/StaticRoleCollector.test.d.mts.map +0 -1
- package/dist/__tests__/collectors/StaticRoleCollector.test.mjs +0 -24
- package/dist/__tests__/module.test.d.mts +0 -2
- package/dist/__tests__/module.test.d.mts.map +0 -1
- package/dist/__tests__/module.test.mjs +0 -71
- package/dist/__tests__/resource/DotNotationResourceParser.test.d.mts +0 -2
- package/dist/__tests__/resource/DotNotationResourceParser.test.d.mts.map +0 -1
- package/dist/__tests__/resource/DotNotationResourceParser.test.mjs +0 -34
- package/dist/__tests__/rules/AttrLiteralCompare.test.d.mts +0 -2
- package/dist/__tests__/rules/AttrLiteralCompare.test.d.mts.map +0 -1
- package/dist/__tests__/rules/AttrLiteralCompare.test.mjs +0 -253
- package/dist/__tests__/rules/AttrLiteralEqual.test.d.mts +0 -2
- package/dist/__tests__/rules/AttrLiteralEqual.test.d.mts.map +0 -1
- package/dist/__tests__/rules/AttrLiteralEqual.test.mjs +0 -138
- package/dist/__tests__/rules/AttrLiteralIn.test.d.mts +0 -2
- package/dist/__tests__/rules/AttrLiteralIn.test.d.mts.map +0 -1
- package/dist/__tests__/rules/AttrLiteralIn.test.mjs +0 -160
- package/dist/__tests__/rules/AttrLiteralNotEqual.test.d.mts +0 -2
- package/dist/__tests__/rules/AttrLiteralNotEqual.test.d.mts.map +0 -1
- package/dist/__tests__/rules/AttrLiteralNotEqual.test.mjs +0 -157
- package/dist/__tests__/rules/AttrLiteralNotIn.test.d.mts +0 -2
- package/dist/__tests__/rules/AttrLiteralNotIn.test.d.mts.map +0 -1
- package/dist/__tests__/rules/AttrLiteralNotIn.test.mjs +0 -168
- package/dist/__tests__/rules/AttrMatchRule.test.d.mts +0 -2
- package/dist/__tests__/rules/AttrMatchRule.test.d.mts.map +0 -1
- package/dist/__tests__/rules/AttrMatchRule.test.mjs +0 -79
- package/dist/__tests__/rules/AttrPairCompare.test.d.mts +0 -2
- package/dist/__tests__/rules/AttrPairCompare.test.d.mts.map +0 -1
- package/dist/__tests__/rules/AttrPairCompare.test.mjs +0 -319
- package/dist/__tests__/rules/AttrPairEqual.test.d.mts +0 -2
- package/dist/__tests__/rules/AttrPairEqual.test.d.mts.map +0 -1
- package/dist/__tests__/rules/AttrPairEqual.test.mjs +0 -155
- package/dist/__tests__/rules/AttrPairNotEqual.test.d.mts +0 -2
- package/dist/__tests__/rules/AttrPairNotEqual.test.d.mts.map +0 -1
- package/dist/__tests__/rules/AttrPairNotEqual.test.mjs +0 -155
- package/dist/__tests__/rules/HasPermission.test.d.mts +0 -2
- package/dist/__tests__/rules/HasPermission.test.d.mts.map +0 -1
- package/dist/__tests__/rules/HasPermission.test.mjs +0 -86
- package/dist/__tests__/rules/HasScope.test.d.mts +0 -2
- package/dist/__tests__/rules/HasScope.test.d.mts.map +0 -1
- package/dist/__tests__/rules/HasScope.test.mjs +0 -42
- package/dist/__tests__/rules/_sharedValidation.test.d.mts +0 -2
- package/dist/__tests__/rules/_sharedValidation.test.d.mts.map +0 -1
- package/dist/__tests__/rules/_sharedValidation.test.mjs +0 -325
- package/dist/__tests__/rules/collectors/ResourceActionPermissionRuleCollector.test.d.mts +0 -2
- package/dist/__tests__/rules/collectors/ResourceActionPermissionRuleCollector.test.d.mts.map +0 -1
- package/dist/__tests__/rules/collectors/ResourceActionPermissionRuleCollector.test.mjs +0 -27
- package/dist/__tests__/rules/collectors/ResourceActionScopeRuleCollector.test.d.mts +0 -2
- package/dist/__tests__/rules/collectors/ResourceActionScopeRuleCollector.test.d.mts.map +0 -1
- package/dist/__tests__/rules/collectors/ResourceActionScopeRuleCollector.test.mjs +0 -48
package/README.ja.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
auth.policy-verifier 向けの組み込み attribute collector、rule collector、および resource parser です。
|
|
4
4
|
|
|
5
|
-
**Runtime:**
|
|
5
|
+
**Runtime:** `BigInt` と `Map.groupBy` をサポートするサーバー/エッジ JavaScript ランタイムが対象です — Node.js 22+(`engines.node` で宣言しており、古い Node ではインストール時にブロックされます)、Cloudflare Workers、Vercel Edge、Deno、Bun。ブラウザは設計上対象外です(認可判定はサーバー側で enforcement する必要があるため)。同梱の `server` パッケージは引き続き Node 専用です。
|
|
6
6
|
|
|
7
7
|
## インストール
|
|
8
8
|
|
|
@@ -16,16 +16,29 @@ npm install @o3co/auth.policy-verifier.builtins
|
|
|
16
16
|
|
|
17
17
|
| 名前 | 読み取り元 | 出力 | コンストラクタ引数 |
|
|
18
18
|
| --- | --- | --- | --- |
|
|
19
|
-
| `PayloadScopeCollector` | `
|
|
20
|
-
| `PayloadSubjectIdCollector` | `
|
|
19
|
+
| `PayloadScopeCollector` | `subject.scope`(スペース区切り文字列) | `ATTR_SCOPES: string[]` | なし |
|
|
20
|
+
| `PayloadSubjectIdCollector` | `subject.sub`、`subject.azp` | `ATTR_USER_ID`、`ATTR_CLIENT_ID` | なし |
|
|
21
21
|
| `StaticPermissionCollector` | — | `ATTR_PERMISSIONS: string[]` | `{ permissions: string[] }` |
|
|
22
22
|
| `StaticRoleCollector` | — | `ATTR_ROLES: Role[]` | `{ roles: Role[] }` |
|
|
23
|
+
| `RequestContextAttributeCollector` | `requestContext` の宣言済みフィールド | 運用者が決めたキー | `{ attributes: Mapping[] }` |
|
|
23
24
|
|
|
24
25
|
`StaticPermissionCollector` と `StaticRoleCollector` は、リクエストのコンテキストに関わらず、コンストラクタに渡した値を常に出力します。
|
|
25
26
|
|
|
26
|
-
###
|
|
27
|
+
### RequestContextAttributeCollector
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
`CollectorContext.requestContext` の宣言済みフィールドを attribute に昇格させます:
|
|
30
|
+
|
|
31
|
+
```hocon
|
|
32
|
+
{ collector = "RequestContextAttributeCollector"
|
|
33
|
+
attributes = [
|
|
34
|
+
{ from = "tenant.id", to = "tenantId" } # dot path。`to` の既定値は `from`
|
|
35
|
+
{ from = "groups", type = "string[]" }
|
|
36
|
+
] }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
各マッピングは `{ from: string; to?: string; type?: "string" | "number" | "boolean" | "string[]" }` で、`type` の既定は `"string"` です。マッピング定義が不正ならコンストラクタで throw しますが、*値* が使えない場合は throw しません — `requestContext` は呼び出し側が渡すリクエストデータなので、欠落・空文字・宣言した型に合わない値は単に昇格されません。
|
|
40
|
+
|
|
41
|
+
この宣言が信頼境界です — [docs/extending.ja.md](../../docs/extending.ja.md#信頼境界-requestcontext-は呼び出し側のもの) が説明する境界の、既製の守り方がこのコレクターです。`requestContext` は自由形式かつ未検証なので、**宣言していないフィールドは attribute になりません**。dot path は own property のみを辿るため `constructor.name` のような指定は何も読みません。このコレクター自身は語彙を持ち込みません — フィールド名もキー名も運用者が決めるため、[AGENTS.md — Core Vocabulary Scope](../../AGENTS.md#core-vocabulary-scope) の方針を崩さずに実用的なものを提供できます。read-check-write を超える処理(値の導出、外部ストア参照など)が必要な場合は、同節が説明するプロジェクト側の `AttributeCollector` を書いてください。
|
|
29
42
|
|
|
30
43
|
## Rules
|
|
31
44
|
|
|
@@ -46,13 +59,15 @@ new HasPermission(permission: string)
|
|
|
46
59
|
### HasScope
|
|
47
60
|
|
|
48
61
|
```ts
|
|
49
|
-
new HasScope(scope: string)
|
|
62
|
+
new HasScope(scope: string, options?: { allowBareScopeRewrite?: boolean })
|
|
50
63
|
```
|
|
51
64
|
|
|
52
65
|
- `ruleType`: `"scope"`、`code`: `"invalid_scope"`
|
|
53
66
|
- `ATTR_SCOPES` を確認します。
|
|
54
|
-
-
|
|
55
|
-
-
|
|
67
|
+
- 比較は**大文字・小文字を区別する完全一致**です。OAuth 2.0 の scope 値は大文字・小文字を区別する不透明な文字列であるため([RFC 6749 §3.3](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3))、`read:PROJECT` は `read:project` を満たしません。
|
|
68
|
+
- `:` を 2 つ以上含む scope はそれ自体が 1 つの値であり、2 つ目の `:` 以降が切り捨てられることはありません。`read:project:restricted` は `read:project` を満たさず(意図的に絞り込まれた grant が広い方へ吸収されてはならない)、`read:project` も `read:project:restricted` を満たしません。
|
|
69
|
+
- `allowBareScopeRewrite`(既定 `false`)を有効にすると、プレフィックスなしの scope `"resource"` を、そのままの値に加えて `"read:resource"` としても扱います。書き換え対象は `:` を **1 つも含まない** scope のみで、`"project:restricted"` は書き換えません(どのセグメントが action かは判別不能であり、推測は過剰付与になるため)。issuer がプレフィックスなしのリソース名を発行する場合以外は無効のままにしてください。
|
|
70
|
+
- `ATTR_SCOPES` 内の文字列以外の要素は、一致もせず例外も投げません。
|
|
56
71
|
|
|
57
72
|
### AttrMatchRule
|
|
58
73
|
|
|
@@ -107,7 +122,7 @@ new AttrLiteralIn({ a: string, values: (string | number | boolean)[], group?: st
|
|
|
107
122
|
```
|
|
108
123
|
|
|
109
124
|
- `code`: `"attr_not_in_set"`。
|
|
110
|
-
- 既定の `ruleType`: `` `attr_literal_in:${a}:${type}:${count}:${hashPrefix}` `` — `count` は重複除去後の要素数、`hashPrefix` は重複除去かつソート済みの `values` を文字列化した内容に対する
|
|
125
|
+
- 既定の `ruleType`: `` `attr_literal_in:${a}:${type}:${count}:${hashPrefix}` `` — `count` は重複除去後の要素数、`hashPrefix` は重複除去かつソート済みの `values` を文字列化した内容に対する FNV-1a 64-bit ハッシュの 16 桁 16 進表記です。ハッシュは非暗号学的ですが、64-bit 幅により現実的な policy サイズで偶発的・意図的な衝突はいずれもほぼ起きません。`node:*` 依存はなく、対象とするサーバー/エッジランタイム(冒頭「Runtime」参照)でロードできます。同じ `a` と内容上等価な `values`(順序・重複を無視)を持つ 2 つのインスタンスは同じ `ruleType` を持ち、評価器で OR 結合されます。
|
|
111
126
|
- `values` は非空かつ同種の配列(`string[]`、`number[]`、`boolean[]` のいずれか)でなければなりません。`attrs.get(a)` が集合に含まれるときに通過します。`values` 内の重複要素は Rule の挙動に影響しません(内部では `Set` として扱われます)。
|
|
112
127
|
|
|
113
128
|
### AttrLiteralNotIn
|
|
@@ -171,7 +186,14 @@ new AttrPairCompare({ a: string, op: "lt" | "le" | "gt" | "ge", b: string, group
|
|
|
171
186
|
| `ResourceActionPermissionRuleCollector` | `"<resource.raw>.perm:<action>"` | `[HasPermission(...)]` |
|
|
172
187
|
| `ResourceActionScopeRuleCollector` | `"<action>:<resource.resourceType>"` | `[HasScope(...)]` |
|
|
173
188
|
|
|
174
|
-
|
|
189
|
+
`ResourceActionPermissionRuleCollector` にコンストラクタ引数はありません。
|
|
190
|
+
`ResourceActionScopeRuleCollector` は `{ scopeless?: "deny" | "skip", allowBareScopeRewrite?: boolean }` を受け取ります。
|
|
191
|
+
|
|
192
|
+
- `scopeless`(既定 `"deny"`): 既定ではリクエストごとに必ず `HasScope` ルールを生成するため、`scope` claim を持たない
|
|
193
|
+
トークンはこのルールに落ちます。`"skip"` は scopeless トークンに対してルールを生成しませんが、ルールが 1 つも集まらない
|
|
194
|
+
リクエストは deny されるため、別のルールグループが認可を担うパイプラインでのみ使ってください。
|
|
195
|
+
- `allowBareScopeRewrite`(既定 `false`): [`HasScope`](#hasscope) へそのまま渡されます。issuer が `{action}:{resourceType}`
|
|
196
|
+
形式(`read:project`)ではなくプレフィックスなしのリソース名(`project`)を発行する場合にのみ `true` にしてください。
|
|
175
197
|
|
|
176
198
|
## Resource Parser
|
|
177
199
|
|
|
@@ -183,13 +205,46 @@ new AttrPairCompare({ a: string, op: "lt" | "le" | "gt" | "ge", b: string, group
|
|
|
183
205
|
new DotNotationResourceParser()
|
|
184
206
|
```
|
|
185
207
|
|
|
186
|
-
|
|
208
|
+
文法:
|
|
209
|
+
|
|
210
|
+
```text
|
|
211
|
+
resource = segment *( "." segment )
|
|
212
|
+
segment = type [ ":" id ]
|
|
213
|
+
type = 1*tchar
|
|
214
|
+
id = 1*tchar
|
|
215
|
+
tchar = %x21 / %x23-2D / %x2F-39 / %x3B-5B / %x5D-7E
|
|
216
|
+
; RFC 6749 NQCHAR から "." と ":" を除いたもの
|
|
217
|
+
; すなわち、空白・`"`・`\`・`.`・`:` を除く印字可能 ASCII
|
|
218
|
+
```
|
|
187
219
|
|
|
188
|
-
例: `"foo.bar:123"` → `{ raw: "foo.bar:123", resourceType: "
|
|
220
|
+
例: `"foo.bar:123"` → `{ raw: "foo.bar:123", resourceType: "foo.bar", resourceId: "123" }`
|
|
189
221
|
|
|
190
222
|
- セグメントは `.` で分割されます。各セグメントには `:id` を含めることができます。
|
|
191
|
-
- `resourceType` はセグメントの type を
|
|
223
|
+
- `resourceType` はセグメントの type を `.` で結合した文字列です。区切り文字は書き換えず、そのまま保持されます。
|
|
192
224
|
- `resourceId` は最後のセグメントの id です(存在する場合)。
|
|
225
|
+
- `raw` は入力そのままです。
|
|
226
|
+
|
|
227
|
+
文法に合わない入力は `ResourceParseError`(`@o3co/auth.policy-verifier.core`)を送出します。パーサーが入力を
|
|
228
|
+
修復することはありません。サーバーはそのリクエストを decision ではなく `400 invalid_request` として返します。
|
|
229
|
+
拒否される例:
|
|
230
|
+
|
|
231
|
+
| 入力 | 理由 |
|
|
232
|
+
| --- | --- |
|
|
233
|
+
| `""`, `a..b`, `.a`, `a.` | 空セグメント — すべてのセグメントに type が必要 |
|
|
234
|
+
| `a:`, `:1` | type または id が空 |
|
|
235
|
+
| `a:1:2` | セグメント内に `:` が 2 個以上 — 末尾を切り捨てず拒否する |
|
|
236
|
+
| ` a:1 `, `a : 1` | 空白 — trim せず拒否する |
|
|
237
|
+
| `プロジェクト`, `a"b`, `a\b` | OAuth scope 値が持てない文字 |
|
|
238
|
+
|
|
239
|
+
`resourceType` は認可の名前空間です。`ResourceActionScopeRuleCollector` はこれを
|
|
240
|
+
`{action}:{resourceType}` scope に変換し、その scope が付与されていることを要求します。つまり同じ type に
|
|
241
|
+
パースされる 2 つの異なるリソースは同一に認可されてしまうため、そうならないように文法を設計しています。
|
|
242
|
+
`.` を区切り文字として予約することで、ネストした type `a.b` と `a_b` という名前のフラットな type が
|
|
243
|
+
区別されます(以前はどちらも `a_b` でした)。これは [`HasScope`](#hasscope) が scope 値に対して適用しているのと
|
|
244
|
+
同じ原則です — 書かれたものをそのまま比較し、意図を正規化して推測しない。
|
|
245
|
+
|
|
246
|
+
`.` や `:`、あるいは許可集合外の文字を必要とする id は、呼び出し側でエンコードする(percent-encoding は
|
|
247
|
+
この文法をそのまま通ります)か、その構文向けに書かれた `ResourceParser` で扱ってください。
|
|
193
248
|
|
|
194
249
|
## builtinCollectorsModule
|
|
195
250
|
|
|
@@ -205,7 +260,8 @@ import { builtinCollectorsModule } from "@o3co/auth.policy-verifier.builtins";
|
|
|
205
260
|
| `attributeCollector` | `"PayloadSubjectIdCollector"` | `() => new PayloadSubjectIdCollector()` |
|
|
206
261
|
| `attributeCollector` | `"StaticPermissionCollector"` | `(config) => new StaticPermissionCollector(config)` |
|
|
207
262
|
| `attributeCollector` | `"StaticRoleCollector"` | `(config) => new StaticRoleCollector(config)` |
|
|
208
|
-
| `
|
|
263
|
+
| `attributeCollector` | `"RequestContextAttributeCollector"` | `(config) => new RequestContextAttributeCollector(config)` |
|
|
264
|
+
| `ruleCollector` | `"ResourceActionScopeRuleCollector"` | `(config) => new ResourceActionScopeRuleCollector(config)` |
|
|
209
265
|
| `ruleCollector` | `"ResourceActionPermissionRuleCollector"` | `() => new ResourceActionPermissionRuleCollector()` |
|
|
210
266
|
| `resourceParser` | `"DotNotationResourceParser"` | `() => new DotNotationResourceParser()` |
|
|
211
267
|
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Built-in attribute collectors, rule collectors, and resource parser for auth.policy-verifier.
|
|
4
4
|
|
|
5
|
-
**Runtime:** Node.js 22
|
|
5
|
+
**Runtime:** Server- and edge-side JavaScript runtimes that support `BigInt` and `Map.groupBy` — Node.js 22+ (declared via `engines.node` so older Node installs are blocked at install time), Cloudflare Workers, Vercel Edge, Deno, Bun. Browsers are out of scope by design: authorization decisions must be enforced server-side. The `server` companion package remains Node-only.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -16,16 +16,29 @@ All collectors implement `AttributeCollector`.
|
|
|
16
16
|
|
|
17
17
|
| Name | Reads from | Emits | Constructor args |
|
|
18
18
|
| --- | --- | --- | --- |
|
|
19
|
-
| `PayloadScopeCollector` | `
|
|
20
|
-
| `PayloadSubjectIdCollector` | `
|
|
19
|
+
| `PayloadScopeCollector` | `subject.scope` (space-separated string) | `ATTR_SCOPES: string[]` | none |
|
|
20
|
+
| `PayloadSubjectIdCollector` | `subject.sub`, `subject.azp` | `ATTR_USER_ID`, `ATTR_CLIENT_ID` | none |
|
|
21
21
|
| `StaticPermissionCollector` | — | `ATTR_PERMISSIONS: string[]` | `{ permissions: string[] }` |
|
|
22
22
|
| `StaticRoleCollector` | — | `ATTR_ROLES: Role[]` | `{ roles: Role[] }` |
|
|
23
|
+
| `RequestContextAttributeCollector` | declared fields of `requestContext` | the operator's own keys | `{ attributes: Mapping[] }` |
|
|
23
24
|
|
|
24
25
|
`StaticPermissionCollector` and `StaticRoleCollector` always emit the values supplied at construction time, regardless of request context.
|
|
25
26
|
|
|
26
|
-
###
|
|
27
|
+
### RequestContextAttributeCollector
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
Promotes declared fields of `CollectorContext.requestContext` into attributes:
|
|
30
|
+
|
|
31
|
+
```hocon
|
|
32
|
+
{ collector = "RequestContextAttributeCollector"
|
|
33
|
+
attributes = [
|
|
34
|
+
{ from = "tenant.id", to = "tenantId" } # dot path; `to` defaults to `from`
|
|
35
|
+
{ from = "groups", type = "string[]" }
|
|
36
|
+
] }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Each mapping is `{ from: string; to?: string; type?: "string" | "number" | "boolean" | "string[]" }`, `type` defaulting to `"string"`. A malformed mapping throws at construction; an unusable *value* does not — `requestContext` is caller-supplied request data, so a field that is missing, empty, or not of its declared type is simply not promoted.
|
|
40
|
+
|
|
41
|
+
The declaration is the trust boundary — this collector is the ready-made way to stay on the right side of the one described in [docs/extending.md](../../docs/extending.md#the-trust-boundary-requestcontext-is-the-callers). `requestContext` is free-form and unvalidated, so **nothing undeclared becomes an attribute** and a configured dot path traverses own properties only (`constructor.name` reads nothing). This collector invents no vocabulary of its own: the operator names both the fields and the keys, which is what keeps [AGENTS.md — Core Vocabulary Scope](../../AGENTS.md#core-vocabulary-scope) intact while still shipping something usable. For anything beyond read-check-write — deriving a value, calling out to a store — write a focused project-side `AttributeCollector` as that section describes.
|
|
29
42
|
|
|
30
43
|
## Rules
|
|
31
44
|
|
|
@@ -37,22 +50,26 @@ new HasPermission(permission: string)
|
|
|
37
50
|
|
|
38
51
|
- `ruleType`: `"permission"`, `code`: `"no_permission"`
|
|
39
52
|
- Checks `ATTR_PERMISSIONS` (direct) and `ATTR_ROLES[].permissions` (via roles).
|
|
40
|
-
-
|
|
53
|
+
- Matching is **exact and case-sensitive** — the same discipline `HasScope` applies to scopes and `DotNotationResourceParser` applies to resources: compare what was written, never a normalized guess at what was meant. The parser preserves case, so `Project:1.perm:read` and `project:1.perm:read` are different permissions, exactly as `Project:1` and `project:1` are different resources to a scope rule.
|
|
54
|
+
- Wildcards in a **granted** permission are honoured — written match structure, not normalization; the literal halves around the `*` still compare exactly:
|
|
41
55
|
- `"*"` matches any permission.
|
|
42
56
|
- `"foo*"` matches any permission with prefix `foo`.
|
|
43
57
|
- `"*bar"` matches any permission with suffix `bar`.
|
|
44
58
|
- `"foo*bar"` matches any permission starting with `foo` and ending with `bar`.
|
|
59
|
+
- More than one `*` in a granted permission never matches (rejecting beats silently over-granting).
|
|
45
60
|
|
|
46
61
|
### HasScope
|
|
47
62
|
|
|
48
63
|
```ts
|
|
49
|
-
new HasScope(scope: string)
|
|
64
|
+
new HasScope(scope: string, options?: { allowBareScopeRewrite?: boolean })
|
|
50
65
|
```
|
|
51
66
|
|
|
52
67
|
- `ruleType`: `"scope"`, `code`: `"invalid_scope"`
|
|
53
68
|
- Checks `ATTR_SCOPES`.
|
|
54
|
-
-
|
|
55
|
-
-
|
|
69
|
+
- Matching is **exact and case-sensitive**. OAuth 2.0 scope values are case-sensitive opaque strings ([RFC 6749 §3.3](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3)), so `read:PROJECT` does not satisfy `read:project`.
|
|
70
|
+
- A scope containing more than one `:` is a value in its own right — nothing is split off at the second `:`. `read:project:restricted` does not satisfy `read:project` (a deliberately narrowed grant must not collapse into the broader one), and `read:project` does not satisfy `read:project:restricted`.
|
|
71
|
+
- `allowBareScopeRewrite` (default `false`) opts in to treating a bare granted scope `"resource"` as `"read:resource"` as well as literally. Only a scope with **no** `:` is ever rewritten; `"project:restricted"` is left alone, because which segment is the action is unknowable and guessing over-grants. Leave it off unless your issuer emits bare resource names.
|
|
72
|
+
- Non-string entries in `ATTR_SCOPES` never match and never throw.
|
|
56
73
|
|
|
57
74
|
### AttrMatchRule
|
|
58
75
|
|
|
@@ -107,7 +124,7 @@ new AttrLiteralIn({ a: string, values: (string | number | boolean)[], group?: st
|
|
|
107
124
|
```
|
|
108
125
|
|
|
109
126
|
- `code`: `"attr_not_in_set"`.
|
|
110
|
-
- Default `ruleType`: `` `attr_literal_in:${a}:${type}:${count}:${hashPrefix}` `` — where `count` is the post-deduplication element count and `hashPrefix` is
|
|
127
|
+
- Default `ruleType`: `` `attr_literal_in:${a}:${type}:${count}:${hashPrefix}` `` — where `count` is the post-deduplication element count and `hashPrefix` is a 16-hex-character FNV-1a 64-bit hash over the deduplicated, sorted, stringified values. The hash is non-cryptographic but the 64-bit width makes accidental and adversarial collisions vanishingly unlikely for any realistic policy size; the package has no `node:*` dependency and loads in any supported server/edge runtime (see "Runtime" above). Two instances with the same `a` and logically equivalent `values` (duplicates and order do not matter) share the same `ruleType` and are OR-combined by the evaluator.
|
|
111
128
|
- `values` must be a non-empty, homogeneous array (`string[]`, `number[]`, or `boolean[]`). Passes when `attrs.get(a)` is in the set. Duplicate elements in `values` are ignored (the rule uses `Set` semantics internally).
|
|
112
129
|
|
|
113
130
|
### AttrLiteralNotIn
|
|
@@ -171,7 +188,14 @@ All attribute comparison rules follow the same grouping semantics described for
|
|
|
171
188
|
| `ResourceActionPermissionRuleCollector` | `"<resource.raw>.perm:<action>"` | `[HasPermission(...)]` |
|
|
172
189
|
| `ResourceActionScopeRuleCollector` | `"<action>:<resource.resourceType>"` | `[HasScope(...)]` |
|
|
173
190
|
|
|
174
|
-
|
|
191
|
+
`ResourceActionPermissionRuleCollector` takes no constructor arguments.
|
|
192
|
+
`ResourceActionScopeRuleCollector` accepts `{ scopeless?: "deny" | "skip", allowBareScopeRewrite?: boolean }`.
|
|
193
|
+
|
|
194
|
+
- `scopeless` (default `"deny"`): it emits the `HasScope` rule for every request, so a token carrying no `scope`
|
|
195
|
+
claim fails it. `"skip"` emits no rule for a scopeless token — only use it in a pipeline where another rule
|
|
196
|
+
group authorizes the request, since a request that collects no rule at all is denied.
|
|
197
|
+
- `allowBareScopeRewrite` (default `false`): forwarded to [`HasScope`](#hasscope). Set it to `true` only if your
|
|
198
|
+
issuer emits bare resource names (`project`) rather than `{action}:{resourceType}` scopes (`read:project`).
|
|
175
199
|
|
|
176
200
|
## Resource Parser
|
|
177
201
|
|
|
@@ -183,13 +207,48 @@ Parses a dot-notation string into a `Resource`.
|
|
|
183
207
|
new DotNotationResourceParser()
|
|
184
208
|
```
|
|
185
209
|
|
|
186
|
-
|
|
210
|
+
Grammar:
|
|
211
|
+
|
|
212
|
+
```text
|
|
213
|
+
resource = segment *( "." segment )
|
|
214
|
+
segment = type [ ":" id ]
|
|
215
|
+
type = 1*tchar
|
|
216
|
+
id = 1*tchar
|
|
217
|
+
tchar = %x21 / %x23-2D / %x2F-39 / %x3B-5B / %x5D-7E
|
|
218
|
+
; RFC 6749 NQCHAR less "." and ":"
|
|
219
|
+
; i.e. printable ASCII except space, `"`, `\`, `.` and `:`
|
|
220
|
+
```
|
|
187
221
|
|
|
188
|
-
Example: `"foo.bar:123"` → `{ raw: "foo.bar:123", resourceType: "
|
|
222
|
+
Example: `"foo.bar:123"` → `{ raw: "foo.bar:123", resourceType: "foo.bar", resourceId: "123" }`
|
|
189
223
|
|
|
190
224
|
- Segments are split by `.`. Each segment may include `:id`.
|
|
191
|
-
- `resourceType` is the segment types joined with
|
|
225
|
+
- `resourceType` is the segment types joined with `.` — the separator is preserved, not rewritten.
|
|
192
226
|
- `resourceId` is the id of the last segment, if present.
|
|
227
|
+
- `raw` is the input verbatim.
|
|
228
|
+
|
|
229
|
+
Anything the grammar does not accept raises `ResourceParseError`
|
|
230
|
+
(from `@o3co/auth.policy-verifier.core`); the parser never repairs its input. The server answers
|
|
231
|
+
such a request `400 invalid_request`, not a decision. Refused, among others:
|
|
232
|
+
|
|
233
|
+
| Input | Why |
|
|
234
|
+
| --- | --- |
|
|
235
|
+
| `""`, `a..b`, `.a`, `a.` | an empty segment — every segment needs a type |
|
|
236
|
+
| `a:`, `:1` | an empty type or id |
|
|
237
|
+
| `a:1:2` | more than one `:` in a segment — the tail is refused, not truncated away |
|
|
238
|
+
| ` a:1 `, `a : 1` | whitespace — it is refused, not trimmed |
|
|
239
|
+
| `プロジェクト`, `a"b`, `a\b` | a character no OAuth scope value may carry |
|
|
240
|
+
|
|
241
|
+
`resourceType` is the authorization namespace: `ResourceActionScopeRuleCollector` turns it into the
|
|
242
|
+
`{action}:{resourceType}` scope that must be granted. Two distinct resources that parse to the same
|
|
243
|
+
type are therefore authorized identically, so the grammar is built to make that impossible —
|
|
244
|
+
`.` is reserved as the separator, which keeps the nested type `a.b` distinct from the flat type
|
|
245
|
+
literally named `a_b` (both were `a_b` before). This is the same principle
|
|
246
|
+
[`HasScope`](#hasscope) applies to scope values: compare what was written, never a normalized guess
|
|
247
|
+
at what was meant.
|
|
248
|
+
|
|
249
|
+
An id that needs `.`, `:` or a character outside the set must be encoded by the caller
|
|
250
|
+
(percent-encoding round-trips through this grammar) or handled by a `ResourceParser` written for
|
|
251
|
+
that syntax.
|
|
193
252
|
|
|
194
253
|
## builtinCollectorsModule
|
|
195
254
|
|
|
@@ -205,7 +264,8 @@ import { builtinCollectorsModule } from "@o3co/auth.policy-verifier.builtins";
|
|
|
205
264
|
| `attributeCollector` | `"PayloadSubjectIdCollector"` | `() => new PayloadSubjectIdCollector()` |
|
|
206
265
|
| `attributeCollector` | `"StaticPermissionCollector"` | `(config) => new StaticPermissionCollector(config)` |
|
|
207
266
|
| `attributeCollector` | `"StaticRoleCollector"` | `(config) => new StaticRoleCollector(config)` |
|
|
208
|
-
| `
|
|
267
|
+
| `attributeCollector` | `"RequestContextAttributeCollector"` | `(config) => new RequestContextAttributeCollector(config)` |
|
|
268
|
+
| `ruleCollector` | `"ResourceActionScopeRuleCollector"` | `(config) => new ResourceActionScopeRuleCollector(config)` |
|
|
209
269
|
| `ruleCollector` | `"ResourceActionPermissionRuleCollector"` | `() => new ResourceActionPermissionRuleCollector()` |
|
|
210
270
|
| `resourceParser` | `"DotNotationResourceParser"` | `() => new DotNotationResourceParser()` |
|
|
211
271
|
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { AttributeCollector, Attributes, CollectorContext } from "@o3co/auth.policy-verifier.core";
|
|
2
|
+
/**
|
|
3
|
+
* Attribute collector that splits the OAuth2 `scope` claim (space-delimited)
|
|
4
|
+
* into a `string[]` and exposes it under `ATTR_SCOPES`. Missing or non-string
|
|
5
|
+
* `scope` yields an empty array.
|
|
6
|
+
*/
|
|
2
7
|
export declare class PayloadScopeCollector implements AttributeCollector {
|
|
3
8
|
collect(context: CollectorContext): Promise<Attributes>;
|
|
4
9
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PayloadScopeCollector.d.mts","sourceRoot":"","sources":["../../src/collectors/PayloadScopeCollector.mts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"PayloadScopeCollector.d.mts","sourceRoot":"","sources":["../../src/collectors/PayloadScopeCollector.mts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACX,kBAAkB,EAClB,UAAU,EACV,gBAAgB,EAChB,MAAM,iCAAiC,CAAC;AAGzC;;;;GAIG;AACH,qBAAa,qBAAsB,YAAW,kBAAkB;IACzD,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;CAK7D"}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 1o1 Co. Ltd.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { ATTR_SCOPES } from "@o3co/auth.policy-verifier.core";
|
|
4
|
+
/**
|
|
5
|
+
* Attribute collector that splits the OAuth2 `scope` claim (space-delimited)
|
|
6
|
+
* into a `string[]` and exposes it under `ATTR_SCOPES`. Missing or non-string
|
|
7
|
+
* `scope` yields an empty array.
|
|
8
|
+
*/
|
|
2
9
|
export class PayloadScopeCollector {
|
|
3
10
|
async collect(context) {
|
|
4
|
-
const scope = context.
|
|
11
|
+
const scope = context.subject.scope;
|
|
5
12
|
const scopes = typeof scope === "string" && scope ? scope.split(" ").filter(Boolean) : [];
|
|
6
13
|
return new Map([[ATTR_SCOPES, scopes]]);
|
|
7
14
|
}
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import type { AttributeCollector, Attributes, CollectorContext } from "@o3co/auth.policy-verifier.core";
|
|
2
|
+
/**
|
|
3
|
+
* Attribute collector that extracts the OAuth/OIDC `sub` and `azp` claims from
|
|
4
|
+
* the subject bag into `ATTR_USER_ID` and `ATTR_CLIENT_ID`. Either claim may
|
|
5
|
+
* be absent.
|
|
6
|
+
*
|
|
7
|
+
* The claim vocabulary lives here, not in core (#170): `SubjectAttributes` is
|
|
8
|
+
* a bag of unknowns, so this collector narrows what it promotes — a claim that
|
|
9
|
+
* is not a non-empty string is not an identity and is left out.
|
|
10
|
+
*/
|
|
2
11
|
export declare class PayloadSubjectIdCollector implements AttributeCollector {
|
|
3
12
|
collect(context: CollectorContext): Promise<Attributes>;
|
|
4
13
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PayloadSubjectIdCollector.d.mts","sourceRoot":"","sources":["../../src/collectors/PayloadSubjectIdCollector.mts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"PayloadSubjectIdCollector.d.mts","sourceRoot":"","sources":["../../src/collectors/PayloadSubjectIdCollector.mts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACX,kBAAkB,EAClB,UAAU,EACV,gBAAgB,EAChB,MAAM,iCAAiC,CAAC;AAGzC;;;;;;;;GAQG;AACH,qBAAa,yBAA0B,YAAW,kBAAkB;IAC7D,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;CAW7D"}
|
|
@@ -1,12 +1,24 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 1o1 Co. Ltd.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { ATTR_CLIENT_ID, ATTR_USER_ID } from "@o3co/auth.policy-verifier.core";
|
|
4
|
+
/**
|
|
5
|
+
* Attribute collector that extracts the OAuth/OIDC `sub` and `azp` claims from
|
|
6
|
+
* the subject bag into `ATTR_USER_ID` and `ATTR_CLIENT_ID`. Either claim may
|
|
7
|
+
* be absent.
|
|
8
|
+
*
|
|
9
|
+
* The claim vocabulary lives here, not in core (#170): `SubjectAttributes` is
|
|
10
|
+
* a bag of unknowns, so this collector narrows what it promotes — a claim that
|
|
11
|
+
* is not a non-empty string is not an identity and is left out.
|
|
12
|
+
*/
|
|
2
13
|
export class PayloadSubjectIdCollector {
|
|
3
14
|
async collect(context) {
|
|
4
15
|
const attrs = new Map();
|
|
5
|
-
|
|
6
|
-
|
|
16
|
+
const { sub, azp } = context.subject;
|
|
17
|
+
if (typeof sub === "string" && sub) {
|
|
18
|
+
attrs.set(ATTR_USER_ID, sub);
|
|
7
19
|
}
|
|
8
|
-
if (
|
|
9
|
-
attrs.set(ATTR_CLIENT_ID,
|
|
20
|
+
if (typeof azp === "string" && azp) {
|
|
21
|
+
attrs.set(ATTR_CLIENT_ID, azp);
|
|
10
22
|
}
|
|
11
23
|
return attrs;
|
|
12
24
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { AttributeCollector, Attributes, CollectorContext } from "@o3co/auth.policy-verifier.core";
|
|
2
|
+
/** Types a `requestContext` field may be promoted as. */
|
|
3
|
+
export type RequestContextAttributeType = "string" | "number" | "boolean" | "string[]";
|
|
4
|
+
/** One field this collector promotes out of `requestContext`. */
|
|
5
|
+
export interface RequestContextAttributeMapping {
|
|
6
|
+
/** Field to read, as a dot path (`"tenant.id"`) into `requestContext`. */
|
|
7
|
+
from: string;
|
|
8
|
+
/** Attribute key to write. Defaults to `from`. */
|
|
9
|
+
to?: string;
|
|
10
|
+
/** Expected type; a value of any other shape is not promoted. Defaults to `"string"`. */
|
|
11
|
+
type?: RequestContextAttributeType;
|
|
12
|
+
}
|
|
13
|
+
/** Config entry accepted by `RequestContextAttributeCollector`. */
|
|
14
|
+
export interface RequestContextAttributeCollectorConfig {
|
|
15
|
+
/** The fields to promote. Must declare at least one. */
|
|
16
|
+
attributes: RequestContextAttributeMapping[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Promotes declared fields of `CollectorContext.requestContext` into attributes.
|
|
20
|
+
*
|
|
21
|
+
* `requestContext` is the free-form container the transport fills in, and until
|
|
22
|
+
* now nothing built-in consumed it: any environment or relationship attribute
|
|
23
|
+
* meant writing a bespoke collector, even for the common case of "read this
|
|
24
|
+
* field, check its type, write it under this key".
|
|
25
|
+
*
|
|
26
|
+
* Core deliberately assumes no shape for `requestContext` and keeps its `ATTR_*`
|
|
27
|
+
* vocabulary to OAuth/OIDC/RBAC standards, so this collector invents no
|
|
28
|
+
* vocabulary either: the operator declares which fields to read and what to call
|
|
29
|
+
* them, and nothing undeclared is promoted. That declaration is also the trust
|
|
30
|
+
* boundary — `requestContext` is caller-supplied and unvalidated, so a field the
|
|
31
|
+
* config did not name cannot reach a rule, and one whose value does not match
|
|
32
|
+
* its declared type is dropped rather than passed along.
|
|
33
|
+
*
|
|
34
|
+
* ```hocon
|
|
35
|
+
* { collector = "RequestContextAttributeCollector"
|
|
36
|
+
* attributes = [
|
|
37
|
+
* { from = "tenant.id", to = "tenantId" }
|
|
38
|
+
* { from = "groups", type = "string[]" }
|
|
39
|
+
* ] }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare class RequestContextAttributeCollector implements AttributeCollector {
|
|
43
|
+
private readonly mappings;
|
|
44
|
+
constructor(config: RequestContextAttributeCollectorConfig);
|
|
45
|
+
collect(context: CollectorContext): Promise<Attributes>;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=RequestContextAttributeCollector.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RequestContextAttributeCollector.d.mts","sourceRoot":"","sources":["../../src/collectors/RequestContextAttributeCollector.mts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACX,kBAAkB,EAClB,UAAU,EACV,gBAAgB,EAChB,MAAM,iCAAiC,CAAC;AAGzC,yDAAyD;AACzD,MAAM,MAAM,2BAA2B,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,UAAU,CAAC;AASvF,iEAAiE;AACjE,MAAM,WAAW,8BAA8B;IAC9C,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,yFAAyF;IACzF,IAAI,CAAC,EAAE,2BAA2B,CAAC;CACnC;AAED,mEAAmE;AACnE,MAAM,WAAW,sCAAsC;IACtD,wDAAwD;IACxD,UAAU,EAAE,8BAA8B,EAAE,CAAC;CAC7C;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,gCAAiC,YAAW,kBAAkB;IAC1E,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6C;gBAE1D,MAAM,EAAE,sCAAsC;IA4BpD,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;CAgB7D"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 1o1 Co. Ltd.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { readUntrustedRequestContext } from "@o3co/auth.policy-verifier.core";
|
|
4
|
+
const ATTRIBUTE_TYPES = [
|
|
5
|
+
"string",
|
|
6
|
+
"number",
|
|
7
|
+
"boolean",
|
|
8
|
+
"string[]",
|
|
9
|
+
];
|
|
10
|
+
/**
|
|
11
|
+
* Promotes declared fields of `CollectorContext.requestContext` into attributes.
|
|
12
|
+
*
|
|
13
|
+
* `requestContext` is the free-form container the transport fills in, and until
|
|
14
|
+
* now nothing built-in consumed it: any environment or relationship attribute
|
|
15
|
+
* meant writing a bespoke collector, even for the common case of "read this
|
|
16
|
+
* field, check its type, write it under this key".
|
|
17
|
+
*
|
|
18
|
+
* Core deliberately assumes no shape for `requestContext` and keeps its `ATTR_*`
|
|
19
|
+
* vocabulary to OAuth/OIDC/RBAC standards, so this collector invents no
|
|
20
|
+
* vocabulary either: the operator declares which fields to read and what to call
|
|
21
|
+
* them, and nothing undeclared is promoted. That declaration is also the trust
|
|
22
|
+
* boundary — `requestContext` is caller-supplied and unvalidated, so a field the
|
|
23
|
+
* config did not name cannot reach a rule, and one whose value does not match
|
|
24
|
+
* its declared type is dropped rather than passed along.
|
|
25
|
+
*
|
|
26
|
+
* ```hocon
|
|
27
|
+
* { collector = "RequestContextAttributeCollector"
|
|
28
|
+
* attributes = [
|
|
29
|
+
* { from = "tenant.id", to = "tenantId" }
|
|
30
|
+
* { from = "groups", type = "string[]" }
|
|
31
|
+
* ] }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export class RequestContextAttributeCollector {
|
|
35
|
+
mappings;
|
|
36
|
+
constructor(config) {
|
|
37
|
+
const attributes = config?.attributes;
|
|
38
|
+
if (!Array.isArray(attributes) || attributes.length === 0) {
|
|
39
|
+
throw new Error("RequestContextAttributeCollector: attributes must be a non-empty array of mappings");
|
|
40
|
+
}
|
|
41
|
+
this.mappings = attributes.map((mapping, index) => {
|
|
42
|
+
const { from, to, type = "string" } = mapping ?? {};
|
|
43
|
+
if (typeof from !== "string" || from === "") {
|
|
44
|
+
throw new Error(`RequestContextAttributeCollector: attributes[${index}].from must be a non-empty string`);
|
|
45
|
+
}
|
|
46
|
+
if (to !== undefined && (typeof to !== "string" || to === "")) {
|
|
47
|
+
throw new Error(`RequestContextAttributeCollector: attributes[${index}].to must be a non-empty string`);
|
|
48
|
+
}
|
|
49
|
+
if (!ATTRIBUTE_TYPES.includes(type)) {
|
|
50
|
+
throw new Error(`RequestContextAttributeCollector: attributes[${index}].type must be one of ${ATTRIBUTE_TYPES.join(", ")}, got "${type}"`);
|
|
51
|
+
}
|
|
52
|
+
return { from, to: to ?? from, type };
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async collect(context) {
|
|
56
|
+
const attrs = new Map();
|
|
57
|
+
// The unwrap is the acknowledgement `UntrustedRequestContext` asks for:
|
|
58
|
+
// everything below this line is the caller's own data, which is why the
|
|
59
|
+
// mapping list — not the request — decides what becomes an attribute.
|
|
60
|
+
const requestContext = readUntrustedRequestContext(context.requestContext);
|
|
61
|
+
if (requestContext === undefined)
|
|
62
|
+
return attrs;
|
|
63
|
+
for (const mapping of this.mappings) {
|
|
64
|
+
const raw = readPath(requestContext, mapping.from);
|
|
65
|
+
if (matchesType(raw, mapping.type)) {
|
|
66
|
+
attrs.set(mapping.to, Array.isArray(raw) ? [...raw] : raw);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return attrs;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Reads a dot path, traversing own properties only. `requestContext` is
|
|
74
|
+
* caller-supplied, so inherited members (`constructor`, `toString`, …) must not
|
|
75
|
+
* be reachable through a configured path.
|
|
76
|
+
*/
|
|
77
|
+
function readPath(root, path) {
|
|
78
|
+
let current = root;
|
|
79
|
+
for (const segment of path.split(".")) {
|
|
80
|
+
if (typeof current !== "object" || current === null)
|
|
81
|
+
return undefined;
|
|
82
|
+
if (!Object.hasOwn(current, segment))
|
|
83
|
+
return undefined;
|
|
84
|
+
current = current[segment];
|
|
85
|
+
}
|
|
86
|
+
return current;
|
|
87
|
+
}
|
|
88
|
+
/** Whether `value` is usable as the declared type. Empty strings count as absent. */
|
|
89
|
+
function matchesType(value, type) {
|
|
90
|
+
switch (type) {
|
|
91
|
+
case "string":
|
|
92
|
+
return typeof value === "string" && value !== "";
|
|
93
|
+
case "number":
|
|
94
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
95
|
+
case "boolean":
|
|
96
|
+
return typeof value === "boolean";
|
|
97
|
+
case "string[]":
|
|
98
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string" && item !== "");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { AttributeCollector, Attributes, CollectorContext } from "@o3co/auth.policy-verifier.core";
|
|
2
|
+
/**
|
|
3
|
+
* Attribute collector that returns a configured constant permission list under
|
|
4
|
+
* `ATTR_PERMISSIONS`, independent of the JWT payload. Useful for
|
|
5
|
+
* environments where permissions are static per deployment.
|
|
6
|
+
*/
|
|
2
7
|
export declare class StaticPermissionCollector implements AttributeCollector {
|
|
3
8
|
private permissions;
|
|
4
9
|
constructor(config: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StaticPermissionCollector.d.mts","sourceRoot":"","sources":["../../src/collectors/StaticPermissionCollector.mts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"StaticPermissionCollector.d.mts","sourceRoot":"","sources":["../../src/collectors/StaticPermissionCollector.mts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACX,kBAAkB,EAClB,UAAU,EACV,gBAAgB,EAChB,MAAM,iCAAiC,CAAC;AAGzC;;;;GAIG;AACH,qBAAa,yBAA0B,YAAW,kBAAkB;IACnE,OAAO,CAAC,WAAW,CAAW;gBAElB,MAAM,EAAE;QAAE,WAAW,EAAE,MAAM,EAAE,CAAA;KAAE;IAIvC,OAAO,CAAC,QAAQ,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;CAG9D"}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 1o1 Co. Ltd.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { ATTR_PERMISSIONS } from "@o3co/auth.policy-verifier.core";
|
|
4
|
+
/**
|
|
5
|
+
* Attribute collector that returns a configured constant permission list under
|
|
6
|
+
* `ATTR_PERMISSIONS`, independent of the JWT payload. Useful for
|
|
7
|
+
* environments where permissions are static per deployment.
|
|
8
|
+
*/
|
|
2
9
|
export class StaticPermissionCollector {
|
|
3
10
|
permissions;
|
|
4
11
|
constructor(config) {
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { AttributeCollector, Attributes, CollectorContext, Role } from "@o3co/auth.policy-verifier.core";
|
|
2
|
+
/**
|
|
3
|
+
* Attribute collector that returns a configured constant role list under
|
|
4
|
+
* `ATTR_ROLES`, independent of the JWT payload. Each role bundles a name and
|
|
5
|
+
* the permissions it implies.
|
|
6
|
+
*/
|
|
2
7
|
export declare class StaticRoleCollector implements AttributeCollector {
|
|
3
8
|
private roles;
|
|
4
9
|
constructor(config: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StaticRoleCollector.d.mts","sourceRoot":"","sources":["../../src/collectors/StaticRoleCollector.mts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"StaticRoleCollector.d.mts","sourceRoot":"","sources":["../../src/collectors/StaticRoleCollector.mts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACX,kBAAkB,EAClB,UAAU,EACV,gBAAgB,EAChB,IAAI,EACJ,MAAM,iCAAiC,CAAC;AAGzC;;;;GAIG;AACH,qBAAa,mBAAoB,YAAW,kBAAkB;IAC7D,OAAO,CAAC,KAAK,CAAS;gBAEV,MAAM,EAAE;QAAE,KAAK,EAAE,IAAI,EAAE,CAAA;KAAE;IAI/B,OAAO,CAAC,QAAQ,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;CAG9D"}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 1o1 Co. Ltd.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { ATTR_ROLES } from "@o3co/auth.policy-verifier.core";
|
|
4
|
+
/**
|
|
5
|
+
* Attribute collector that returns a configured constant role list under
|
|
6
|
+
* `ATTR_ROLES`, independent of the JWT payload. Each role bundles a name and
|
|
7
|
+
* the permissions it implies.
|
|
8
|
+
*/
|
|
2
9
|
export class StaticRoleCollector {
|
|
3
10
|
roles;
|
|
4
11
|
constructor(config) {
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { PayloadScopeCollector } from "./collectors/PayloadScopeCollector.mjs";
|
|
2
2
|
export { PayloadSubjectIdCollector } from "./collectors/PayloadSubjectIdCollector.mjs";
|
|
3
|
+
export { RequestContextAttributeCollector, type RequestContextAttributeCollectorConfig, type RequestContextAttributeMapping, type RequestContextAttributeType, } from "./collectors/RequestContextAttributeCollector.mjs";
|
|
3
4
|
export { StaticPermissionCollector } from "./collectors/StaticPermissionCollector.mjs";
|
|
4
5
|
export { StaticRoleCollector } from "./collectors/StaticRoleCollector.mjs";
|
|
5
6
|
export { builtinCollectorsModule } from "./module.mjs";
|
|
@@ -14,7 +15,7 @@ export { AttrPairCompare, type AttrPairCompareConfig } from "./rules/AttrPairCom
|
|
|
14
15
|
export { AttrPairEqual, type AttrPairEqualConfig } from "./rules/AttrPairEqual.mjs";
|
|
15
16
|
export { AttrPairNotEqual, type AttrPairNotEqualConfig } from "./rules/AttrPairNotEqual.mjs";
|
|
16
17
|
export { ResourceActionPermissionRuleCollector } from "./rules/collectors/ResourceActionPermissionRuleCollector.mjs";
|
|
17
|
-
export { ResourceActionScopeRuleCollector } from "./rules/collectors/ResourceActionScopeRuleCollector.mjs";
|
|
18
|
+
export { ResourceActionScopeRuleCollector, type ResourceActionScopeRuleCollectorConfig, type ScopelessPolicy, } from "./rules/collectors/ResourceActionScopeRuleCollector.mjs";
|
|
18
19
|
export { HasPermission } from "./rules/HasPermission.mjs";
|
|
19
|
-
export { HasScope } from "./rules/HasScope.mjs";
|
|
20
|
+
export { HasScope, type HasScopeOptions } from "./rules/HasScope.mjs";
|
|
20
21
|
//# sourceMappingURL=index.d.mts.map
|