ADORSYS-GIS / ADORSYS-GIS/lightbridge-authz

[Story]: Phase 6b — grants write the Keycloak x-budget-tier attribute

Open
#196 1 comment 0 reactions 0 assignees View on GitHub
governance user-story
Dominant language
Rust
Stars
0
Forks
1
Avg merge
6h 42m
Merged PRs (30d)
246

Description

### Story Statement

As **a user whose refill has been granted**,
I want **the grant to reach Keycloak as a user attribute so the platform's protocol mapper can turn it into the `x-budget-tier` claim**,
so that **the gateway can eventually enforce my new tier — after my next token refresh, honestly, not before.**

### Real Intent

This is the write-back half of ADR-0008's design: the ledger records the grant, but nothing enforces it at the gateway until the tier reaches a JWT claim. Deliberately **not a live lookup** — ADR-0008 is explicit that the Keycloak introspection metadata step was disabled in production on 2026-07-02 (#533) because a slow dependency in that path turns into fail-open at the ext_authz layer. The attribute write happens once, at grant time, from this service.

Outline only — filed now so the epic's delivery-plan PR breakdown has an issue to reference; not scheduled ahead of #189/#191, and independent of #190's engine choice.

### Background and Context

`docs/runbooks/stuck-augmentation-request.md` §1 already describes debugging this exact write-back — checking whether the ledger grant landed and separately whether the Keycloak attribute landed, calling out "grant present, attribute missing → the write-back failed, that is a real bug" as a distinct case from "grant and attribute both present → tell them to sign out and back in, nothing is broken." That runbook was written before this story exists; this story is what makes it non-fiction.

### Source of truth (links)

- Epic: #188
- Depends on: #189 (the ledger — this story fires on a committed grant)
- ADR-0008 (docs/adr/0008-refills-are-discrete-budget-tiers.md) — the `x-budget-tier` claim design, why it is not a live lookup
- Runbook: docs/runbooks/stuck-augmentation-request.md §1 — the failure modes this story must make observable, not silent

### Acceptance Criteria

#### Functional
- [ ] Given a grant commits to the ledger, when the write-back runs, then the account's Keycloak user attribute is set to the granted tier.
- [ ] Given the attribute is set, when the user's token is next refreshed, then the protocol mapper carries the new tier into the claim (this story does not touch the mapper itself — confirm it already does this per ADR-0008, and adjust only if it does not).

#### Negative / Edge Cases
- [ ] Given the Keycloak write fails (network, auth, target user not found), when it happens, then the failure is **observable** — surfaced distinctly from "grant succeeded, write-back succeeded" so the runbook's diagnostic step (grant present, attribute missing → real bug) has something to actually check against.
- [ ] Given a retry of a failed write-back, when it succeeds, then it does not depend on re-evaluating policy or re-granting — it is purely the attribute catching up to an already-committed grant.

#### Non-Functional
- [ ] The write-back is not in the request/ledger-commit transaction (Keycloak is a separate system) — document the resulting window where a grant is committed but the attribute has not yet landed, and make sure that window is what the runbook's diagnostic queries actually check.
- [ ] `cargo clippy --all-targets --all-features -- -D warnings` clean.

### Out of Scope

- Any live/introspection-based lookup — explicitly rejected by ADR-0008's #533 precedent.
- Phase 6a (the gateway re-key to `x-budget-tier`) — a separate, ai-helm-side, boundary-scheduled ticket (ai-helm#877). This story writes the attribute; 6a is what makes the gateway read it.
- The protocol mapper configuration itself, unless it turns out not to already do what ADR-0008 assumes.

### Dependencies and Blockers

- Depends on #189 (grants must exist to write back).
- Independent of #190/#191 — this can be built as soon as a grant exists, regardless of which policy engine produced it.
- Sequencing note (from #191): until Phase 6a lands at the gateway, this story's write-back has no visible effect for users — that is expected and should be stated, not treated as this story being broken.

### Assumptions

1. Keycloak exposes an admin API this service can call to set a user attribute (kcadm-equivalent, per the runbook's own example command).
2. The write-back is fire-and-retry, not fire-and-forget — a failure must be visible to an operator, not just logged and dropped.

### Implementation Notes

Guidance, not gospel: consider a small outbox/retry table rather than a synchronous call inside the grant path, so a slow or down Keycloak does not make refill requests themselves slow or fail — the grant is the authoritative fact regardless of when the attribute catches up.

### Test Expectations

- A test that a successful write-back is distinguishable from a failed one in whatever the runbook's diagnostic step actually queries.
- A test that a grant commits successfully even if the write-back subsequently fails (the ledger is not blocked on Keycloak's availability).

### Verification evidence

*To be completed on delivery.*

### Human accountable owner

@stephane-segning — filed at outline depth on the epic owner's behalf per the #188 backlog comment (2026-08-01); needs their confirmation before entering sprint planning.

### AI Usage Declaration

Drafting this story

### Human verification completed

- [ ] I checked the story against the source of truth
- [ ] I confirmed the acceptance criteria
- [ ] I am the accountable owner and accept responsibility for this story.

> Filed by an AI planning session as part of decomposing epic #188 (see the delivery plan discussion on that issue). None of the boxes above are ticked because the session that filed this is not the accountable owner — @stephane-segning, please review and tick what you've verified.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.