# Access Control API — Requirements Traceability Matrix

Each spec requirement from `api-spec.md` is mapped to its implementation file, method/class, and the test that proves it works.

All endpoints live under `/api/v1/access-control`. The spec originally used `/data-consumer/requests/timeseries`, `/data-consumer/requests/by-consumer`, and `/policy/violations/*`; these were consolidated under `/access-control` to avoid route collisions with fides OSS and improve RESTful grouping.

**Implementation base path:** `src/fidesplus/`
**Service test file:** `tests/ops/service/pbac/violations/test_service.py`
**Route test file:** `tests/ops/api/test_access_control_routes.py`

---

## EP-1: `GET /access-control/summary`

Headline stats for the access control dashboard.


| ID       | Requirement                                                              | Implementation                                                                                  | Test                                                        |
| -------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| EP1-URL  | `GET /access-control/summary`                                            | `api/routes/pbac/access_control.py` → `get_access_control_summary()`                            | `test_access_control_routes.py::test_summary_returns_200`   |
| EP1-P1   | Param: `start_date` (datetime)                                           | Route: `Query(...)` param                                                                       | `test_access_control_routes.py::test_summary_returns_200`   |
| EP1-P2   | Param: `end_date` (datetime)                                             | Route: `Query(...)` param                                                                       | `test_access_control_routes.py::test_summary_returns_200`   |
| EP1-R1   | Response field: `violations` (int)                                       | `ViolationService.get_access_control_summary()` → count from `_repo._get_violations_in_range()` | `test_service.py::test_summary_violations_count`            |
| EP1-R2   | Response field: `total_requests` (int)                                   | `ViolationService.get_access_control_summary()` → `_access_log_repo.count_in_range()`           | `test_service.py::test_summary_total_requests`              |
| EP1-R3   | Response field: `trend` (float, nullable) — % change vs preceding period | `ViolationService.get_access_control_summary()` → `(current - prev) / prev`                     | `test_service.py::test_summary_trend_calculation`           |
| EP1-R4   | `trend` = None when preceding period has 0 violations                    | Same method, `if prev == 0: trend = None`                                                       | `test_service.py::test_summary_trend_none_when_no_previous` |
| EP1-R5   | Response field: `active_consumers` (int)                                 | Distinct `consumer_id` from violations in range                                                 | `test_service.py::test_summary_active_consumers`            |
| EP1-R6   | Response field: `total_policies` (int)                                   | `len(_purpose_repo.get_all())`                                                                  | `test_service.py::test_summary_total_policies`              |
| EP1-S1   | Schema: `AccessControlSummaryResponse`                                   | `api/schemas/violation.py`                                                                      | Type-checked by mypy                                        |
| EP1-AUTH | Requires `POLICY_VIOLATION_READ` scope                                   | Route `dependencies=[Security(...)]`                                                            | Auth enforced by middleware                                 |


---

## EP-2: `GET /access-control/requests`

Timeseries of requests and violations. Bucket interval auto-derived from range (hourly for <=48h, daily otherwise).


| ID      | Requirement                                                  | Implementation                                                    | Test                                                       |
| ------- | ------------------------------------------------------------ | ----------------------------------------------------------------- | ---------------------------------------------------------- |
| EP2-URL | `GET /access-control/requests`                               | `api/routes/pbac/access_control.py` → `get_requests_timeseries()` | `test_access_control_routes.py::test_requests_returns_200` |
| EP2-P1  | Param: `start_date` (datetime)                               | Route: `Query(...)`                                               | Tested in route test                                       |
| EP2-P2  | Param: `end_date` (datetime)                                 | Route: `Query(...)`                                               | Tested in route test                                       |
| EP2-P3  | Param: `consumer[]` (list, optional)                         | Route: `Query(None)`                                              | `test_service.py::test_timeseries_filter_by_consumer`      |
| EP2-P4  | Param: `policy[]` (list, optional)                           | Route: `Query(None)`                                              | Tested via `_apply_filters`                                |
| EP2-P5  | Param: `dataset[]` (list, optional)                          | Route: `Query(None)`                                              | `test_service.py::test_timeseries_filter_by_dataset`       |
| EP2-P6  | Param: `data_use[]` (list, optional)                         | Route: `Query(None)`                                              | Tested via `_apply_filters`                                |
| EP2-R1  | Response: `{ items: [{ timestamp, requests, violations }] }` | `TimeseriesResponse` with `TimeseriesBucketResponse` items        | Type-checked by mypy                                       |
| EP2-B1  | Auto-bucket: hourly for <= 48h range                         | `ViolationRedisRepository._build_time_buckets()`                  | `test_service.py::test_timeseries_hourly_buckets`          |
| EP2-B2  | Auto-bucket: daily for > 48h range                           | Same method                                                       | `test_service.py::test_timeseries_daily_buckets`           |


---

## EP-3: `GET /access-control/consumers`

Per-consumer request and violation counts.


| ID      | Requirement                                                 | Implementation                                                                 | Test                                                        |
| ------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------- |
| EP3-URL | `GET /access-control/consumers`                             | `api/routes/pbac/access_control.py` → `get_requests_by_consumer()`             | `test_access_control_routes.py::test_consumers_returns_200` |
| EP3-P1  | Param: `start_date` (datetime)                              | Route: `Query(...)`                                                            | Tested in route test                                        |
| EP3-P2  | Param: `end_date` (datetime)                                | Route: `Query(...)`                                                            | Tested in route test                                        |
| EP3-P3  | Params: `consumer[]`, `policy[]`, `dataset[]`, `data_use[]` | Route: `Query(None)` each                                                      | `test_service.py::test_consumer_summaries_with_filters`     |
| EP3-P4  | Param: `order_by` (`violation_count` | `request_count`)     | Route: `Query("violation_count")`                                              | `test_service.py::test_consumer_summaries_order_by`         |
| EP3-R1  | Response: `{ items: [{ name, requests, violations }] }`     | `ConsumerRequestsByConsumerResponse` wrapping `ConsumerRequestSummaryResponse` | Type-checked by mypy                                        |


---

## EP-4: `GET /access-control/violations`

Paginated aggregate findings grouped by policy + control.


| ID      | Requirement                                                           | Implementation                                                   | Test                                                              |
| ------- | --------------------------------------------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------- |
| EP4-URL | `GET /access-control/violations`                                      | `api/routes/pbac/access_control.py` → `get_violation_findings()` | `test_access_control_routes.py::test_violations_page_returns_200` |
| EP4-P1  | Param: `page` (int)                                                   | `Params` dependency                                              | `test_access_control_routes.py::test_violations_pagination`       |
| EP4-P2  | Param: `size` (int)                                                   | `Params` dependency                                              | `test_access_control_routes.py::test_violations_pagination`       |
| EP4-P3  | Param: `policy[]` (list, optional) — was single string                | Route: `Query(None)`                                             | `test_service.py::test_findings_multi_policy_filter`              |
| EP4-P4  | Param: `control[]` (list, optional) — was single string               | Route: `Query(None)`                                             | Tested via repo `get_findings()`                                  |
| EP4-P5  | Param: `sort_by` (`violation_count` | `last_violation`)               | Route: `Query("violation_count")`                                | Tested via repo sort logic                                        |
| EP4-P6  | Param: `sort_direction` (`asc` | `desc`) — new                        | Route: `Query("desc")`                                           | `test_service.py::test_findings_sort_direction_asc`               |
| EP4-P7  | Param: `start_date` (datetime, optional) — new                        | Route: `Query(None)`                                             | `test_service.py::test_findings_date_range`                       |
| EP4-P8  | Param: `end_date` (datetime, optional) — new                          | Route: `Query(None)`                                             | Same as EP4-P7                                                    |
| EP4-R1  | Response: `{ items, total, page, size, pages }`                       | `Page[ViolationFindingResponse]`                                 | Type-checked by mypy                                              |
| EP4-R2  | Item fields: `policy`, `control`, `violation_count`, `last_violation` | `ViolationFindingResponse`                                       | Type-checked by mypy                                              |


---

## EP-5: `GET /access-control/violations/logs`

Cursor-paginated violation log entries with enriched fields.


| ID      | Requirement                                                       | Implementation                                               | Test                                                   |
| ------- | ----------------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------ |
| EP5-URL | `GET /access-control/violations/logs`                             | `api/routes/pbac/access_control.py` → `get_violation_logs()` | `test_access_control_routes.py::test_logs_returns_200` |
| EP5-P1  | Param: `cursor` (str, optional) — replaces page/size offset       | Route: `Query(None)`                                         | `test_service.py::test_logs_cursor_first_page`         |
| EP5-P2  | Param: `size` (int, default 25)                                   | Route: `Query(25)`                                           | Tested in cursor tests                                 |
| EP5-P3  | Params: `consumer[]`, `policy[]`, `dataset[]`, `data_use[]` — new | Route: `Query(None)` each                                    | `test_service.py::test_logs_cursor_with_filters`       |
| EP5-P4  | Params: `start_date`, `end_date` — new                            | Route: `Query(None)` each                                    | Tested via repo date range                             |
| EP5-R1  | Response: `{ items, next_cursor, size }` — was Page               | `CursorPaginatedViolationLogs` schema                        | Type-checked by mypy                                   |
| EP5-R2  | Item field: `id`                                                  | Maps from `PolicyViolationEntity.id`                         | `test_access_control_routes.py::test_logs_returns_200` |
| EP5-R3  | Item field: `timestamp`                                           | Maps from `entity.violation_timestamp`                       | Tested in route test                                   |
| EP5-R4  | Item field: `consumer`                                            | Maps from `entity.consumer_name`                             | Tested in route test                                   |
| EP5-R5  | Item field: `consumer_email` — new                                | Maps from `entity.consumer_email`                            | Tested in route test                                   |
| EP5-R6  | Item field: `policy`                                              | Maps from `entity.policy_key`                                | Tested in route test                                   |
| EP5-R7  | Item field: `policy_description` — new                            | Service enriches via `_purpose_repo.get_by_fides_key()`      | `test_service.py::test_detail_policy_description`      |
| EP5-R8  | Item field: `dataset`                                             | Maps from `entity.dataset_key`                               | Tested in route test                                   |
| EP5-R9  | Item field: `data_use`                                            | Maps from `entity.data_use`                                  | Tested in route test                                   |
| EP5-R10 | Item field: `sql_statement` — new (was `query_text`)              | Maps from `entity.query_text`                                | Tested in route test                                   |
| EP5-R11 | Item field: `ai_reason` (nullable) — new (was `reason`)           | Maps from `entity.reason`                                    | Tested in route test                                   |
| EP5-C1  | `next_cursor` is null when no more results                        | Repo fetches `size + 1`, returns null if <= size results     | `test_service.py::test_logs_cursor_last_page`          |
| EP5-C2  | Cursor-based continuation returns next batch                      | Repo uses `ZRANGEBYSCORE` + position after cursor            | `test_service.py::test_logs_cursor_continuation`       |


---

## EP-6: `GET /access-control/violations/{id}`

Full detail for a single violation.


| ID      | Requirement                                                         | Implementation                                                 | Test                                                                     |
| ------- | ------------------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------ |
| EP6-URL | `GET /access-control/violations/{id}`                               | `api/routes/pbac/access_control.py` → `get_violation_detail()` | `test_access_control_routes.py::test_detail_returns_200`                 |
| EP6-R1  | Response field: `id`                                                | `entity.id`                                                    | `test_access_control_routes.py::test_detail_response_shape`              |
| EP6-R2  | Response field: `timestamp`                                         | `entity.violation_timestamp`                                   | Tested in route test                                                     |
| EP6-R3  | Response field: `consumer` — was nested `data_consumer.name`        | `entity.consumer_name` (flattened)                             | Tested in route test                                                     |
| EP6-R4  | Response field: `consumer_email` — was nested `data_consumer.email` | `entity.consumer_email` (flattened)                            | Tested in route test                                                     |
| EP6-R5  | Response field: `policy` — was `policy_key`                         | `entity.policy_key` (renamed in schema)                        | Tested in route test                                                     |
| EP6-R6  | Response field: `policy_description` — new                          | Service enriches via `_purpose_repo.get_by_fides_key()`        | `test_service.py::test_detail_policy_description`                        |
| EP6-R7  | Response field: `dataset`                                           | `entity.dataset_key`                                           | Tested in route test                                                     |
| EP6-R8  | Response field: `data_use` — new in detail                          | `entity.data_use`                                              | Tested in route test                                                     |
| EP6-R9  | Response field: `sql_statement` — was `request_context`             | `entity.query_text` (renamed in schema)                        | Tested in route test                                                     |
| EP6-R10 | Response field: `ai_reason` (nullable) — was `reason`               | `entity.reason` (renamed in schema)                            | Tested in route test                                                     |
| EP6-404 | Returns 404 `{ "detail": "Not found" }` for missing ID              | Route catches `ResourceNotFoundError` → `HTTPException(404)`   | `test_access_control_routes.py::test_detail_returns_404_for_nonexistent` |


---

## EP-7: `GET /access-control/filters`

Cascading filter values for dropdown population. Follows the asset-reporting pattern (`GET /assets-report/filters`): accepts the same filter params as the data endpoints so that returned options narrow based on current selections.


| ID       | Requirement                                                                | Implementation                                                    | Test                                                      |
| -------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------- |
| EP7-URL  | `GET /access-control/filters`                                              | `api/routes/pbac/access_control.py` → `get_filters()`             | `test_access_control_routes.py::test_filters_returns_200` |
| EP7-P1   | Param: `start_date` (datetime, optional)                                   | Route: `Query(None)`                                              | `test_service.py::test_filters_with_date_range`           |
| EP7-P2   | Param: `end_date` (datetime, optional)                                     | Route: `Query(None)`                                              | Same as EP7-P1                                            |
| EP7-P3   | Params: `consumer[]`, `policy[]`, `dataset[]`, `data_use[]` (all optional) | Route: `Query(None)` each                                         | `test_service.py::test_filters_cascading`                 |
| EP7-C1   | Cascading: active filters narrow returned values                           | Repo applies `_apply_filters()` before collecting distinct values | `test_service.py::test_filters_cascading`                 |
| EP7-R1   | Response field: `consumers` (list[str])                                    | Repo → distinct `consumer_name` from filtered set                 | `test_service.py::test_filters_consumers`                 |
| EP7-R2   | Response field: `policies` (list[str])                                     | Repo → distinct `policy_key` from filtered set                    | `test_service.py::test_filters_policies`                  |
| EP7-R3   | Response field: `datasets` (list[str])                                     | Repo → distinct `dataset_key` from filtered set                   | `test_service.py::test_filters_datasets`                  |
| EP7-R4   | Response field: `data_uses` (list[str])                                    | Repo → distinct `data_use` from filtered set                      | `test_service.py::test_filters_data_uses`                 |
| EP7-AUTH | Requires `POLICY_VIOLATION_READ` scope                                     | Route `dependencies=[Security(...)]`                              | Auth enforced by middleware                               |


---

## Files Modified


| File                                                  | Change                                                                                       |
| ----------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `src/fidesplus/service/pbac/violations/entities.py`   | Added `AccessControlSummary`, `FilterValues` dataclasses; extended `ViolationLogEntry`       |
| `src/fidesplus/service/pbac/violations/repository.py` | Added indexes, `_apply_filters()`, cursor pagination, auto-bucket intervals, `get_filters()` |
| `src/fidesplus/service/pbac/query_log/repository.py`  | Added time index on `create()`, `count_in_range()`                                           |
| `src/fidesplus/service/pbac/violations/service.py`    | New constructor with purpose/access-log repos; new + updated methods                         |
| `src/fidesplus/api/schemas/violation.py`              | New response models; reshaped `ViolationDetailResponse` and `ViolationLogEntryResponse`      |
| `src/fidesplus/api/routes/pbac/access_control.py`     | **New** — all 7 dashboard endpoints + `/reason`                                              |
| `src/fidesplus/api/routes/pbac/violations.py`         | **Removed** — routes moved to `access_control.py`                                            |
| `src/fidesplus/main.py`                               | Registered `access_control.router`; removed `violations.router`                              |
| `tests/ops/service/pbac/violations/test_service.py`   | **New** — 29 service unit tests                                                              |
| `tests/ops/api/test_access_control_routes.py`         | **New** — 15 route integration tests                                                         |


