ADORSYS-GIS / ADORSYS-GIS/lightbridge-authz

[Ticket]: quota-tier catalogue is never enforced — unknown tiers silently disable the ceiling

Đang mở
#177 2 bình luận 0 reaction 0 người được giao Xem trên GitHub
ticket
Ngôn ngữ chính
Rust
Star
0
Fork
1
Merge trung bình
7 giờ 7 phút
Pull request đã merge (30 ngày)
237

Mô tả

### Type

Bug (silent authorization gap — governance values are unvalidated)

### Summary

We need to **enforce the quota-tier catalogue on the write paths, and decide what that catalogue
is**, because governance tier values are currently accepted unvalidated and the gateway matches them
with `Exact` — so a typo silently disables a ceiling with no error anywhere.

Expected result:

> `Account.defaultQuota`, `Project.projectQuota` and `ProjectMember.quotaTier` are rejected at write
> time unless they are in the configured catalogue, and the catalogue is agreed and configured in
> prod, so a value stored in the DB is guaranteed to match a rendered BackendTrafficPolicy selector.

### Intent

Epic ai-helm#531 shipped two ceilings — a pooled `projects.project_quota` and a per-member
`project_members.quota_tier`. Both reach the gateway as headers (`x-project-quota`, `x-quota-tier`)
and are matched by BackendTrafficPolicy rules with **`Exact`** selectors.

`Exact` means the string in the database must equal the string rendered into the chart, character for
character. Nothing currently guarantees that, in either direction:

* nothing validates what gets written, and
* no catalogue is configured in prod at all.

A lead can set `quota_tier: "medim"` today. It is stored, introspection returns it, Authorino stamps
it, and the BTP rule for `"medium"` never matches — so that member ends up with **no per-member
ceiling** and nothing reports a problem. The failure is silent in the direction that removes a limit,
which is the wrong direction for a governance control.

This is the blocker in front of populating `tiers` / `projectEnvelope`. Turning those rules on before
the vocabulary is enforced makes typos fail open.

### Source of truth

* Epic: ADORSYS-GIS/ai-helm#531
* `docs/governance-model-and-enforcement.md` §5 (this repo)
* ADR-0006 (this repo), ai-helm ADR-0110
* Follow-on from #174 (per-member tier at introspection) and ai-helm#854 / ai-helm-values#166

### Current Behavior

The catalogue **type exists but is never consulted**:

* `crates/lightbridge-authz-core/src/config/mod.rs:204` — `pub struct QuotaTiers`
* `:212` — `pub struct QuotaTier`
* `:217` — `impl QuotaTiers` with `is_allowed`
* `:40` — `pub quota_tiers: QuotaTiers` on `Config`

`grep -rn "QuotaTiers\|quota_tiers" --include="*.rs" crates/ app/` returns **no hits outside
`config/mod.rs`**. `is_allowed` is dead code.

The write path passes the value straight through —
`crates/lightbridge-authz-api-key/src/repo.rs:512` `set_project_member_quota_tier` does
`UPDATE project_members SET quota_tier = $1` with no check, and the handler at
`crates/lightbridge-authz-rest/src/handlers/mod.rs:508` just forwards.

⚠️ The doc comment at `repo.rs:506` asserts the check happens elsewhere:

> "that catalogue check happens where the request is first accepted (the procedure/handler layer that
> holds the loaded `Config`), not in the repository"

**That handler-layer check was never written.** The comment describes intended behaviour as though it
were implemented, which is how it went unnoticed — and it is why
`docs/governance-model-and-enforcement.md` initially repeated the claim.

`environments/prod/values/lightbridge-app.yaml` in ai-helm-values contains no `quota_tiers` block, so
even once wired the catalogue would be empty (which by design accepts anything).

### Expected Behavior

1. `createAccount` / `updateAccount` (`defaultQuota`), `Project.create` / `Project.update`
(`projectQuota`), and `setProjectMemberQuotaTier` (`quotaTier`) reject a value absent from the
catalogue with `400 Bad Request`, naming the value and listing the permitted ones.
2. `NULL` / absent stays valid and keeps meaning "no ceiling of this kind" — this must not become a
required field.
3. An **empty or absent** catalogue keeps accepting anything, so existing deployments do not break on
upgrade (same contract `Billing` already has).
4. The catalogue is agreed and set in prod, and its ids match the `tiers` / `projectEnvelope` ids
rendered by ai-helm.

### Acceptance Criteria

- [ ] `QuotaTiers::is_allowed` is called on all three write paths; no longer dead code.
- [ ] An unknown tier returns `400` with the offending value and the permitted set.
- [ ] `NULL`/absent still accepted on all three.
- [ ] An empty/absent catalogue accepts any value (no behaviour change for current deployments).
- [ ] Catalogue values agreed and configured in `ai-helm-values` prod.
- [ ] Those same ids appear in ai-helm's `tiers` / `projectEnvelope`, so every storable value has a
matching rule.
- [ ] Tests cover: accepted value, rejected value, `NULL`, and empty catalogue.

### Out of Scope

* Populating `tiers` / `projectEnvelope` (ai-helm) — the follow-up this unblocks, tracked separately.
* Making tiers first-class DB entities. ADR-0110's reasoning stands: Envoy Gateway can only enforce a
statically rendered menu, so a freely-creatable table would not remove the chart change.
* `projectEnvelope`'s single-static-rule shape — it matches **one** `projectQuota` value, so several
project ceilings would need it to become an ordered list like `tiers`. Real, but a separate ai-helm
template change.

### Technical Context

The vocabulary spans three places that must agree:

| Where | What | Today |
|---|---|---|
| authz config (`quota_tiers`) | validation | type exists, never called |
| Postgres (`project_quota`, `quota_tier`, `default_quota`) | stored values | any string |
| ai-helm (`tiers[].id`, `projectEnvelope.projectQuota`) | BTP `Exact` selectors | `[]` / `{}` |

`Billing`/`billing_plan` is the pattern to copy — it is catalogue-validated at the handler layer and
degrades the same way when the catalogue is empty. Worth noting `create_api_key` already logs a
warning when a key references a plan absent from the catalogue; the equivalent signal does not exist
for tiers.

### Risks

* **Tightening a live write path.** If any tier values are already stored that are not in the agreed
catalogue, enabling validation makes subsequent writes to those rows fail. Count distinct values
first: `SELECT DISTINCT quota_tier FROM project_members WHERE quota_tier IS NOT NULL;` and the
equivalents for `projects.project_quota` / `accounts.default_quota`.
* **Choosing the catalogue is a product decision, not a technical one** — it is the menu of ceilings
the business intends to sell/allocate. This ticket should not invent it.
* Low blast radius otherwise: validation-only, no schema change, no migration.

### Test Plan

* Unit: `QuotaTiers::is_allowed` — present, absent, empty catalogue, `None`.
* Integration (`it-tests`, real Postgres): `setProjectMemberQuotaTier` with a valid id succeeds; with
an unknown id returns `400`; with `NULL` clears; with an empty catalogue accepts anything.
* Same three cases for `projectQuota` on project create/update and `defaultQuota` on account
create/update.
* Regression: an existing row whose value is outside the catalogue can still be **read** (validation
is write-time only).

### Verification evidence

Findings verified against prod on 2026-07-31, not inferred:

* Live Envoy `config_dump` (`envoy-converse-gateway-core-gateway`): `x-billing-plan` and
`x-account-id` appear **1156** times each; `x-quota-tier`, `x-project-quota`, `x-project-id` appear
**0** times — the tier/envelope rules are genuinely not deployed.
* `charts/ai-models/values.yaml` on ai-helm `main`: `tiers: []`, `projectEnvelope: {}`.
* `grep` for `QuotaTiers|quota_tiers` across `crates/` and `app/`: no hits outside `config/mod.rs`.
* `environments/prod/values/lightbridge-app.yaml`: no quota/tier configuration.

### Human accountable owner

@Koufan-De-King

### AI Usage Declaration

AI-assisted. Claude (Opus 5) found the gap while tracing why `x-project-quota` had no selector in the
live BTPs, verified it against the running cluster and the source, and drafted this ticket. The
catalogue contents are deliberately left to a human — it is a product decision.

> AI may accelerate the work, but it must not launder ignorance into polished artifacts.
> Governance: https://adorsys-gis.github.io/ai-governance/

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Hướng nghiên cứu

Start in crates/lightbridge-authz-core/src/config/mod.rs with QuotaTiers::is_allowed, then trace the create/update handlers and setProjectMemberQuotaTier in crates/lightbridge-authz-rest/src/handlers/mod.rs and crates/lightbridge-authz-api-key/src/repo.rs. Compare the handler-layer Billing/billing_plan validation and run the listed unit and it-tests. Done means all three write paths cover accepted, rejected, NULL, and empty-catalogue cases, with agreed ids configured in ai-helm-values and matching ai-helm selectors.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
helm, postgresql, rust
Lĩnh vực
authorization, backend, databases, devops
Loại issue
Lỗi
Độ khó
5/5
Thời gian dự kiến
Hơn một tuần
Mức độ hoạt động
Sôi nổi
Độ rõ ràng
Khá rõ ràng
Mức phù hợp với người mới
35/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.