# Auth Scan: ows-track

Generated by: `python-service-scan-authz-baseline` skill on 2026-06-16

## Summary

| Posture | Count |
|---|---|
| 🟢 PP enforced (complete) | 0 |
| 🟡 PP + permissive fallback | 0 |
| 🟡 PP + required fallback | 0 |
| 🟡 PP + legacy fallback (FastAPI) | 0 |
| 🟡 Legacy only — needs PP (FastAPI) | 0 |
| 🟡 Grass only (required) | 0 |
| 🟡 Grass only (permissive) | 0 |
| 🟡 Access rules (enforced) | 69 |
| 🟡 Access rules (enforced) | 0 |
| 🔴 Access rules (default deny / unmatched) | 0 |
| 🔴 No auth | 0 |
| 🔴 No resource-level auth (FastAPI) | 0 |
| ⚪ Needs human review | 6 |
| 🟢 No auth (excluded) | 2 |

> **Key nuance**: `flask_request.setup` has `verify_access=False` and `rules_file=None` — the middleware enforcer is off. However, `flask_request.set_rules_validator(app, 'backend/access_rules.yml')` is called separately, and every handler independently calls `flask_request.verify_rules_access_standalone(request)` to enforce `access_rules.yml` at the handler level. Because every handler calls it directly, these 69 endpoints are classified as `🟡 Access rules (enforced)` — requests that don't satisfy the rules are rejected with 403. The risk is architectural: there is no middleware backstop if a new handler omits the standalone call.

---

## Auth infrastructure

- `verify_access`: `False`
- `access_log_only`: `False` (passed as `config.ONLY_LOG_ACCESS_ERRORS`, defined `= False` at `config.py:121`)
- `rules_file`: `None` (passed to `flask_request.setup`)
- `exclude_paths`: `[HEALTH_CHECK, '/public/performer/roles']` → `['/hello/', '/public/performer/roles']` (from `config.EXCLUDE_PATH`, `config.py:123`)
- `set_rules_validator`: `'backend/access_rules.yml'` — registers rules for standalone per-handler enforcement
- `access_rules.yml`: 31 explicit path rule entries (28 unique paths) + catch-all `<*>` (`LabelProfile: [administrator]`, `OrchAdminProfile: ['*']`)

> ⚠️ Interpretation: The middleware-level access-rules enforcer is disabled (`verify_access=False`, `rules_file=None`). However, every handler calls `verify_rules_access_standalone(request)` directly, which enforces `access_rules.yml` at the handler level — this is active, working protection. Endpoints are classified `🟡 Access rules (enforced)`. The architectural risk is that there is no middleware backstop for a future handler that omits the standalone call.

**PP status:**
- `PdpAuthorizationBackend` imported and instantiated in `config.py:146` as `pdp_authorization_backend` ✅ — this is the enforce-phase backend; do not change it
- `MigrationAuthorizationBackend` not yet added — add as a second singleton in `backend/api.py` (wrapping the existing `pdp_authorization_backend`) before any endpoint can emit `pp_auth.rollout.would_deny`
- No handler-level `is_authorized()` calls found anywhere in the service yet

---

## Endpoint Auth Posture

> **Column order is fixed — do not reorder.**

| Posture | Endpoint | Handler | File | Evidence | Confidence |
|---|---|---|---|---|---|
| 🟢 No auth (excluded) | GET /hello/ | `health` | base_handlers.py:16 | In `config.EXCLUDE_PATH` | High |
| 🟢 No auth (excluded) | GET /public/performer/roles | `get_public_performer_roles` | performer_handlers.py:43 | In `config.EXCLUDE_PATH` | High |
| 🟡 Access rules (enforced) | POST /track | `create_track` | track_handlers.py:34 | `verify_rules_access_standalone` enforces at handler level | High |
| ⚪ Needs human review | GET /track/upc/\<upc\>/isrc/\<isrc\> | `get_track_by_upc_and_isrc` | track_handlers.py:61 | rules check + JWT identity fallback via `is_jwt_identity_authorized` | High |
| 🟡 Access rules (enforced) | POST /product/\<product_id\>/tracks | `bulk_create_tracks` | track_handlers.py:93 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | POST /product/\<product_id\>/tracks/import | `bulk_import_tracks` | track_handlers.py:130 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | POST /product/\<product_id\>/tracks/change_genre | `update_artist` | track_handlers.py:166 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | POST /product/\<src_id\>/tracks/copy/\<dest_id\> | `import_product_tracks` | track_handlers.py:213 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | GET /track/\<tuid\> | `get_track` | track_handlers.py:262 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | GET /tracks | `get_multiple_tracks` | track_handlers.py:294 | `verify_rules_access_standalone` enforces at handler level | High |
| ⚪ Needs human review | GET /tracks-bulk | `get_multiple_bulk_tracks` | track_handlers.py:322 | rules check + JWT identity fallback via `is_jwt_identity_authorized` | High |
| 🟡 Access rules (enforced) | POST /track-wide-dataloader | `get_tracks_dataloaded` | track_handlers.py:374 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | GET /product/\<product_id\>/tracks | `get_all_tracks_by_product_id` | track_handlers.py:403 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | POST /products/trackids | `get_tracks_ids_by_product_ids` | track_handlers.py:437 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | GET /product/\<product_id\>/tracks/light | `get_all_tracks_by_product_id_light` | track_handlers.py:510 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | PUT /track/\<tuid\> | `update_track` | track_handlers.py:541 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | PATCH /track/\<tuid\> | `update_track_patch` | track_handlers.py:579 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | PATCH /track/\<tuid\>/duration | `update_track_duration` | track_handlers.py:622 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | PATCH /product/\<product_id\>/tracks | `update_field_for_all_tracks_in_product` | track_handlers.py:654 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | PUT /product/\<product_id\>/tracks/arrange | `reorder_tracks` | track_handlers.py:690 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | DELETE /track/\<tuid\> | `delete_track` | track_handlers.py:718 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | DELETE /product/\<product_id\>/tracks | `bulk_delete_tracks` | track_handlers.py:744 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | POST /track/\<tuid\>/\<role\> | `create_track_role` | track_handlers.py:780 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | PATCH /track/\<tuid\>/\<role\>/\<role_id\> | `update_track_role` | track_handlers.py:810 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | PATCH /product/\<product_id\>/tracks/\<role_type\> | `update_roles_for_all_tracks_in_product` | track_handlers.py:843 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | DELETE /track/\<tuid\>/\<role_type\>/\<track_role_id\> | `delete_track_role` | track_handlers.py:872 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | PUT /track/\<tuid\>/localization/\<language_id\> | `update_track_localization` | track_handlers.py:903 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | DELETE /track/\<tuid\>/localization/\<language_id\> | `delete_track_localization` | track_handlers.py:935 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | GET /product/\<product_id\>/tracks/validate | `validate_tracks_for_product` | track_handlers.py:964 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | POST /vendor/tracks/validate/dataloader | `dataload_validate_tracks_for_vendor` | track_handlers.py:1029 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | GET /isrc/\<track_type\>/\<isrc\> | `is_isrc_in_use` | track_handlers.py:1089 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | GET /track/\<track_type\>/\<isrc\> | `get_all_tracks_by_type_and_isrc` | track_handlers.py:1111 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | GET /\<account_type\>/\<account_id\>/tracks/\<track_type\>/\<isrc\> | `get_account_tracks_by_type_and_isrc` | track_handlers.py:1139 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | PUT /product/\<product_id\>/tracks | `create_tracks_with_metadata` | track_handlers.py:1178 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | PUT /product/\<product_id\>/tracks/set | `set_product_tracks` | track_handlers.py:1207 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | GET /track-artist/\<track_artist_id\>/track | `get_track_by_track_artist_id` | track_handlers.py:1244 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | POST /claim_new_isrc | `claim_new_isrc` | track_handlers.py:1274 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | GET /isrc/\<isrc\>/matches | `isrc_match_info` | track_handlers.py:1293 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | HEAD /profile/uuid/\<profile_uuid\>/resource/track/id/\<tuid\> | `check_profile_track_access` | track_handlers.py:1319 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | GET /tracks/osr_id/\<osr_id\> | `get_tracks_by_osrid` | track_handlers.py:1333 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | GET /track/\<tuid\>/spatial | `get_track_spatial` | track_handlers.py:1357 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | GET /products/\<product_id\>/tracks/spatial | `get_spatial_isrc_map_by_product_id` | track_handlers.py:1383 | `verify_rules_access_standalone` enforces at handler level | High |
| ⚪ Needs human review | POST /track/\<tuid\>/spatial | `create_track_spatial` | track_handlers.py:1410 | rules check + JWT identity fallback via `is_jwt_identity_authorized` | High |
| 🟡 Access rules (enforced) | PUT /track/\<tuid\>/spatial | `update_track_spatial` | track_handlers.py:1451 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | DELETE /track/\<tuid\>/spatial | `delete_track_spatial` | track_handlers.py:1481 | `verify_rules_access_standalone` enforces at handler level | High |
| ⚪ Needs human review | GET /performer/roles | `get_performer_roles` | performer_handlers.py:17 | rules check + JWT identity fallback via `is_jwt_identity_authorized` | High |
| 🟡 Access rules (enforced) | GET /track/\<tuid\>/performers | `get_performers_for_track` | performer_handlers.py:55 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | POST /tracks/performers | `get_performers_for_tracks` | performer_handlers.py:79 | `verify_rules_access_standalone` enforces at handler level | High |
| ⚪ Needs human review | GET /product/\<product_id\>/tracks/performers | `get_performers_for_tracks_in_product` | performer_handlers.py:99 | rules check + JWT identity fallback via `is_jwt_identity_authorized` | High |
| 🟡 Access rules (enforced) | PATCH /product/\<product_id\>/tracks/performers/\<performer_type\> | `update_by_performer_type_for_product` | performer_handlers.py:132 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | PUT /track/\<tuid\>/performers | `update_performers` | performer_handlers.py:163 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | GET /product/\<product_id\>/tracks/validate/performers | `validate_tracks_performers_for_product` | performer_handlers.py:191 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | GET /audio-attributes | `get_audio_attributes` | audio_attribute_handlers.py:17 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | GET /audio-attributes/\<tuid\> | `get_track_audio_attributes` | audio_attribute_handlers.py:35 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | GET /suggested-audio-attributes/\<tuid\> | `get_track_suggested_audio_attributes` | audio_attribute_handlers.py:53 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | GET /audio-attributes-edits/\<tuid\> | `get_track_audio_attributes_edits` | audio_attribute_handlers.py:77 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | POST /upsert-audio-attributes | `upsert_track_audio_attributes` | audio_attribute_handlers.py:95 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | POST /upsert-audio-attributes-edits | `upsert_track_audio_attributes_edits` | audio_attribute_handlers.py:124 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | GET /rights-attributes | `get_rights_attributes` | rights_attribute_handlers.py:17 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | GET /suggested-rights-attributes/\<tuid\> | `get_suggested_rights_attributes` | rights_attribute_handlers.py:35 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | GET /rights-attributes/\<tuid\> | `get_track_rights_attributes` | rights_attribute_handlers.py:60 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | GET /rights-attributes-edits/\<tuid\> | `get_track_rights_attributes_edits` | rights_attribute_handlers.py:79 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | POST /upsert-rights-attributes | `upsert_track_rights_attributes` | rights_attribute_handlers.py:98 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | POST /upsert-rights-attributes-edits | `upsert_track_rights_attributes_edits` | rights_attribute_handlers.py:128 | `verify_rules_access_standalone` enforces at handler level | High |
| ⚪ Needs human review | GET /product/\<product_id\>/tracks/grats | `get_all_instant_grats_by_product_id` | instant_grat_handlers.py:15 | rules check + JWT identity fallback via `is_jwt_identity_authorized` | High |
| 🟡 Access rules (enforced) | DELETE /product/\<product_id\>/tracks/grats/\<track_id\>/store/\<store_id\> | `delete_instant_grat_by_store_id` | instant_grat_handlers.py:54 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | DELETE /product/\<product_id\>/tracks/grats | `delete_all_instant_grats_by_product_id` | instant_grat_handlers.py:87 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | POST /product/\<product_id\>/tracks/grats | `create_instant_grats_for_product_id` | instant_grat_handlers.py:117 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | PATCH /product/\<product_id\>/tracks/grats | `update_instant_grats_for_product_id` | instant_grat_handlers.py:149 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | POST /track/\<tuid\>/samples | `create_track_sample` | track_sample_handlers.py:15 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | GET /track/samples/\<sampleid\> | `get_track_sample` | track_sample_handlers.py:35 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | DELETE /track/samples/\<sampleid\> | `delete_sample_data` | track_sample_handlers.py:57 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | DELETE /track/samples | `bulk_delete_samples_by_tuids` | track_sample_handlers.py:79 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | PATCH /track/samples/\<sampleid\> | `update_sample_data` | track_sample_handlers.py:107 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | GET /product/\<product_id\>/tracks/publishing-obligation | `get_tracks_publishing_obligation_data_for_product` | publishing_obligation_handlers.py:16 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | PUT /product/\<product_id\>/tracks/publishing-obligation | `update_tracks_publishing_obligation` | publishing_obligation_handlers.py:44 | `verify_rules_access_standalone` enforces at handler level | High |
| 🟡 Access rules (enforced) | GET /product/\<product_id\>/tracks/validate/publishing-obligation | `validate_tracks_pub_obl_for_product` | publishing_obligation_handlers.py:74 | `verify_rules_access_standalone` enforces at handler level | High |

> **Endpoint discovery confidence**: Routes discovered via `@app.route` grep across all handler files imported in `application.py` via `from backend.handlers import *`. No `add_url_rule`, `MethodView`, `as_view`, or `flask_restful.add_resource` patterns found. All handler files in `backend/handlers/` are covered.

---

## Detailed Findings

### Group: Access Rules (Enforced) — Per-Handler Standalone Enforcement (69 endpoints)

This finding covers all 69 endpoints that use `verify_rules_access_standalone(request)` as their only auth mechanism. The pattern is uniform across all handler files.

- **Current posture**: 🟡 Access rules (enforced)
- **Enforcement mechanism**: `flask_request.verify_rules_access_standalone(request)` — checks the caller's profile against `backend/access_rules.yml` rules registered via `flask_request.set_rules_validator`. Returns falsy if the caller's profile/role is not in the rule for the matched path.
- **Auth call**: `access_rule_decision = flask_request.verify_rules_access_standalone(request)` → 403 if falsy
- **PP status**: Not started — `pdp_authorization_backend` exists in `config.py` but no handler calls `is_authorized()`
- **Why `verify_access=False`**: The middleware is intentionally off; all enforcement is per-handler. There is no middleware safety net.
- **access_rules.yml coverage**: Explicit rules for most critical paths; all others fall through to the catch-all (`<*>`, `LabelProfile: [administrator]`, `OrchAdminProfile: ['*']`). The catch-all is restrictive — it only admits label administrators and OrchAdmin, not regular label or content profiles.

#### Phase 1 — Baseline integration tests

Representative test cases for a mutating endpoint (e.g. `PUT /track/<tuid>`):

- Label profile caller with `LabelProfile` role sends PUT → expect `200`
- OrchAdmin profile caller sends PUT → expect `200`
- ContentProfile caller with `review_digital_audio` sends PUT → expect `403` (not in `/track/<int><*>` rule for PUT)
- Caller with no profile headers at all sends PUT → expect `403`
- Caller with profile matching only the catch-all (`LabelProfile: [administrator]`) sends PUT → expect `200`

Representative test cases for a read endpoint (e.g. `GET /track/<tuid>`):

- LabelProfile caller sends GET → expect `200`
- ContentProfile caller with `view_orchard_sound_recording` sends GET → expect `200`
- ArtistProfile caller sends GET → expect `200`
- No-profile caller sends GET → expect `403`

#### Phase 2 — Shadow

- **Key API**: `MigrationAuthorizationBackend` wrapping `pdp_authorization_backend` (already in `config.py`); add `request_tags()` in `backend/context.py`; bolt a side-effect-only `shadow_authorization(resource_id=..., resource_type='track', action=...)` call in each handler alongside the untouched `verify_rules_access_standalone` check.
- **Behavior change**: None — `verify_rules_access_standalone` continues to enforce; shadow call always allows and emits `pp_auth.rollout.would_deny` on a would-deny.
- **Note**: `pdp_authorization_backend` (`PdpAuthorizationBackend`) is already instantiated in `config.py:146` — **do not change it**. For Phase 2, add a second singleton `migration_authorization_backend` in `api.py` that wraps the existing one. Handlers import from `api.py`, not `config.py`, so the enforce-phase swap (Phase 3) only requires updating `api.py`.

  ```python
  # backend/api.py  (add after app creation)
  from python_pdp_sdk.migration import MigrationAuthorizationBackend
  from backend import config

  migration_authorization_backend = MigrationAuthorizationBackend(
      config.pdp_authorization_backend,
      service_name="ows-track",
  )
  ```

  Handlers then do `from backend.api import migration_authorization_backend` and call
  `migration_authorization_backend.is_authorized(...)` alongside the untouched
  `verify_rules_access_standalone` call. In Phase 3, swap the import to
  `from backend import config` and call `config.pdp_authorization_backend.is_authorized(...)`
  directly — or rename the `api.py` export to `authorization_backend` and flip the
  implementation behind it without changing handler imports.

#### Phase 3 — Enforce

- **Template**: C
- **Key API**: `PdpAuthorizationBackend.is_authorized()` via `auth.assert_authorization(resource_id=..., resource_type='track', action=...)` — no fallback. Replace the `verify_rules_access_standalone` call with the PP enforcement call.
- **Caller analysis required**: Yes — verify no non-profile callers (Lambdas, internal services) hit these endpoints without a JWT before enabling enforcement.
- **Risk**: The catch-all rule today allows `LabelProfile: [administrator]` and `OrchAdminProfile: ['*']` across all unmatched paths. Callers relying on the catch-all may not have corresponding PP derived roles defined. Run `endpoint-resource-action-pp-authorization-table` to map resource/action pairs before starting Phase 3.

---

### GET /track/upc/\<upc\>/isrc/\<isrc\>

- **Current posture**: ⚪ Needs human review
- **Handler**: `get_track_by_upc_and_isrc` in `track_handlers.py:61`
- **Auth call**: `verify_rules_access_standalone` → if falsy, checks `g.request_context.jwt_identity_id`; requires JWT present AND `jwt_identity_id in AUTHORIZED_IDENTITIES` (3 hardcoded UUIDs: `BULK_ASSET_DOWNLOAD_IDENTITY_UUID`, `OWS_DELIVERY_METADATA_IDENTITY_UUID`, `SPATIAL_ASSET_INGESTER_IDENTITY_UUID`)
- **PP status**: Not started

#### Phase 1 — Baseline integration tests

- Profile-authorized caller (LabelProfile) sends GET → expect `200`
- Caller with one of the 3 AUTHORIZED_IDENTITIES UUIDs in JWT sends GET (no matching profile) → expect `200`
- Caller with a valid JWT but UUID not in AUTHORIZED_IDENTITIES sends GET → expect `403`
- Caller with no JWT and no profile → expect `403`
- Caller with expired JWT → expect `403`

#### Phase 2 — Shadow

- **Key API**: Same shadow setup as the main group. The two auth paths (profile via rules, machine identity via JWT UUID) both need to be modeled in PP — this is the reason for human review.
- **Behavior change**: None.

#### Phase 3 — Enforce

- **Template**: C
- **Key API**: `PdpAuthorizationBackend.is_authorized()` — requires both human (label profile) and machine (JWT UUID) callers to have PP principals defined.
- **Caller analysis required**: Yes — the 3 hardcoded UUIDs correspond to internal service identities; each needs a dedicated M2M JWT principal policy in PP before enforcement.
- **Risk**: HIGH — if M2M principals are not provisioned before enforcement, three internal service integrations (bulk asset download, OWS delivery metadata, spatial asset ingester) will receive 403s.

---

### GET /tracks-bulk

- **Current posture**: ⚪ Needs human review
- **Handler**: `get_multiple_bulk_tracks` in `track_handlers.py:322`
- **Auth call**: `verify_rules_access_standalone` → if falsy, checks JWT identity UUID against `AUTHORIZED_IDENTITIES`
- **PP status**: Not started

#### Phase 1 — Baseline integration tests

- Profile-authorized caller (LabelProfile, InsightsProfile) sends GET → expect `200`
- Caller with `OWS_DELIVERY_METADATA_IDENTITY_UUID` in JWT, no profile → expect `200`
- Caller with valid JWT, UUID not in AUTHORIZED_IDENTITIES → expect `403`
- No-auth caller → expect `403`

#### Phase 2 — Shadow

- **Key API**: Same shadow setup. Both auth paths need PP coverage.
- **Behavior change**: None.

#### Phase 3 — Enforce

- **Template**: C
- **Key API**: `PdpAuthorizationBackend.is_authorized()`; M2M principals for the AUTHORIZED_IDENTITIES service accounts required.
- **Caller analysis required**: Yes.
- **Risk**: HIGH — internal service callers will break if M2M principals are not defined first.

---

### GET /performer/roles

- **Current posture**: ⚪ Needs human review
- **Handler**: `get_performer_roles` in `performer_handlers.py:17`
- **Auth call**: `verify_rules_access_standalone` → if falsy, requires JWT present (401 if missing) AND `is_jwt_identity_authorized` (403 if UUID not authorized)
- **PP status**: Not started

#### Phase 1 — Baseline integration tests

- Profile-authorized caller (any profile matching catch-all) sends GET → expect `200`
- Caller with one of the 3 AUTHORIZED_IDENTITIES UUIDs in JWT, no profile → expect `200`
- Caller with no JWT and no profile → expect `401`
- Caller with JWT but UUID not in AUTHORIZED_IDENTITIES → expect `403`

#### Phase 2 — Shadow

- **Key API**: Same shadow setup.
- **Behavior change**: None.

#### Phase 3 — Enforce

- **Template**: C
- **Key API**: `PdpAuthorizationBackend.is_authorized()`
- **Caller analysis required**: Yes — verify which internal services call this endpoint; they need M2M JWT principals.
- **Risk**: Medium — read-only endpoint, but used by internal services.

---

### GET /product/\<product_id\>/tracks/performers

- **Current posture**: ⚪ Needs human review
- **Handler**: `get_performers_for_tracks_in_product` in `performer_handlers.py:99`
- **Auth call**: `verify_rules_access_standalone` → if falsy, requires JWT present (401 if missing) AND `is_jwt_identity_authorized` (403 if UUID not in AUTHORIZED_IDENTITIES). Note: the check is `if jwt_identity_id and not is_jwt_identity_authorized(...)` — a present-but-unauthorized JWT returns 403; absent JWT returns 401.
- **PP status**: Not started

#### Phase 1 — Baseline integration tests

- Profile-authorized caller with matching access rule sends GET → expect `200`
- Caller with `BULK_ASSET_DOWNLOAD_IDENTITY_UUID` JWT, no profile → expect `200`
- No JWT, no profile → expect `401`
- JWT with unknown UUID → expect `403`

#### Phase 2 — Shadow

- **Key API**: Same shadow setup.
- **Behavior change**: None.

#### Phase 3 — Enforce

- **Template**: C
- **Key API**: `PdpAuthorizationBackend.is_authorized()`
- **Caller analysis required**: Yes.
- **Risk**: Medium — read-only, but internal service callers must have M2M tokens.

---

### GET /product/\<product_id\>/tracks/grats

- **Current posture**: ⚪ Needs human review
- **Handler**: `get_all_instant_grats_by_product_id` in `instant_grat_handlers.py:15`
- **Auth call**: `verify_rules_access_standalone` → if falsy, requires JWT (401 if missing) AND `is_jwt_identity_authorized` (403 if UUID not authorized)
- **PP status**: Not started

#### Phase 1 — Baseline integration tests

- ContentProfile, LabelProfile, OrchAdmin callers send GET → expect `200` (explicit rule in `access_rules.yml`)
- Caller with one of AUTHORIZED_IDENTITIES JWT UUIDs, no profile → expect `200`
- No-auth caller → expect `401`
- JWT with unknown UUID → expect `403`

#### Phase 2 — Shadow

- **Key API**: Same shadow setup.
- **Behavior change**: None.

#### Phase 3 — Enforce

- **Template**: C
- **Key API**: `PdpAuthorizationBackend.is_authorized()`
- **Caller analysis required**: Yes.
- **Risk**: Medium — read-only.

---

### POST /track/\<tuid\>/spatial

- **Current posture**: ⚪ Needs human review
- **Handler**: `create_track_spatial` in `track_handlers.py:1410`
- **Auth call**: `verify_rules_access_standalone` → if falsy, requires JWT (403 if missing) AND `is_jwt_identity_authorized` (403 if UUID not in AUTHORIZED_IDENTITIES, specifically `SPATIAL_ASSET_INGESTER_IDENTITY_UUID`)
- **PP status**: Not started

#### Phase 1 — Baseline integration tests

- Profile-authorized caller (catch-all: `LabelProfile: [administrator]`, `OrchAdminProfile`) sends POST → expect `200`/`201`
- Caller with `SPATIAL_ASSET_INGESTER_IDENTITY_UUID` JWT, no profile → expect `200`/`201`
- No JWT, no profile → expect `403`
- JWT with UUID not in AUTHORIZED_IDENTITIES → expect `403`

#### Phase 2 — Shadow

- **Key API**: Same shadow setup. `SPATIAL_ASSET_INGESTER_IDENTITY_UUID` is the primary machine identity for this endpoint — provision its M2M token before enforcing.
- **Behavior change**: None.

#### Phase 3 — Enforce

- **Template**: C
- **Key API**: `PdpAuthorizationBackend.is_authorized()`
- **Caller analysis required**: Yes — `SPATIAL_ASSET_INGESTER_IDENTITY_UUID` is a known internal service identity that creates spatial records and must have a PP principal policy before enforcement.
- **Risk**: HIGH — mutating endpoint; spatial asset ingester will break if M2M principal is not defined first.

---

## Caller analysis and rollout safety

> ⚠️ **Adding an enforcing PP check to a live endpoint will reject requests from any
> caller that cannot send a valid JWT.** Always follow this three-step rollout:

### Step 1 — Identify callers (Datadog APM, required before any code change)

1. Open [Datadog APM](https://sonymusic-pde.datadoghq.com/apm/home) → find this service.
2. Navigate to each endpoint resource and open the **Dependencies** tab.
3. Set the time window to **1 month** to capture infrequent callers.
4. Classify each caller using **only** the types in this table (do not add rows for caller types not listed here):

| Caller Type | JWT Support | Rollout Approach |
|---|---|---|
| **SPA / Suite Application (frontend)** | ✅ Yes if authenticated via Auth0; ❌ No if unauthenticated session | Confirm JWT presence in the shadow-phase `would_deny` metric before enforcing; unauthenticated SPAs must log in first |
| **Lambda** | ✅ Yes if M2M JWT provisioned; ❌ No if not yet provisioned | File a ticket to provision a dedicated M2M JWT before enforcing |

### Step 2 — Deploy in shadow mode first (`MigrationAuthorizationBackend` wired)

Wire `MigrationAuthorizationBackend` (Phase 2 in the migration templates) and add the side-effect
`is_authorized()` call. The wrapper always allows traffic and emits the Datadog metric
`pp_auth.rollout.would_deny` whenever the real PP decision *would* have denied. Monitor it over
1–2 weeks. Each increment identifies a caller that *would* have been rejected. The metric is tagged
with `environment`, `service_name`, `action`, `resource_type`, `reason`
(`pp_denied` | `unauthenticated` | `exception`), plus the `extra_tags_getter` tags
(`method`, `endpoint`, `has_authorization_header`, and `profile_type` on Flask). Break down by
`reason`, `endpoint`, and `has_authorization_header` to identify which callers lack a valid JWT.
Resolve every would-deny source before enabling enforcement.

> **Tracing individual denials**: the metric gives you counts, not per-request detail. To
> investigate a specific denial, use Datadog APM → find the service → filter by the endpoint in
> question and look for requests that correlate with a metric spike.

### Step 3 — Enforce (follow-up PR)

We can move to the `enforce` step when we're confident that enabling PP will not result in legitimate traffic being denied.

#### PP Enforce Readiness Criteria

1. PP [resource policies](https://app.notion.com/p/Writing-Cerbos-resource-policies-dbe5cc1d2ffd4ea6aa70bea59e772e6e) and [derived roles](https://app.notion.com/p/Derived-roles-tenants-tenant-hierarchy-13e84204dbdf48f99ce8cf06209d4836) 
   are defined for the application and downstream services. (`/endpoint-resource-action-pp-authorization-table` skill)
2. Human identities in requests from JWT-enabled applications have derived roles attached in PP. Configured using SettingsV2 or pdp-backfill.
3. Machine identities have dedicated M2M tokens and Principal policies defined in PP.
4. Traffic that does not meet the PP criteria can be authorized by the fallback method, if available.
5. All other traffic is rejected.

The `pp_auth.rollout.would_deny` metric is how you verify the criteria above took effect: each
increment is a request PP *would* deny. You're ready to enforce when the only remaining increments
are traffic you intend to reject (criterion 5) — i.e. every legitimate caller (criteria 1–4) already
passes the PP check or is covered by the fallback.

Then ship the Phase 3 enforce change: swap `MigrationAuthorizationBackend` → `PdpAuthorizationBackend`
and restructure the handler to PP-first + legacy fallback (Templates A/B/C for Flask, FA/FB/FC for
FastAPI). This is a code change, not an env-var flip.

---

## Prerequisites checklist

- [ ] `python-pdp-sdk[migration] == 6.2.0` in service dependencies (the `migration` extra provides `MigrationAuthorizationBackend`; current version is `5.1.0` — upgrade required)
- [x] `OwsClient` configured (`config.py:145` — `ows_client = setup_ows_client(ENVIRONMENT)`)
- [x] `PdpAuthorizationBackend` instantiated — `config.py:146` as `pdp_authorization_backend`. **Do not modify this object.** It is the enforce-phase backend; Phase 2 wraps it.
- [ ] `migration_authorization_backend` singleton added to `backend/api.py` — wraps `config.pdp_authorization_backend` with `MigrationAuthorizationBackend` (see Phase 2 note in the Group finding above). Handlers import this for shadow calls.
- [ ] `request_tags()` helper added to `backend/context.py` so `MigrationAuthorizationBackend` can tag `pp_auth.rollout.would_deny` metrics (see `references/flask-migration-templates.md`)
- [ ] Cerbos resource/action policies exist (run `endpoint-resource-action-pp-authorization-table` if not — a CSV exists at `ows-track_endpoint_resource_action_table.csv` in the repo root)
- [ ] All upstream callers identified and capable of sending a JWT
- [ ] M2M JWT principals provisioned for the 3 `AUTHORIZED_IDENTITIES` service accounts (`BULK_ASSET_DOWNLOAD`, `OWS_DELIVERY_METADATA`, `SPATIAL_ASSET_INGESTER`) before enforcing any JWT-fallback endpoint

---

## Recommended Migration Order

### Priority table

| Priority | Endpoint | Posture | Template | Notes |
|---|---|---|---|---|
| MEDIUM | POST /track | 🟡 Access rules (enforced) | C | Mutating; creates tracks |
| MEDIUM | POST /product/\<product_id\>/tracks | 🟡 Access rules (enforced) | C | Mutating; bulk create |
| MEDIUM | POST /product/\<product_id\>/tracks/import | 🟡 Access rules (enforced) | C | Mutating; track import |
| MEDIUM | PUT /track/\<tuid\> | 🟡 Access rules (enforced) | C | Mutating; track update |
| MEDIUM | PATCH /track/\<tuid\> | 🟡 Access rules (enforced) | C | Mutating; track patch |
| MEDIUM | DELETE /track/\<tuid\> | 🟡 Access rules (enforced) | C | Mutating; track delete |
| MEDIUM | DELETE /product/\<product_id\>/tracks | 🟡 Access rules (enforced) | C | Mutating; bulk delete |
| MEDIUM | PUT /product/\<product_id\>/tracks | 🟡 Access rules (enforced) | C | Mutating; bulk create with metadata |
| MEDIUM | PUT /product/\<product_id\>/tracks/set | 🟡 Access rules (enforced) | C | Mutating; set tracks on product |
| HIGH | POST /track/\<tuid\>/spatial | ⚪ Needs human review | C | Mutating; JWT-fallback for `SPATIAL_ASSET_INGESTER`; M2M principal required first |
| MEDIUM | POST /claim_new_isrc | 🟡 Access rules (enforced) | C | Mutating; ISRC claim |
| MEDIUM | POST /product/\<product_id\>/tracks/change_genre | 🟡 Access rules (enforced) | C | Mutating; genre update |
| MEDIUM | POST /product/\<src_id\>/tracks/copy/\<dest_id\> | 🟡 Access rules (enforced) | C | Mutating; track copy |
| MEDIUM | PUT /track/\<tuid\>/localization/\<language_id\> | 🟡 Access rules (enforced) | C | Mutating; localization update |
| MEDIUM | DELETE /track/\<tuid\>/localization/\<language_id\> | 🟡 Access rules (enforced) | C | Mutating; localization delete |
| MEDIUM | POST /upsert-audio-attributes | 🟡 Access rules (enforced) | C | Mutating; audio attribute upsert |
| MEDIUM | POST /upsert-audio-attributes-edits | 🟡 Access rules (enforced) | C | Mutating |
| MEDIUM | POST /upsert-rights-attributes | 🟡 Access rules (enforced) | C | Mutating; rights attribute upsert |
| MEDIUM | POST /upsert-rights-attributes-edits | 🟡 Access rules (enforced) | C | Mutating |
| MEDIUM | PUT /product/\<product_id\>/tracks/publishing-obligation | 🟡 Access rules (enforced) | C | Mutating; publishing obligation update |
| MEDIUM | POST /product/\<product_id\>/tracks/grats | 🟡 Access rules (enforced) | C | Mutating; grat create |
| MEDIUM | DELETE /product/\<product_id\>/tracks/grats | 🟡 Access rules (enforced) | C | Mutating; grat bulk delete |
| MEDIUM | DELETE /product/\<product_id\>/tracks/grats/\<track_id\>/store/\<store_id\> | 🟡 Access rules (enforced) | C | Mutating; grat delete by store |
| MEDIUM | PATCH /product/\<product_id\>/tracks/grats | 🟡 Access rules (enforced) | C | Mutating; grat update |
| MEDIUM | POST /track/\<tuid\>/samples | 🟡 Access rules (enforced) | C | Mutating; sample create |
| MEDIUM | DELETE /track/samples/\<sampleid\> | 🟡 Access rules (enforced) | C | Mutating; sample delete |
| MEDIUM | DELETE /track/samples | 🟡 Access rules (enforced) | C | Mutating; bulk sample delete |
| MEDIUM | PATCH /track/samples/\<sampleid\> | 🟡 Access rules (enforced) | C | Mutating; sample update |
| MEDIUM | PATCH /track/\<tuid\>/duration | 🟡 Access rules (enforced) | C | Mutating; duration update |
| MEDIUM | PATCH /product/\<product_id\>/tracks | 🟡 Access rules (enforced) | C | Mutating; bulk field update |
| MEDIUM | PUT /product/\<product_id\>/tracks/arrange | 🟡 Access rules (enforced) | C | Mutating; track reorder |
| MEDIUM | POST /track/\<tuid\>/\<role\> | 🟡 Access rules (enforced) | C | Mutating; track role create |
| MEDIUM | PATCH /track/\<tuid\>/\<role\>/\<role_id\> | 🟡 Access rules (enforced) | C | Mutating; track role update |
| MEDIUM | PATCH /product/\<product_id\>/tracks/\<role_type\> | 🟡 Access rules (enforced) | C | Mutating; bulk role update |
| MEDIUM | DELETE /track/\<tuid\>/\<role_type\>/\<track_role_id\> | 🟡 Access rules (enforced) | C | Mutating; track role delete |
| MEDIUM | PUT /track/\<tuid\>/performers | 🟡 Access rules (enforced) | C | Mutating; performer update |
| MEDIUM | PATCH /product/\<product_id\>/tracks/performers/\<performer_type\> | 🟡 Access rules (enforced) | C | Mutating; performer type update |
| MEDIUM | PUT /track/\<tuid\>/spatial | 🟡 Access rules (enforced) | C | Mutating; spatial update |
| MEDIUM | DELETE /track/\<tuid\>/spatial | 🟡 Access rules (enforced) | C | Mutating; spatial delete |
| MEDIUM | GET /track/\<tuid\> | 🟡 Access rules (enforced) | C | Read; track by ID |
| MEDIUM | GET /tracks | 🟡 Access rules (enforced) | C | Read; multiple tracks by IDs |
| MEDIUM | GET /product/\<product_id\>/tracks | 🟡 Access rules (enforced) | C | Read; tracks by product |
| MEDIUM | GET /product/\<product_id\>/tracks/light | 🟡 Access rules (enforced) | C | Read; lightweight track list |
| MEDIUM | POST /products/trackids | 🟡 Access rules (enforced) | C | Read; track IDs by product list |
| MEDIUM | POST /track-wide-dataloader | 🟡 Access rules (enforced) | C | Read; bulk track dataload |
| MEDIUM | GET /track/\<tuid\>/performers | 🟡 Access rules (enforced) | C | Read; performers for track |
| MEDIUM | POST /tracks/performers | 🟡 Access rules (enforced) | C | Read; performers for track list |
| MEDIUM | GET /product/\<product_id\>/tracks/performers | 🟡 Access rules (enforced) | C | Read; performers for product |
| MEDIUM | GET /product/\<product_id\>/tracks/validate | 🟡 Access rules (enforced) | C | Read; track validation |
| MEDIUM | GET /product/\<product_id\>/tracks/validate/performers | 🟡 Access rules (enforced) | C | Read; performer validation |
| MEDIUM | GET /product/\<product_id\>/tracks/publishing-obligation | 🟡 Access rules (enforced) | C | Read; publishing obligation |
| MEDIUM | GET /product/\<product_id\>/tracks/validate/publishing-obligation | 🟡 Access rules (enforced) | C | Read; pub obligation validation |
| MEDIUM | GET /audio-attributes | 🟡 Access rules (enforced) | C | Read; all audio attributes |
| MEDIUM | GET /audio-attributes/\<tuid\> | 🟡 Access rules (enforced) | C | Read; track audio attributes |
| MEDIUM | GET /suggested-audio-attributes/\<tuid\> | 🟡 Access rules (enforced) | C | Read; suggested audio attributes |
| MEDIUM | GET /audio-attributes-edits/\<tuid\> | 🟡 Access rules (enforced) | C | Read; audio attribute edits |
| MEDIUM | GET /rights-attributes | 🟡 Access rules (enforced) | C | Read; all rights attributes |
| MEDIUM | GET /rights-attributes/\<tuid\> | 🟡 Access rules (enforced) | C | Read; track rights attributes |
| MEDIUM | GET /suggested-rights-attributes/\<tuid\> | 🟡 Access rules (enforced) | C | Read; suggested rights attributes |
| MEDIUM | GET /rights-attributes-edits/\<tuid\> | 🟡 Access rules (enforced) | C | Read; rights attribute edits |
| MEDIUM | GET /track/samples/\<sampleid\> | 🟡 Access rules (enforced) | C | Read; track sample |
| MEDIUM | GET /track/\<tuid\>/spatial | 🟡 Access rules (enforced) | C | Read; track spatial |
| MEDIUM | GET /products/\<product_id\>/tracks/spatial | 🟡 Access rules (enforced) | C | Read; spatial ISRC map |
| MEDIUM | GET /tracks/osr_id/\<osr_id\> | 🟡 Access rules (enforced) | C | Read; tracks by OSR ID |
| MEDIUM | GET /isrc/\<isrc\>/matches | 🟡 Access rules (enforced) | C | Read; ISRC match info |
| MEDIUM | GET /isrc/\<track_type\>/\<isrc\> | 🟡 Access rules (enforced) | C | Read; ISRC in-use check |
| MEDIUM | GET /track/\<track_type\>/\<isrc\> | 🟡 Access rules (enforced) | C | Read; tracks by type and ISRC |
| MEDIUM | GET /\<account_type\>/\<account_id\>/tracks/\<track_type\>/\<isrc\> | 🟡 Access rules (enforced) | C | Read; account tracks by type/ISRC |
| MEDIUM | GET /track-artist/\<track_artist_id\>/track | 🟡 Access rules (enforced) | C | Read; track by track artist ID |
| MEDIUM | HEAD /profile/uuid/\<profile_uuid\>/resource/track/id/\<tuid\> | 🟡 Access rules (enforced) | C | Read; profile access check |
| MEDIUM | POST /vendor/tracks/validate/dataloader | 🟡 Access rules (enforced) | C | Read; ISRC cross-track validation |
| LOW | GET /track/upc/\<upc\>/isrc/\<isrc\> | ⚪ Needs human review | C | JWT fallback; M2M principals required before enforcing |
| LOW | GET /tracks-bulk | ⚪ Needs human review | C | JWT fallback; M2M principals required |
| LOW | GET /performer/roles | ⚪ Needs human review | C | JWT fallback; M2M principals required |
| LOW | GET /product/\<product_id\>/tracks/performers | ⚪ Needs human review | C | JWT fallback; M2M principals required |
| LOW | GET /product/\<product_id\>/tracks/grats | ⚪ Needs human review | C | JWT fallback; M2M principals required |

> **Note on ⚪ priority**: JWT-fallback endpoints are marked LOW in the migration order table because they require human confirmation of M2M token provisioning before *any* enforcement — not because they are low risk. Treat them as blocking dependencies for the overall rollout.

> **Template C means no fallback.** The `verify_rules_access_standalone` per-handler call is replaced entirely by the PP decision. Any request that fails the PP check is rejected with 403. There is no grass or legacy fallback.

---

## References

- [`references/auth-system-quick-reference.md`](references/auth-system-quick-reference.md) — grass headers, access_rules, PP+grass fallback pattern
- [`references/flask-migration-templates.md`](references/flask-migration-templates.md) — Phase 2 shadow wiring (`MigrationAuthorizationBackend` + `request_tags`); Phase 3 enforce Templates A, B, C
- [`references/caller-analysis-and-rollout.md`](references/caller-analysis-and-rollout.md) — Datadog APM rollout steps; caller classification
- [`references/important-notes.md`](references/important-notes.md) — pitfalls and cross-cutting reminders
