ADORSYS-GIS / ADORSYS-GIS/lightbridge-authz

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

Aberta
#177 2 comentários 0 reações 0 responsáveis Ver no GitHub
ticket
Linguagem predominante
Rust
Estrelas
0
Forks
1
Merge médio
6h 42min
PRs com merge (30d)
246

Descrição

### 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/

Guia de contribuição

Abrir o guia de contribuição

Avaliação

Esta issue ainda não foi avaliada.

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.