# G5 — Admin API RBAC enforcement (hygiene PR)

> **For Claude:** REQUIRED SUB-SKILL: Use `superpowers:executing-plans` to implement this plan task-by-task. **NO Manual Verification gate** — RBAC is internal architecture, not BCB-facing wire format.

**Branch:** `sta/g5-admin-api-rbac` (cut from `origin/main` `03155087`)
**PR target:** `main`
**Scope:** **(A) Big-bang full** — `RequirePermission` plug + `StaConnector.Rbac` context + wire in **all 10 admin controllers** + super_admin role bypass.
**Estimated effort:** 1-1.5 days · LOC delta estimate: ~700 (libs + tests + 10-controller wires).
**Predecessor handoff:** `sta/docs/handoff/2026-04-27-sta-onda2d-full-complete.md`.

---

## Index

1. [Objective](#objective)
2. [Architecture](#architecture)
3. [Pre-flight](#pre-flight)
4. [Tasks](#tasks)
5. [Acceptance](#acceptance)
6. [Out of scope](#out-of-scope)
7. [References](#references)

---

<a id="objective"></a>
## 1. Objective

Closes the gap noted in Onda 2.B PR #62 body, Onda 2.C handoff §Out of scope #8, and Onda 2.D handoff §Out of scope #4: the `:admin_api` Phoenix pipeline currently has a `# Future: Add admin authentication here` TODO. Today admin endpoints inherit `SsoAuth` (validates Monetarie SSO JWT) but **no permission check** — any SSO-authenticated user passes through to all `/api/admin/*` endpoints.

This PR adds per-endpoint permission enforcement following the same pattern as `MonetarieWeb.Plugs.RequirePermission` (Core), with super_admin role bypass and role-fallback for operator/auditor.

**G5 fully closed when this PR merges.** STA cabin becomes prod-deploy-ready.

---

<a id="architecture"></a>
## 2. Architecture

### Component 1: `StaConnector.Rbac` context module

**Path:** `lib/sta_connector/rbac.ex` (NEW)

**Public API:**

```elixir
@spec user_has_permission?(user_id :: String.t(), feature_code :: String.t(), action :: atom()) :: boolean()
@spec super_admin?(user :: map() | String.t()) :: boolean()
@spec role_grants?(role :: String.t() | nil, action :: atom()) :: boolean()
```

**Decision flow inside `user_has_permission?/3`:**

1. **Super admin bypass** — if user `:role == "super_admin"` OR user is in `SUPER_ADMIN` group → `true`. Skip DB.
2. **Role fallback** — if user has NO group memberships (`user_groups` empty) but has `:role` set, fall back to `role_grants?/2`:
   - `role == "operator"` → `:view`, `:create`, `:edit`, `:export` granted; `:delete`, `:approve`, `:configure` denied.
   - `role == "auditor"` → `:view`, `:export` granted; everything else denied.
3. **Group-based check** — query `user_groups → groups → group_features` for the requested feature_code + action. ETS-cached per `{user_id, feature_code, action}` with 5min TTL.
4. **Default:** `false` (fail-closed).

**ETS cache:** new table `:sta_rbac_permission_cache` created at GenServer init; entries expire by absolute timestamp; 5min TTL match Core.

**Pure functions exposed for testing:**
- `super_admin?/1` (taking either user struct or role string)
- `role_grants?/2` — pure dispatcher returning boolean

### Component 2: `StaConnectorWeb.Plugs.RequirePermission`

**Path:** `lib/sta_connector_web/plugs/require_permission.ex` (NEW)

**Init opts (compile-time):**
- `:feature` — string like `"outbound.enviados"` (required)
- `:action` — atom from `{:view, :create, :edit, :delete, :approve, :configure, :export}` (required)

**Call:**

```elixir
@impl Plug
def call(conn, %{feature: feature, action: action}) do
  case conn.assigns[:current_user] do
    nil ->
      deny(conn, "no_current_user")

    %{} = user ->
      cond do
        Rbac.super_admin?(user) ->
          conn

        user[:id] && Rbac.user_has_permission?(user[:id], feature, action) ->
          conn

        true ->
          deny(conn, "permission_denied", feature: feature, action: action)
      end
  end
end
```

**Failure mode:** **always 403 Forbidden** with JSON body `{"error": "forbidden", "feature": ..., "action": ...}`. Logged at warning-level so operators can spot misconfiguration in CloudWatch.

**Bypass during tests:** Application env `:bypass_rbac_in_test` defaults to `false`. Set to `true` only in `test.exs` for tests that don't want to set up DB user/group rows. Existing controller tests (touchstones) will use this bypass to avoid breaking.

### Component 3: Wire in admin controllers

Per the mapping decided pre-flight:

| Controller | Plug declaration |
|---|---|
| `AuthController` | (none — login/me/logout exempt; required for bootstrap) |
| `ConfigController` | `plug RequirePermission, feature: "configuration.geral", action: :view when action in [:show, :bcb_credentials, :test_bcb_connectivity]` + `:configure` for write actions |
| `FilesController` | `:view` for index/show/statistics; `:edit` for retry/delete |
| `HealthController` | `:view, feature: "monitoring.health"` |
| `LogsController` | `:view, feature: "monitoring.alertas"` |
| `MetricsController` | `:view, feature: "monitoring.dashboard"` |
| `OutboundFilesController` | `:edit, feature: "outbound.pendentes"` (resume_upload is write-class) |
| `PollerController` | `:view` for status; `:configure` for start/stop/pause/resume/trigger |
| `RoutesController` | `:view` for index/show; `:create` for create; `:edit` for update/toggle/test; `:delete` for delete |
| `SimulatorController` | `:view` for status; `:configure` for enable/disable/process/update_config |
| `StaQueryController` | `:view, feature: "protocols.historico"` for both actions |

Each controller gets its `plug RequirePermission` declarations near the top, with `when action in [...]` guards for action-specific restrictions where needed.

### Files to create

| File | Purpose | Est. LOC |
|---|---|---|
| `lib/sta_connector/rbac.ex` | Context module + ETS cache | ~180 |
| `lib/sta_connector_web/plugs/require_permission.ex` | Plug | ~120 |
| `test/sta_connector/rbac_test.exs` | Pure helper tests + ETS interaction | ~80 |
| `test/sta_connector_web/plugs/require_permission_test.exs` | Plug Plug.Test tests | ~100 |

### Files to modify

| File | Modification | Est. LOC delta |
|---|---|---|
| `lib/sta_connector/application.ex` | Start `Rbac.Cache` GenServer (ETS owner) | +5 |
| `lib/sta_connector_web/controllers/api/admin/*.ex` (10 files) | Add plug declarations | +30 (~3 LOC each) |
| `config/test.exs` | `config :sta_connector, :bypass_rbac_in_test, true` | +1 |

**Total estimated:** ~510 LOC delta (~50% tests).

---

<a id="pre-flight"></a>
## 3. Pre-flight

**Already verified at session start:**
- ✅ Branch `sta/g5-admin-api-rbac` cut off `origin/main` `03155087`
- ✅ RBAC schemas: `StaConnector.Rbac.{Module, Feature, Group, GroupFeature, UserGroup}` all in place
- ✅ `StaConnector.Auth.User.role` field with `super_admin | operator | auditor` enum
- ✅ Seeds populated: 5 modules + 20 features + 3 groups (SUPER_ADMIN / OPERADOR_STA / AUDITOR)
- ✅ AuthController login response includes `role` + permissions in JWT (assigns become `current_user.role`)
- ✅ `SsoAuth` plug populates `current_user` with `:role` from JWT claims
- ❌ `StaConnector.Rbac` context module — does NOT exist (only schemas)
- ❌ `RequirePermission` plug — does NOT exist
- ❌ Zero plug declarations in 10 admin controllers
- ❌ ETS cache table — does NOT exist (Rbac.Cache GenServer needed)

---

<a id="tasks"></a>
## 4. Tasks

### Task 1 — `StaConnector.Rbac` context

**TDD red:** Create `test/sta_connector/rbac_test.exs`:

```elixir
describe "super_admin?/1 (pure)" do
  test "user with role super_admin → true"
  test "user with role operator → false"
  test "user with role auditor → false"
  test "string \"super_admin\" → true"
  test "nil → false"
end

describe "role_grants?/2 (pure)" do
  test "operator role → true for :view/:create/:edit/:export, false for :delete/:approve/:configure"
  test "auditor role → true for :view/:export, false for everything else"
  test "super_admin → true for ALL actions"
  test "nil/unknown role → false"
end

describe "user_has_permission?/3 (DB-backed)" do
  test "super_admin user passes via role bypass without hitting DB"
  test "user with no groups + role=operator + action=:view → true (role-fallback)"
  test "user with no groups + role=operator + action=:delete → false"
  test "user with explicit group_feature.can_view=true → true"
  # ... ETS cache hit/miss exercised
end
```

**TDD green:** Create `lib/sta_connector/rbac.ex` per Architecture §Component 1.

**Commit:** `feat(sta): StaConnector.Rbac context with super_admin bypass + role-fallback + ETS cache (G5 Task 1)`

**Estimate:** 2-3h.

---

### Task 2 — `RequirePermission` plug

**TDD red:** Create `test/sta_connector_web/plugs/require_permission_test.exs`:

```elixir
describe "call/2" do
  test "no current_user → 403"
  test "current_user with role super_admin → passes through"
  test "current_user with role operator + feature/action permitted → passes"
  test "current_user with role operator + delete action → 403"
  test "bypass_rbac_in_test=true → always passes (test mode)"
end
```

**TDD green:** Create `lib/sta_connector_web/plugs/require_permission.ex`. Add `config/test.exs` entry for `:bypass_rbac_in_test, true`.

**Commit:** `feat(sta): RequirePermission plug + test bypass config (G5 Task 2)`

**Estimate:** 2h.

---

### Task 3 — Wire plug in admin controllers

**TDD red:** Existing controller tests already cover happy paths for super_admin user; the test bypass keeps them green. NO new tests in this task (intentional — wires only).

**TDD green:** Add `plug RequirePermission, feature: ..., action: ... when action in [...]` declarations per the mapping table in Architecture §Component 3 to each of 10 controllers.

**Touchstone post-step:** all 14 touchstone test files green; super_admin path through every controller still works (validated via existing tests with bypass).

**Commit:** `feat(sta): wire RequirePermission plug in 10 admin controllers (G5 Task 3)`

**Estimate:** 2-3h.

---

### Task 4 — Touchstones + handoff

**Touchstone batch (15 files post-Task 3):**

```bash
mix test \
  test/sta_connector/sta/client_test.exs \
  test/sta_connector/sta/operations_test.exs \
  test/sta_connector/files/outbound_file_test.exs \
  test/sta_connector/application_test.exs \
  test/sta_connector/outbound_test.exs \
  test/sta_connector_web/controllers/api/admin/outbound_files_controller_test.exs \
  test/sta_connector/sta/state_code_test.exs \
  test/sta_connector/inbound/worker_test.exs \
  test/sta_connector/outbound/worker_test.exs \
  test/sta_connector_web/controllers/api/admin/sta_query_controller_test.exs \
  test/sta_connector/sta/rate_limiter_three_bucket_test.exs \
  test/sta_connector/sta/password_manager_test.exs \
  test/sta_connector/workers/password_expiry_worker_test.exs \
  test/sta_connector/rbac_test.exs \
  test/sta_connector_web/plugs/require_permission_test.exs
```

Expected: ~310 tests, 6 baseline failures preserved (~26 new vs 2.D end-state).

Compile preserves 5 baseline warnings.

**Handoff:** `sta/docs/handoff/2026-04-27-sta-g5-admin-rbac-complete.md`.

**Commit:** `docs(sta): G5 admin RBAC end-of-session handoff`

**Estimate:** 1h.

---

<a id="acceptance"></a>
## 5. Acceptance

- ✅ G5 closed: `RequirePermission` plug enforces feature+action checks on all `/api/admin/*` endpoints (except auth/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 base..HEAD --name-only | grep -v "^sta/"` empty).

---

<a id="out-of-scope"></a>
## 6. Out of scope

1. **Bootstrap user creation seed** — homolog already has admin user via existing seeds. Production deploy must run user seed separately.
2. **Per-endpoint feature granularity** — current mapping is coarse (1-2 features per controller). Granular endpoint-level features deferred to a later hygiene PR if operators ask.
3. **PIX/SPB/NPC/CLST 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 is deferred. CloudWatch logs cover the immediate operational need.
6. **Cache invalidation on group_feature update** — current 5min TTL is 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-merge.

---

<a id="references"></a>
## 7. References

### Plan docs
- **Predecessor handoff:** `sta/docs/handoff/2026-04-27-sta-onda2d-full-complete.md`
- **Comprehensive EOS handoff:** `sta/docs/handoff/2026-04-27-sta-paridade-EOS-COMPREHENSIVE.md` §12 (G5 row)
- **Onda 2.B/2.C/2.D plans:** same dir.

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

### STA RBAC scaffolding (already in place)
- Schemas: `sta/backend/lib/sta_connector/rbac/{module,feature,group,group_feature,user_group}.ex`
- User schema: `sta/backend/lib/sta_connector/auth/user.ex`
- Seeds: `sta/backend/priv/repo/seeds/rbac_seed.exs` (317 LOC, 5 modules + 20 features + 3 groups + group_features)
- AuthController login flow: `sta/backend/lib/sta_connector_web/controllers/api/admin/auth_controller.ex:109` (load_super_admin_permissions)
- SsoAuth: `sta/backend/lib/sta_connector_web/plugs/sso_auth.ex`

### Onda PRs (cross-reference)
| PR | Closes | Title |
|---|---|---|
| #62 | partial G5 (TODO documented) | Onda 2.B status & queries |
| #66 | — | Onda 2.C rate & lifecycle |
| #74 | deploy plumbing | CI workflow |
| #79 | — | Onda 2.D password mgmt |
| **(this)** | **G5 fully** | Admin API RBAC enforcement |

---

**End of plan.** Implementation begins at Task 1.
