@o3co/create-auth-policy-verifier 0.10.0 → 0.12.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/package.json +1 -1
- package/templates/standalone/README.ja.md +35 -1
- package/templates/standalone/README.md +35 -1
- package/templates/standalone/config/application.conf +11 -0
- package/templates/standalone/package.json +1 -1
- package/templates/standalone/src/__tests__/loadConfig.test.mts +2 -1
- package/templates/versions.json +3 -3
package/package.json
CHANGED
|
@@ -50,6 +50,7 @@ OAUTH_JWT_SECRET=$(openssl rand -hex 32) \
|
|
|
50
50
|
| `RULE_ON_EMPTY_RULE_SET` | `deny` | ルールが 1 つも集まらなかったときの決定(`deny` \| `allow`) |
|
|
51
51
|
| `VERIFY_MAX_BATCH_SIZE` | `50` | `POST /verify/batch` の件数上限 |
|
|
52
52
|
| `VERIFY_CREDENTIAL_TO_COLLECTORS` | `never` | `expose` は生の資格情報を `context.credential` として collector に渡す — subject として下流 API を呼ぶ collector のためだけの設定。既定では collector は検証済みクレームだけを扱う。資格情報はリプレイ可能であり、context がログに出ればそれが漏れる |
|
|
53
|
+
| `VERIFY_EVALUATION_IN_RESPONSE` | `omit` | `include` にすると、policy を背後に持つ各 Rule の `evaluation`(status と、評価した policy revision。#244)を決定の response に載せる。どの revision が操作を許可したかを記録する呼び出し側サービス向け。`decision` ログイベントにはどちらの設定でも載る。受理される token の保持者全員に「policy set がいつ変わったか」を伝えることになるので、それが問題になる環境では `HTTP_CALLER_AUTH_TOKEN` と併用する |
|
|
53
54
|
| `LOG_LEVEL` | `info` | 出力する最低レベル: `trace`\|`debug`\|`info`\|`warn`\|`error`\|`fatal`\|`silent`。decision ログのスイッチも兼ねる — [可観測性](#可観測性)を参照 |
|
|
54
55
|
|
|
55
56
|
## HS256 シークレットの強度
|
|
@@ -128,7 +129,7 @@ Authorization: Bearer <jwt>
|
|
|
128
129
|
| `code` | deny のみ — 呼び出し元がワイヤ上で受け取ったコードと同じ |
|
|
129
130
|
| `satisfiedBy` | allow のみ — 各グループを満たしたルールの `{ruleType, code}` |
|
|
130
131
|
| `deniedBy` | deny のみ — 最初に失敗したグループと、そこで拒否した全代替ルール |
|
|
131
|
-
| `requestId` | 呼び出し元が送った `x-request-id
|
|
132
|
+
| `requestId` | 呼び出し元が送った `x-request-id`(サーバーが受け入れる形のもの。[失敗イベント](#失敗イベント) を参照)。無い場合は省略される |
|
|
132
133
|
| `durationMs` | Collector パイプラインと evaluator に費やした時間。HTTP の往復時間ではない |
|
|
133
134
|
|
|
134
135
|
判定 1 件につき 1 行なので、N 件の `POST /verify/batch` は同一 `requestId` を持つ N 行を出力します。アラートとインデックスはイベント名に対して張り、メッセージ本文には張らないでください。
|
|
@@ -141,6 +142,37 @@ Authorization: Bearer <jwt>
|
|
|
141
142
|
- **呼び出し元の `context` オブジェクト。** 自由形式で collector にそのまま渡されるため、呼び出し側サービスのリクエストペイロードが行き着く場所そのものです。これを記録すると監査ストリームがそのペイロードの複製になります。
|
|
142
143
|
- **ルールの `message` 文字列。** 既に行に載っている resource と action から導かれる内容です。
|
|
143
144
|
|
|
145
|
+
### 失敗イベント
|
|
146
|
+
|
|
147
|
+
判定できなかった decision は `error`(level 50)でログに出て、その行はどの種類の失敗がどこで起きたかを示します。
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{"level":50,"msg":"verify_internal_error","endpoint":"/verify","requestId":"6f1c…","category":"collector_threw","collector":"attribute.collectors[1] (EntitlementStoreCollector)","err":{"type":"Error","message":"entitlement store is down","stack":"…"}}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
| イベント | 応答 | `category` | 併せて示すもの |
|
|
154
|
+
|---|---|---|---|
|
|
155
|
+
| `collector_timeout` | `403 collector_timeout` | `collector_timeout` | `collector`: `VERIFY_COLLECTOR_TIMEOUT_MS` を超えたエントリ、または pipeline が `VERIFY_COLLECTOR_DEADLINE_MS` を超えた場合はリストそのもの(`attribute.collectors`) |
|
|
156
|
+
| `rule_timeout` | `403 rule_timeout` | `rule_timeout` | `rule`: `{ ruleType, code }` |
|
|
157
|
+
| `attribute_conflict` | `403 attribute_conflict` | `attribute_conflict` | —(2 つのコレクターが食い違った。どちらか一方だけの責任ではない) |
|
|
158
|
+
| `verify_internal_error` | `500 internal_error` | `collector_threw`、`rule_threw`、`body_rejected`、`internal` のいずれか | `collector_threw` なら `collector`、`rule_threw` なら `rule` |
|
|
159
|
+
|
|
160
|
+
| `category` | 何が失敗したか |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `collector_timeout` | コレクターが予算を、または pipeline がデッドラインを超えた |
|
|
163
|
+
| `collector_threw` | コレクターが reject / throw した |
|
|
164
|
+
| `attribute_conflict` | 2 つの attribute コレクターが同じスカラーキーに異なる値を書いた |
|
|
165
|
+
| `rule_timeout` | 非同期ルールが予算を、またはルールフェーズがデッドラインを超えた |
|
|
166
|
+
| `rule_threw` | ルールの `verify` が throw した、または `decide` が reject した |
|
|
167
|
+
| `body_rejected` | JSON body parser が、deny エンベロープが 4xx に対応付けていない形で失敗した |
|
|
168
|
+
| `internal` | decision 自身の collect や評価から出てきたのではないもの — throw した resource parser や authenticator(何を throw したかによらず) |
|
|
169
|
+
|
|
170
|
+
この集合は閉じています: アラートとフィルタは `err.message` ではなく `category` の等値に対して張ってください。`collector` は `config/application.conf` の `attribute.collectors` / `rule.collectors` 内のエントリ位置とクラス名です。JWKS に到達できないことは category ではありません — `401` で応答され、`jwt_verification_unavailable` としてログに出ます(後述)。
|
|
171
|
+
|
|
172
|
+
サーバーがこれらの行に加えるものは、トークン・クレーム・`context` のいずれも含みません。collector はその decision についてコレクターのランナーが記録したもので、エラーから読んだ名前ではなく、何も記録されていないタイムアウトは `collector: "unattributed"` です。ルールの `ruleType` と `code` はルールコレクターがリクエストごとにルールを組み立てうるため、識別子の形(英字で始まり、英数字・`_`・`.`・`-` が続く 64 文字以内)の場合だけ載り、それ以外は `redacted` になります。リクエスト ID は検証済みです。`err` は throw されたエラーそのものですが、例外が 1 つあります: core が定義する 3 つの deny エラー(`CollectorTimeoutError`、`RuleTimeoutError`、`AttributeConflictError`)はメッセージと自身のフィールドにコレクター・ルール・属性キーを含むため、分類されたコレクターやルール、同じ識別子の形に制限した属性キー、ヘッダ行だけの stack で組み立て直したものを記録します。それ以外のエラーのメッセージは、その作者の責任です。
|
|
173
|
+
|
|
174
|
+
**リクエストの相関.** 呼び出し元が送った `x-request-id` は、`/verify` と `/verify/batch` が返すすべての応答 — allow、deny、拒否、`500` — にレスポンスヘッダとしてそのまま返され、これらの行と `decision` 行に `requestId` として載ります。これで deny を enforcement 側サービス自身のログと突き合わせられます。載せるのは `A-Z a-z 0-9 - _ . : + / = #` からなる 1〜128 文字の場合だけで、UUID、ULID、16 進や W3C のトレース ID、base64、[protobuf.interceptors](https://github.com/o3co/protobuf.interceptors) が採番する ID はすべて収まります。それ以外は送られなかったものとして扱い — 返さず、ログに出さず、コレクターにも渡しません — 呼び出し元が送らなかった場合にサーバーが採番することもありません。`HTTP_CALLER_AUTH_TOKEN` のゲートは decision エンドポイントより手前で応答するため、ID を返しません。
|
|
175
|
+
|
|
144
176
|
### メトリクス
|
|
145
177
|
|
|
146
178
|
`GET /metrics` は Prometheus text exposition format を返します。
|
|
@@ -150,6 +182,7 @@ Authorization: Bearer <jwt>
|
|
|
150
182
|
| `auth_decisions_total` | counter | `decision` | allow/deny 比率。系列はちょうど 2 本 |
|
|
151
183
|
| `auth_denials_total` | counter | `code` | どのルールが deny しているか — ログ行の `deniedBy` の集計版 |
|
|
152
184
|
| `auth_decision_duration_seconds` | histogram | `decision` | パイプラインと evaluator に費やした時間 |
|
|
185
|
+
| `auth_collector_failures_total` | counter | `collector`, `category` | どのファクトソースがどう判定を失敗させているか — 失敗イベント行の `collector` フィールドの集計版。`category` は `collector_timeout` か `collector_threw` |
|
|
153
186
|
| `http_request_duration_seconds` | histogram | `method`, `route`, `status` | リクエストレート・エラーレート・レイテンシ(RED メソッド) |
|
|
154
187
|
| `auth_policy_verifier_*` | 各種 | — | Node プロセス既定メトリクス — event loop lag, heap, GC, handles |
|
|
155
188
|
|
|
@@ -165,6 +198,7 @@ deny はエラーではないので、書く価値のあるアラートは deny
|
|
|
165
198
|
- **`route`** は URL ではなく Express の route *パターン*で、マッチしなかったもの(ポートに到達できる何かからの 404 プローブ)はすべて `route="unmatched"` に潰れます。
|
|
166
199
|
- **`method`** はこのサービスが実際に処理できる 9 メソッドの allowlist で、それ以外は `method="other"` です。Node のパーサは llhttp が知る全メソッド(`PURGE`, `MKCOL`, `PROPFIND` など)をサーバーに渡すため、`req.method` はパスと同程度に呼び出し元の制御下にあります。
|
|
167
200
|
- **`code`** は deployment が設定したルールに由来するので運用者が有界にできます — ただし `code` は `Rule` インターフェースのフィールドであり、ルールはリクエストごとに構築されるため、カスタム rule collector が resource から code を導出するようになるまでは 1 回の編集です。異なる値 32 個で打ち止め、それ以降は `code="other"` に潰れます。`code="other"` が伸びていること自体が、リクエストごとに code を作っているルールがあるというシグナルです。
|
|
201
|
+
- **`collector`** は設定されたエントリ — コレクターのランナーが記録した `attribute.collectors` / `rule.collectors` 内の位置と識別子の形のクラス名 — または `collector="unattributed"` なので、設定によって有界です。コレクターが自分で作った `CollectorTimeoutError` で別の名前を名乗ることはできません。それでも念のため異なる値 32 個で打ち止め、それ以降は `collector="other"` に潰れます。**`category`** は閉じた列挙値です。
|
|
168
202
|
|
|
169
203
|
#### `/metrics` への到達方法
|
|
170
204
|
|
|
@@ -50,6 +50,7 @@ Individual values can also be overridden with environment variables.
|
|
|
50
50
|
| `RULE_ON_EMPTY_RULE_SET` | `deny` | Decision when no rule is collected (`deny` \| `allow`) |
|
|
51
51
|
| `VERIFY_MAX_BATCH_SIZE` | `50` | Cap on `POST /verify/batch` entries |
|
|
52
52
|
| `VERIFY_CREDENTIAL_TO_COLLECTORS` | `never` | `expose` hands collectors the raw credential as `context.credential` — only for a collector that calls a downstream API as the subject. The default keeps collectors on verified claims; the credential is replayable and a logged context would leak it |
|
|
53
|
+
| `VERIFY_EVALUATION_IN_RESPONSE` | `omit` | `include` puts each policy-backed rule's `evaluation` — its status and the policy revision it evaluated (#244) — on the decision response, for a calling service that records which revision authorized an operation. The `decision` log event carries it either way. It tells any holder of an accepted token when the policy set changed, so pair it with `HTTP_CALLER_AUTH_TOKEN` where that matters |
|
|
53
54
|
| `LOG_LEVEL` | `info` | Minimum level emitted: `trace`\|`debug`\|`info`\|`warn`\|`error`\|`fatal`\|`silent`. Also the switch for the decision log — see [Observability](#observability) |
|
|
54
55
|
|
|
55
56
|
## HS256 secret strength
|
|
@@ -128,7 +129,7 @@ Every decision emits one structured event named `decision`, at `info`:
|
|
|
128
129
|
| `code` | Deny only — the same code the caller got on the wire |
|
|
129
130
|
| `satisfiedBy` | Allow only — the `{ruleType, code}` of the rule that satisfied each group |
|
|
130
131
|
| `deniedBy` | Deny only — the first failing group, and every alternative in it that refused |
|
|
131
|
-
| `requestId` | `x-request-id` as sent, when the caller sent one. Omitted otherwise |
|
|
132
|
+
| `requestId` | `x-request-id` as sent, when the caller sent one the server carries (see [Failure events](#failure-events)). Omitted otherwise |
|
|
132
133
|
| `durationMs` | Time in the collector pipelines and the evaluator — not the HTTP round trip |
|
|
133
134
|
|
|
134
135
|
One line per decision, so a `POST /verify/batch` of N entries emits N lines sharing one `requestId`. Alert and index on the event name, never on message text.
|
|
@@ -141,6 +142,37 @@ One line per decision, so a `POST /verify/batch` of N entries emits N lines shar
|
|
|
141
142
|
- **the caller's `context` object.** It is free-form and forwarded verbatim to collectors, so it is exactly where a calling service's own request payload ends up. Logging it would make the audit stream a copy of that payload.
|
|
142
143
|
- **rule `message` text**, which is derived from the resource and action already on the line.
|
|
143
144
|
|
|
145
|
+
### Failure events
|
|
146
|
+
|
|
147
|
+
A decision that could not be made is logged at `error` (level 50), and the line says what kind of failure it was and where:
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{"level":50,"msg":"verify_internal_error","endpoint":"/verify","requestId":"6f1c…","category":"collector_threw","collector":"attribute.collectors[1] (EntitlementStoreCollector)","err":{"type":"Error","message":"entitlement store is down","stack":"…"}}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
| Event | Answered | `category` | Also names |
|
|
154
|
+
|---|---|---|---|
|
|
155
|
+
| `collector_timeout` | `403 collector_timeout` | `collector_timeout` | `collector`: the entry that overran `VERIFY_COLLECTOR_TIMEOUT_MS`, or the list itself (`attribute.collectors`) when the pipeline overran `VERIFY_COLLECTOR_DEADLINE_MS` |
|
|
156
|
+
| `rule_timeout` | `403 rule_timeout` | `rule_timeout` | `rule`: `{ ruleType, code }` |
|
|
157
|
+
| `attribute_conflict` | `403 attribute_conflict` | `attribute_conflict` | — (two collectors disagreed; neither alone is answerable) |
|
|
158
|
+
| `verify_internal_error` | `500 internal_error` | `collector_threw`, `rule_threw`, `body_rejected` or `internal` | `collector` for `collector_threw`, `rule` for `rule_threw` |
|
|
159
|
+
|
|
160
|
+
| `category` | What failed |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `collector_timeout` | A collector overran its budget, or a pipeline its deadline |
|
|
163
|
+
| `collector_threw` | A collector rejected or threw |
|
|
164
|
+
| `attribute_conflict` | Two attribute collectors wrote different values to one scalar key |
|
|
165
|
+
| `rule_timeout` | An asynchronous rule overran its budget, or the rule phase its deadline |
|
|
166
|
+
| `rule_threw` | A rule's `verify` threw or its `decide` rejected |
|
|
167
|
+
| `body_rejected` | The JSON body parser failed in a way the deny envelope does not map to a 4xx |
|
|
168
|
+
| `internal` | Anything that did not come out of a decision's own collect or evaluation — a resource parser or authenticator that threw, whatever it threw |
|
|
169
|
+
|
|
170
|
+
The set is closed: alert and filter on `category` by equality, never on `err.message`. `collector` is the entry's position in `attribute.collectors` / `rule.collectors` in `config/application.conf`, plus its class. An unreachable JWKS is not a category — it is answered `401` and logged as `jwt_verification_unavailable` (see below).
|
|
171
|
+
|
|
172
|
+
Nothing the server adds to these lines carries the token, the claims or `context`. The collector is the one the collector runner recorded for that decision, never a name read off an error, and a timeout nothing recorded is `collector: "unattributed"`. A rule's `ruleType` and `code` are carried only when identifier-shaped (a letter, then letters, digits, `_`, `.` or `-`, at most 64 characters) and are `redacted` otherwise, because a rule collector may build rules per request. The request id is validated. `err` is the error as thrown, with one exception: the three deny errors core defines (`CollectorTimeoutError`, `RuleTimeoutError`, `AttributeConflictError`) name a collector, a rule or an attribute key in their message and their own fields, so each is logged rebuilt — the classified collector or rule, the key held to the same identifier shape, and a stack of the header line only. Any other error's message is its author's responsibility.
|
|
173
|
+
|
|
174
|
+
**Request correlation.** An `x-request-id` the caller sent is echoed as a response header on every answer `/verify` and `/verify/batch` give — allow, deny, refusal and `500` — and carried as `requestId` on these lines and the `decision` line, so a denial can be matched to the enforcing service's own log. It is carried only as 1–128 characters of `A-Z a-z 0-9 - _ . : + / = #`, which UUIDs, ULIDs, hex and W3C trace ids, base64 and the ids [protobuf.interceptors](https://github.com/o3co/protobuf.interceptors) mints all fit. Anything else is treated as absent — not echoed, not logged, not forwarded to collectors — and the server never mints one when the caller sent none. The `HTTP_CALLER_AUTH_TOKEN` gate answers before the decision endpoints and does not echo it.
|
|
175
|
+
|
|
144
176
|
### Metrics
|
|
145
177
|
|
|
146
178
|
`GET /metrics` serves the Prometheus text exposition format.
|
|
@@ -150,6 +182,7 @@ One line per decision, so a `POST /verify/batch` of N entries emits N lines shar
|
|
|
150
182
|
| `auth_decisions_total` | counter | `decision` | The allow/deny rate. Exactly two series |
|
|
151
183
|
| `auth_denials_total` | counter | `code` | Which rule is doing the denying — the aggregate of the log line's `deniedBy` |
|
|
152
184
|
| `auth_decision_duration_seconds` | histogram | `decision` | Time in the pipelines and the evaluator |
|
|
185
|
+
| `auth_collector_failures_total` | counter | `collector`, `category` | Which fact source is failing decisions, and how — the aggregate of the `collector` field on the failure lines. `category` is `collector_timeout` or `collector_threw` |
|
|
153
186
|
| `http_request_duration_seconds` | histogram | `method`, `route`, `status` | Request rate, error rate and latency (the RED method) |
|
|
154
187
|
| `auth_policy_verifier_*` | various | — | Node process defaults — event-loop lag, heap, GC, handles |
|
|
155
188
|
|
|
@@ -165,6 +198,7 @@ An unbounded label mints a fresh time series per distinct value, which is how a
|
|
|
165
198
|
- **`route`** is the Express route *pattern*, never the URL, and anything unmatched — 404 probes from whatever can route to the port — collapses to `route="unmatched"`.
|
|
166
199
|
- **`method`** is an allowlist of the nine methods this service can serve; everything else is `method="other"`. Node's parser hands the server every method llhttp knows (`PURGE`, `MKCOL`, `PROPFIND`, …), so `req.method` is as caller-controlled as a path.
|
|
167
200
|
- **`code`** comes from the rules a deployment configured, which makes it operator-bounded — but `code` is a field on the `Rule` interface and rules are built per request, so a custom rule collector is one edit away from deriving it from the resource. It is capped at 32 distinct values, after which the rest collapse to `code="other"`. A climbing `code="other"` is itself the signal that a rule is minting codes per request.
|
|
201
|
+
- **`collector`** is a configured entry — its position in `attribute.collectors` / `rule.collectors` and its identifier-shaped class name, as the collector runner recorded it — or `collector="unattributed"`, so it is bounded by the config. A `CollectorTimeoutError` a collector builds itself cannot name anything else. The label is still capped at 32 distinct values as a backstop, collapsing to `collector="other"`. **`category`** is a closed enum.
|
|
168
202
|
|
|
169
203
|
#### Reaching `/metrics`
|
|
170
204
|
|
|
@@ -369,4 +369,15 @@ verify {
|
|
|
369
369
|
# then the greppable record of that decision. Never log the field.
|
|
370
370
|
credentialToCollectors = "never"
|
|
371
371
|
credentialToCollectors = ${?VERIFY_CREDENTIAL_TO_COLLECTORS}
|
|
372
|
+
|
|
373
|
+
# Whether a decision RESPONSE carries what each policy-backed rule reported
|
|
374
|
+
# about the evaluation behind its answer — its status, and the policy revision
|
|
375
|
+
# it evaluated (#244). The `decision` log event always carries it; this is
|
|
376
|
+
# only about the wire. "omit" (the default): the response is key-for-key what
|
|
377
|
+
# it was. Set "include" when the calling service records which policy
|
|
378
|
+
# revision authorized an operation. It tells any holder of an accepted token
|
|
379
|
+
# when the policy set changed and whether a denial was a policy's or the
|
|
380
|
+
# engine failing, so pair it with `http.callerAuth` where that matters.
|
|
381
|
+
evaluationInResponse = "omit"
|
|
382
|
+
evaluationInResponse = ${?VERIFY_EVALUATION_IN_RESPONSE}
|
|
372
383
|
}
|
|
@@ -389,7 +389,7 @@ rule { collectors = [ { collector = ResourceActionScopeRuleCollector } ] }
|
|
|
389
389
|
|
|
390
390
|
const config = loadAppConfig(withoutVerify(), "development");
|
|
391
391
|
|
|
392
|
-
//
|
|
392
|
+
// Every one, spelled out. A knob that reached the shape but not the
|
|
393
393
|
// block's `.default()` literal is `undefined` here, and `toEqual` says
|
|
394
394
|
// which one.
|
|
395
395
|
expect(config.verify).toEqual({
|
|
@@ -406,6 +406,7 @@ rule { collectors = [ { collector = ResourceActionScopeRuleCollector } ] }
|
|
|
406
406
|
collectorConcurrency: 8,
|
|
407
407
|
batchConcurrency: 8,
|
|
408
408
|
credentialToCollectors: "never",
|
|
409
|
+
evaluationInResponse: "omit",
|
|
409
410
|
});
|
|
410
411
|
});
|
|
411
412
|
|
package/templates/versions.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"@o3co/auth.policy-verifier.core": "0.
|
|
3
|
-
"@o3co/auth.policy-verifier.builtins": "0.
|
|
4
|
-
"@o3co/auth.policy-verifier.server": "0.
|
|
2
|
+
"@o3co/auth.policy-verifier.core": "0.12.0",
|
|
3
|
+
"@o3co/auth.policy-verifier.builtins": "0.12.0",
|
|
4
|
+
"@o3co/auth.policy-verifier.server": "0.12.0"
|
|
5
5
|
}
|