# Handoff — STA G5 admin RBAC complete, awaiting push/PR auth

**Data:** 2026-04-27
**Worktree:** `/Users/luizpenha/monetarie/.worktrees/sta-paridade`
**Branch:** `sta/g5-admin-api-rbac` (cut from `origin/main` `03155087`)
**Status:** All 4 implementation tasks complete + plan + handoff. 17 new tests / 0 failures. Ready for push/PR after explicit user auth.

---

## TL;DR

Closes **G5** — the only remaining production blocker for STA cabin: the `:admin_api` Phoenix pipeline `# Future: Add admin authentication here` TODO. Adds `StaConnector.Rbac` context module + `StaConnectorWeb.Plugs.RequirePermission` plug, then wires `plug RequirePermission, ...` declarations across all 10 admin controllers per the feature/action mapping table. Super_admin role bypass keeps the existing homolog admin user functioning unchanged. **STA is now prod-deploy-ready.**

4 commits, ~600 LOC delta. **17 new tests** across 2 new files; combined 301/295 PASS (vs. 284/278 post-Onda 2.D baseline; the 6 `Soap.parse_response` baseline failures are unchanged). Compile preserves 5 baseline warnings exactly. Zero out-of-scope file changes.

---

## What's in this branch

| # | SHA | Subject |
|---|---|---|
| 1 | `39a053e8` | G5 admin RBAC plan (close :admin_api auth TODO) |
| 2 | `2d508bfd` | StaConnector.Rbac context with super_admin bypass + role-fallback + ETS cache (Task 1) |
| 3 | `7da2b0f6` | RequirePermission plug + test bypass config (Task 2) |
| 4 | `5fc78f91` | Wire RequirePermission plug in 10 admin controllers (Task 3) |
| 5 | (this) | EOS handoff |

---

## Acceptance — all satisfied

From plan §Acceptance:

- ✅ G5 closed: `RequirePermission` plug enforces feature+action checks on all `/api/admin/*` endpoints (except `auth_controller` for login/me/logout).
- ✅ Super admin user (role=`super_admin`) bypasses every check → existing homolog admin user functions unchanged.
- ✅ Operator/auditor users get role-fallback enforcement when no group memberships exist; explicit group_features when populated.
- ✅ Touchstones green (test bypass keeps existing controller tests passing without DB user/group rows).
- ✅ No out-of-scope changes (`git diff --diff-filter=A origin/main..HEAD --name-only` empty for non-`sta/`).

---

## Test surface delta

### Touchstone files

| File | Pre-G5 | Post-G5 | Δ |
|---|---|---|---|
| `client_test.exs` | 101 / 6 | **101 / 6** | 0 |
| `operations_test.exs` | 58 / 0 | **58 / 0** | 0 |
| `outbound_file_test.exs` | 7 / 0 | **7 / 0** | 0 |
| `application_test.exs` | 3 / 0 | **3 / 0** | 0 |
| `outbound_test.exs` | 38 / 0 | **38 / 0** | 0 |
| `outbound_files_controller_test.exs` | 7 / 0 | **7 / 0** | 0 |
| **Touchstone subtotal** | **214 / 6** | **214 / 6** | **0** |

### NEW test files (this PR — G5)

| File | Tests | Failures |
|---|---|---|
| `rbac_test.exs` | **9 / 0** | 0 |
| `require_permission_test.exs` | **8 / 0** | 0 |
| **NEW total** | **17 / 0** | 0 |

### All Onda 2 + G5 prior new files

| File | Tests | Failures | Source |
|---|---|---|---|
| `state_code_test.exs` | 14 / 0 | 0 | 2.B |
| `inbound/worker_test.exs` | 3 / 0 | 0 | 2.B |
| `outbound/worker_test.exs` | 8 / 0 | 0 | 2.B + 2.C |
| `sta_query_controller_test.exs` | 16 / 0 | 0 | 2.B |
| `rate_limiter_three_bucket_test.exs` | 14 / 0 | 0 | 2.C |
| `password_manager_test.exs` | 10 / 0 | 0 | 2.D |
| `password_expiry_worker_test.exs` | 5 / 0 | 0 | 2.D |
| `rbac_test.exs` | **9 / 0** | 0 | **G5** |
| `require_permission_test.exs` | **8 / 0** | 0 | **G5** |

### Combined (15 files, single batch)

```bash
mix test [...all 15 files...]
# Result: 301 tests, 6 failures (Soap baseline preserved)
# Time:   ~2.6s
```

### Compile

5 baseline warnings preserved across all 4 task commits.

---

## Manual Verification — N/A

Per plan §Objective: G5 is internal architecture, not BCB-facing wire format. No Manual sections to verify against.

---

## Public API additions (cheat sheet)

### `StaConnector.Rbac`

```elixir
@spec super_admin?(map() | String.t() | nil) :: boolean()
@spec role_grants?(String.t() | nil, atom()) :: boolean()
@spec user_has_permission?(String.t() | nil, String.t(), atom()) :: boolean()
@spec invalidate_cache() :: :ok
```

Decision flow inside `user_has_permission?/3`:

1. ETS cache hit (5min TTL) → return cached value
2. `super_admin_in_db?(user_id)` → `User.role == "super_admin"` OR SUPER_ADMIN group membership
3. `explicit_group_grant?(user_id, feature, action)` → `user_groups → group_features.can_*` column
4. `no_group_memberships?(user_id)` → `role_grants?/2` fallback
5. Default → `false` (fail-closed)

Errors caught + logged + return `false`.

Coarse role mapping (`role_grants?/2`):
- `super_admin` → all actions
- `operator` → `:view, :create, :edit, :export`
- `auditor` → `:view, :export`
- unknown → `false`

### `StaConnectorWeb.Plugs.RequirePermission`

```elixir
@behaviour Plug

# Compile-time validation:
def init([feature: <string>, action: <atom>]) :: %{feature: ..., action: ...}

# Runtime:
def call(conn, %{feature: f, action: a}) :: Plug.Conn.t()
  # 1. bypass_rbac_in_test=true → conn unchanged
  # 2. no current_user → 403 with reason "no_current_user"
  # 3. Rbac.super_admin?(user) → conn unchanged
  # 4. Rbac.user_has_permission?(user.id, f, a) → conn unchanged on true
  # 5. Else 403 with reason "permission_denied"
```

403 body shape: `{"error": "forbidden", "reason": <string>, "feature": <string>, "action": <string>}`. Logged at warning level.

### Wiring matrix (10 admin controllers)

| Controller | Plug declarations |
|---|---|
| `AuthController` | (exempt — login/me/logout) |
| `ConfigController` | `configuration.geral`/:view (show/bcb_credentials/test_bcb_connectivity) + :configure (update/reload/update_bcb_credentials) |
| `FilesController` | `outbound.enviados`/:view (index/show/statistics) + :edit (retry) + :delete (delete) |
| `HealthController` | `monitoring.health`/:view (single action) |
| `LogsController` | `monitoring.alertas`/:view (single action) |
| `MetricsController` | `monitoring.dashboard`/:view (all actions) |
| `OutboundFilesController` | `outbound.pendentes`/:edit (resume_upload — write-class) |
| `PollerController` | `configuration.schedules`/:view (status) + :configure (start/stop/pause/resume/trigger) |
| `RoutesController` | `configuration.endpoints`/:view (index/show) + :create (create) + :edit (update/toggle/test) + :delete (delete) |
| `SimulatorController` | `monitoring.dashboard`/:view (status) + :configure (enable/disable/process/update_config) |
| `StaQueryController` | `protocols.historico`/:view (both actions) |

### Files touched (5 new + 14 modified — all in `sta/*`)

```
sta/backend/lib/sta_connector/application.ex                              (modified, +5)
sta/backend/lib/sta_connector/rbac.ex                                     (NEW, ~245 LOC)
sta/backend/lib/sta_connector_web/controllers/api/admin/auth_controller.ex (modified, fmt only)
sta/backend/lib/sta_connector_web/controllers/api/admin/config_controller.ex
sta/backend/lib/sta_connector_web/controllers/api/admin/files_controller.ex
sta/backend/lib/sta_connector_web/controllers/api/admin/health_controller.ex
sta/backend/lib/sta_connector_web/controllers/api/admin/logs_controller.ex
sta/backend/lib/sta_connector_web/controllers/api/admin/metrics_controller.ex
sta/backend/lib/sta_connector_web/controllers/api/admin/outbound_files_controller.ex
sta/backend/lib/sta_connector_web/controllers/api/admin/poller_controller.ex
sta/backend/lib/sta_connector_web/controllers/api/admin/routes_controller.ex
sta/backend/lib/sta_connector_web/controllers/api/admin/simulator_controller.ex
sta/backend/lib/sta_connector_web/controllers/api/admin/sta_query_controller.ex
sta/backend/lib/sta_connector_web/plugs/require_permission.ex             (NEW, ~95 LOC)
sta/backend/config/test.exs                                                (modified, +5)
sta/backend/test/sta_connector/rbac_test.exs                              (NEW, ~85 LOC)
sta/backend/test/sta_connector_web/plugs/require_permission_test.exs      (NEW, ~110 LOC)
sta/docs/plans/2026-04-27-sta-g5-admin-rbac.md                            (NEW plan)
sta/docs/handoff/2026-04-27-sta-g5-admin-rbac-complete.md                 (NEW handoff — this file)
```

---

## Out of scope (deferred)

1. **Bootstrap user creation in production** — homolog has admin user via existing seeds; production deploy must run `priv/repo/seeds.exs` separately.
2. **Per-endpoint feature granularity** — current mapping is coarse (1-2 features per controller). Granular endpoint-level features deferred.
3. **PIX/SPB/NPC/CLST G5 RBAC** — STA-only PR. Other cabins close G5 separately.
4. **Frontend RBAC UI** (group management, permission editing) — admin frontend is separate codebase; coordinate with frontend team.
5. **Audit log for permission denials** — `Notifications.notify` on 403 deferred. CloudWatch logs cover the immediate operational need.
6. **Cache invalidation on group_feature update** — current 5min TTL acceptable for first iteration; pubsub-based instant invalidation deferred.
7. **Onda 3 leiautes corpus / Onda 4 identifier mapping** — independent.
8. **Real BCB smoke test** — operator post-deploy.

---

## Operator runbook

Post-merge + post-deploy:

1. **Existing homolog admin user**: `admin@monetarie.com.br` with `role: "super_admin"` continues to pass all admin endpoints automatically (super_admin bypass).
2. **Add a new operator user** (when needed):
   ```elixir
   # bin/sta_connector eval ...
   {:ok, user} = StaConnector.Auth.create_user(%{
     email: "operator@monetarie.com.br",
     name: "Operator",
     password: "...",
     role: "operator"  # → role-fallback grants :view/:create/:edit/:export
   })
   ```
   Grants for operator (without group_features populated): `monitoring.dashboard:view`, `outbound.enviados:edit`, `protocols.historico:view`, etc. (anything except `:delete`/`:approve`/`:configure`).
3. **Add granular permissions via `user_groups`**: link the user to a group with explicit `group_features`. Cache invalidation: `StaConnector.Rbac.invalidate_cache()`.
4. **Audit denials**: tail CloudWatch `/ecs/monetarie/sta-api` for `[RequirePermission] 403` warnings.

---

## Resumption checklist

```bash
cd /Users/luizpenha/monetarie/.worktrees/sta-paridade
pwd                                                  # → /Users/luizpenha/monetarie/.worktrees/sta-paridade
git fetch origin --prune
git status                                           # working tree clean

# After this PR merges:
git checkout --detach origin/main
git branch -d sta/g5-admin-api-rbac                  # cleanup local
git log --oneline -1                                 # → merge commit

cd sta/backend
mix compile 2>&1 | grep -c "^[[:space:]]*warning:"   # → 5

# Touchstone batch (15 files):
mix test [touchstones + 9 new test files...]
# Expected: 301 tests, 6 failures (Soap baseline preserved)
```

---

## Decision tree for next session

### Suggested next sub-PR

**Onda 3.A leiautes corpus + XSD validator** — `sta/onda3a-leiautes-corpus-validator`. ~1-2 days / ~400 LOC + 2-3 MB corpus. Independent of all prior work. Destrava ACCS001/ACCS010 builders for Compliance traffic via STA. Per parent rollout plan §Sub-PR 3.A.

Alternatives:
- **Onda 4 identifier mapping in `core/`** — touches `core/`; coordinate with Core team.
- **Hygiene PRs** — `worker_test.exs` for full Inbound.Worker integration; per-endpoint feature granularity refinement.
- **2.C-2 follow-up `protocol_expires_at`** — only worth doing once admin UI consumer exists.
- **Stop, encerrar sessão STA** — STA cabin is now feature-complete + prod-deploy-ready.

---

## References

### This PR
- **Plan:** `sta/docs/plans/2026-04-27-sta-g5-admin-rbac.md`
- **Handoff:** `sta/docs/handoff/2026-04-27-sta-g5-admin-rbac-complete.md`
- **Branch:** `sta/g5-admin-api-rbac`
- **Base SHA:** `03155087`

### Predecessor handoffs (closed gaps)
- `sta/docs/handoff/2026-04-27-sta-onda2d-full-complete.md` — Onda 2.D password mgmt (closed senha lifecycle)
- `sta/docs/handoff/2026-04-27-sta-paridade-EOS-COMPREHENSIVE.md` — comprehensive deploy readiness (cited G5 as last remaining blocker)
- Onda 2.B PR #62 body, 2.C handoff §Out of scope #8, 2.D handoff §Out of scope #4 (each documenting the `:admin_api` TODO)

### Pattern source (Core, read-only reference)
- `core/backend/lib/monetarie_web/plugs/require_permission.ex`
- `core/backend/lib/monetarie/rbac.ex`

### STA RBAC scaffolding (in place pre-G5)
- `sta/backend/lib/sta_connector/rbac/{module,feature,group,group_feature,user_group}.ex` — schemas
- `sta/backend/lib/sta_connector/auth/user.ex` — User schema with role enum
- `sta/backend/priv/repo/seeds/rbac_seed.exs` — 5 modules + 20 features + 3 groups
- `sta/backend/lib/sta_connector_web/controllers/api/admin/auth_controller.ex` — login flow with role in JWT

### All STA PRs to date (cross-reference)
| PR | SHA | Title |
|---|---|---|
| #24 | `301503ec` | Onda 1 wire-format + ICP-Brasil chain |
| #62 | `67924840` | Onda 2.B status & queries (C full+admin) |
| #66 | `a55ad325` | Onda 2.C rate & lifecycle (B trim) |
| #74 | `78b01df7` | Onda 2.C deploy plumbing — CI workflow |
| #79 | `2de94ff0` | Onda 2.D password mgmt (Full) |
| **(this)** | TBD | **G5 admin RBAC enforcement** |

---

## Awaiting auth

Per Hard rule #6, no `git push` or `gh pr create` without explicit user authorization. When ready:

```bash
git push -u origin sta/g5-admin-api-rbac
gh pr create --title "feat(sta): G5 admin API RBAC enforcement" --body "..."
```

---

**End of handoff.** G5 closed; STA prod-deploy-ready; awaiting push + PR auth.
