This document describes the steps for Github Copilot agent to enable Permissions Platform (PP) authorization checks to a 
python Flask microservice.

## AI Agent Guidelines

### Before Making Changes:
1. Always read the existing endpoint implementation first
2. Check for existing authorization patterns in the codebase
3. Verify test file locations before creating new tests
4. Use `grep_search` to find similar implementations for reference

### File Discovery Patterns:
- Search for `flask_request.setup` to find the main API configuration
- Look for `*View` classes to identify Flask endpoints
- Find test files using patterns like `test_*.py` or `*_test.py`

### Development Notes

Follow these guidelines when updating a Flask microservice. 

1. Only update 1 file at a time.
2. Enable `flask_request.verify_rules_access_standalone` for every endpoint.
3. Add permissions platform authorization checks to 1 handler at a time.
4. The handler updates must be backwards-compatible with the existing `Profile-based` permissions method.
5. Every Flask handler change must have a corresponding unit test. The module path is usually `tests/unit/`.
6. If the probject already has functional tests, every Flask handler change must have a corresponding functional test. The module path is usually `tests/functional/`.
7. Every Flask handler change must have a corresponding integration test. The module path is usually `tests/integration/`.


### Building the Python developent environment

- make pip_dev

### Running the unit tests
- make ci_unit_lint

### Running the integration tests
- make ci_test_integration


### PyPI server
- The internal PyPI server is: https://pypi.theorchard.io/


### Expected Project Structure

project-root/ 
├── requirements.txt 
├── requirements-dev.txt 
├── project_root_module
   ├── config.py 
   ├── api.py 
   ├── utils/ 
   │ └── authorization.py 
└── tests/ 
   ├── unit/ 
   ├── functional/ 
   └── integration/


### Expected flask_request.setup location
- The `flask_request.setup` call is usually in a file named `api.py`. For example:
  - product_review/api.py
  - abacus_account/api.py


### Version Compatibility

| owsclient | python-pdp-sdk | jwtauth | Python |
|-----------|----------------|---------|--------|
| 0.7.0     | 5.3.0          | 0.5.3   | 3.8+   |


###  Troubleshooting

#### Common Issues:
1. **Import errors**: Ensure all dependencies are installed
2. **Authorization backend not found**: Check config.py setup
3. **PDP timeout**: Implement proper error handling
4. **Test failures**: Verify mock configurations match actual implementations


## Migration Steps

The content comes from this notion doc: https://www.notion.so/PDP-SDK-Integration-Recipes-1c297177520f807790f3c98c36866fa9?source=copy_link#1c297177520f808cbb0bdde13bbb33d1

### 1. Update the project dependencies to the latest owsclient.

- Add the `python-owsclient` dependency to requirements.txt, then rebuild the python environment.
- If the project already has `owsclient` in `requirements.txt`, then change the version to `0.7.0`.

```shell
owsclient==0.7.0
```

Here is an example PR:
* https://github.com/theorchard/ows-track/pull/940


#### After Step 1 Checklist:
- [ ] `owsclient==0.7.0` added to requirements.txt


### 2. Update the project dependencies to the latest python-ows-pdp.

- Add the `python-ows-pdp` dependency to requirements.txt.
- If the project already has `python-pdp-sdk` in `requirements.txt`, then change the version to `5.3.0`.


```shell
python-pdp-sdk==5.3.0
```

Find the latest version from the pypi index: https://pypi.theorchard.io/#/package/python-pdp-sdk

Here is an example PR:
* https://github.com/theorchard/ows-track/pull/944


#### After Step 2 Checklist:
- [ ] `python-pdp-sdk==5.3.0` added to requirements.txt

### 3. Update `config.py` module. Import helpers for getting correlation id and request context from our PDE Flask services:

```python
from owsrequest.ows_client import correlation_id_getter, request_context_getter
```

Instantiate a singleton for OwsClient:

```python
from owsclient import OwsClient

...
def setup_ows_client(environment) -> OwsClient:
    """Set up ows-client."""
    return OwsClient(
        environment=environment,
        service_name=Config.SERVICE_NAME,
        correlation_id_getter=correlation_id_getter,
        request_context_getter=request_context_getter,
    )

...
ows_client = setup_ows_client(Config.ENVIRONMENT)
```

Instantiate singleton for PdpAuthorizationBackend:

```python
from python_pdp_sdk.backends.authorization_backend import (
    AuthorizationBackend,
    PdpAuthorizationBackend,
)
from python_pdp_sdk.connectors.ows_pdp.ows_pdp import OwsPdpClient

...

def setup_authorization_backend(ows_client: OwsClient) -> AuthorizationBackend:
    """Set up Authorization Backend."""
    ows_pdp_client = OwsPdpClient(ows_client=ows_client)
    return PdpAuthorizationBackend(ows_pdp_client)

...
authorization_backend = setup_authorization_backend(ows_client)
```

Here are some example PRs:
* https://github.com/theorchard/ows-payment/pull/375
* https://github.com/theorchard/ows-track/pull/944


#### After Step 3 Checklist:
- [ ] `setup_authorization_backend` function exists.


### 4. Enable Standalone Access Rule Checks

- update the `flask_request.setup()` configuration to stop using the `verify_access_rules` decorator
- Set `verify_access=False` in `flask_request.setup()`
- Set `rules_file=None` in `flask_request.setup()`


Example from ows-account https://github.com/theorchard/ows-account/blob/7b1cb910be8c04cc210a27d05fa02c0777792a11/account/api.py#L44-L59
```python
flask_request.setup(
    app,
    config.ENVIRONMENT,
    add_request_context=True,
    label_profile=True,
    verify_access=False,
    rules_file=None,
    access_log_only=config.ONLY_LOG_ACCESS_ERRORS,
    exclude_paths=[
        config.HEALTH_CHECK,
        '/lookup/vendors/uuids/',
        '/lookup/vendors/vendor-ids/',
        '/lookup/subaccounts/uuids/',
    ],
    uwsgi_cache_enabled=True,
)
```

#### After Step 4 Checklist:
- [ ] `flask_request.setup` has `verify_access=False` .
- [ ] `flask_request.setup` has `rules_file=None` .

### 5. Update each and every endpoint to use verify_rules_access_standalone
Update EACH AND EVERY handler to use `flask_request.verify_rules_access_standalone`. Here's the `verify_rules_access_standalone` function that we want to use: https://github.com/theorchard/python-owsrequest/blob/master/Readme.md#how-to-use-verify_rules_access_standalone

Update the handler functions to use `verify_rules_access_standalone`. Here's an example PR: https://github.com/theorchard/ows-abacus-account/pull/272

Here's an example from ows-abacus-account: https://github.com/theorchard/ows-abacus-account/blob/879b457e83cb3f6f8262102914b1a1ab04dcdce0/abacus_account/blueprints/account.py#L48-L53
```python
class AccountItemView(ItemView):
    """View for finding an account by ID."""

    model_class = Account
    object_detail_schema = AccountDetailSchema()
    put_schema = AccountPutSchema()
    
    def put(self, object_id, **kwargs):
        """Update an account."""
        access_rule_decision = flask_request.verify_rules_access_standalone(request)
        if not access_rule_decision:
            return flaskify(response.create_error_response(
                code=error.ERROR_CODE_AUTHORIZATION,
                message='Unauthorized', status=401,
            ))
        return super().put(object_id, **kwargs)
```


#### After Step 5 Checklist:
- [ ] All Flask handler functions use `flask_request.verify_rules_access_standalone(request)`.

### 6. Create a `authorization` module with pdp-sdk wrapper methods

Create a `utils.authorization` module to hold convenience functions that call python-pdp-sdk.

The `PdpAuthorizationBackend` class expose 3 methods
- `is_authorized`: https://github.com/theorchard/python-pdp-sdk/blob/master/README.md#is_authorized
- `is_authorized_many`: https://github.com/theorchard/python-pdp-sdk/blob/master/README.md#is_authorized_many
- `is_authorized_many_resources_and_actions`: https://github.com/theorchard/python-pdp-sdk/blob/master/README.md#is_authorized_many_resources_and_actions
- `get_authorized_tenants`: https://github.com/theorchard/python-pdp-sdk/blob/master/README.md#get_authorized_tenants

Most applications need utility functions for `is_authorized` and `is_authorized_many`.

The utility functions must be generic to handle authorization calls by handlers.

Here's an `is_authorized` example from ows-abacus-account: https://github.com/theorchard/ows-abacus-account/blob/656dd9f218e6e78d8f5f3d258b6f8f0fc8b8bdcd/abacus_account/utils/authorization.py#L15-L39

```python
from ddtrace import tracer
from flask import g
from python_pdp_sdk import (
    ResourceWithAttributes,
)
from python_pdp_sdk.backends import exceptions
from python_pdp_sdk.resource_getters.base import ForwardKwargsGetter

from abacus_account.config import authorization_backend
from abacus_account.constants import error

@tracer.wrap()
def authorize_resource(
    resource_id: int,
    resource_type: str,
    action: str,
) -> bool:
    """Authorize a resource with empty attributes."""
    authorized = authorization_backend.is_authorized(
        action=action,
        resource_id=resource_id,
        resource_type=resource_type,
        resource_getter=ForwardKwargsGetter(),
    )
    if not authorized:
        g.log.warn(
            error.ERROR_CODE_UNAUTHORIZED_RESOURCE,
            resources={
                'identity_id': g.request_context.jwt_identity_id,
                'resource_id': resource_id,
                'resource_type': resource_type,
                'auth_response': authorized
            }
        )
        return False
    return True
```


If needed, create a method for `authorize_many_accounts`

Here's an example from ows-abacus-account: https://github.com/theorchard/ows-abacus-account/blob/656dd9f218e6e78d8f5f3d258b6f8f0fc8b8bdcd/abacus_account/utils/authorization.py#L42-L97
```python
from ddtrace import tracer
from flask import g
from python_pdp_sdk import (
    ResourceWithAttributes,
)
from python_pdp_sdk.backends import exceptions
from python_pdp_sdk.resource_getters.base import ForwardKwargsGetter

from abacus_account.config import authorization_backend
from abacus_account.constants import error

@tracer.wrap()
def authorize_many_resources(
    resource_ids: list[int],
    resource_type: str,
    tenant_type: str,
    action: str,
) -> bool:
    """Authorize many accounts via."""
    g.log.debug(
        'PP authorize many accounts',
        resources={
            'identity_id': g.request_context.jwt_identity_id,
            'account_ids': resource_ids,
        }
    )
    resources_with_attributes = [
        ResourceWithAttributes(
            resource_id=account_id,
            attributes={
                'tenant': {'tenant_type': resource_type},
                'id_to_uuid_exchange_tenant': {
                    'tenant_type': tenant_type,
                    'tenant_id': account_id,
                }
            },
        )
        for account_id in resource_ids
    ]

    try:
        auth_response = authorization_backend.is_authorized_many(
            action=action,
            resource_type='account',
            resources_with_attributes=resources_with_attributes,
        )
    except exceptions.InvalidRequestException as e:
        g.log.warn(
            'Caught a PDP InvalidRequestException',
            resources={
                'identity_id': g.request_context.jwt_identity_id,
                'resource_ids': resource_ids,
                'resource_type': resource_type,
                'error': str(e),
            }
        )
        return False

    if len(auth_response) != len(resource_ids) or not all(auth_response):
        g.log.warn(
            error.ERROR_CODE_UNAUTHORIZED_MANY_ACCOUNTS,
            resources={
                'identity_id': g.request_context.jwt_identity_id,
                'account_ids': resource_ids,
                'auth_response': auth_response
            }
        )
        return False
    return True
```

#### After Step 6 Checklist:
- [ ] The `utils.authorization` module exists.
- [ ] The `authorize_resource` function exists.
- [ ] The `authorize_many_resources` function exists.
- [ ] Unit tests exist for `authorize_resource` and `authorize_many_resources`.


### 7. Execute PP Authorization Checks

Send auth requests to PDP using ows-pdp-sdk when the standalone check returns `False`. (ex: ows-abacus-account, ows-account)

Example from ows-abacus-account:

```python
class AccountItemView(ItemView):
    """View for finding an account by ID."""

    model_class = Account
    object_detail_schema = AccountDetailSchema()
    put_schema = AccountPutSchema()

    def get(self, object_id, **kwargs):
        """Get account by id."""
        access_rule_decision = flask_request.verify_rules_access_standalone(request)
        if not access_rule_decision:
            authorized = authorize_many_resources([object_id], resource_type="account", tenant_type="account", action="view")
            if not authorized:
                return flaskify(response.create_error_response(
                    code=error.ERROR_CODE_AUTHORIZATION,
                    message='Unauthorized', status=403,
                ))
        return super().get(object_id, **kwargs)
```

#### After Step 7 Checklist:
- [ ] 1 handler uses `authorize_resource` or `authorize_many_resources`

### 8. Write Unit tests

Write a unit test for the updated endpoint. The `profile_type` and mocks will differ for every endpoint and service.
For unit tests, create one (1) test with maximum four (4) `pytest.param` cases. At least 1 case to verify the `403` 
status code is handled properly.

Here's an example from ows-abacus-account: https://github.com/theorchard/ows-abacus-account/blob/656dd9f218e6e78d8f5f3d258b6f8f0fc8b8bdcd/tests/unit/handlers/test_account.py#L163-L266
```python
@pytest.mark.parametrize(
    (
        'profile_type',
        'authorize_return',
        'permissions_return',
        'expected_status',
    ),
    [
        pytest.param(
            'MoneyhubProfile',
            None,
            True,
            200,
            id='Standalone check OK, Permissions check OK'
        ),
        pytest.param(
            'MoneyhubProfile',
            None,
            False,
            403,
            id='Standalone check OK, Permissions check not OK'
        ),
        pytest.param(
            'Account360Profile',
            True,
            True,
            200,
            id='Standalone check not OK, PDP check OK'
        ),
        pytest.param(
            'Account360Profile',
            False,
            True,
            403,
            id='Standalone check not OK, PDP check not OK'
        ),
    ],
)
@patch('abacus_account.blueprints.account.ows_client')
@patch('abacus_account.blueprints.account.permissions_authorize_many_accounts')
@patch('abacus_account.blueprints.account.authorize_many_accounts')
@patch('abacus_account.blueprints.account.dataload_accounts_by_ids')
def test_dataloader_accounts_by_ids(
    mock_get_accounts: MagicMock,
    mock_authorize_many_accounts: MagicMock,
    mock_permissions_authorize_many_accounts: MagicMock,
    mock_ows_client: MagicMock,
    fixture_client: FlaskClient,
    profile_type: str,
    authorize_return: bool | None,
    permissions_return: bool | None,
    expected_status: int,
) -> None:
    """Test for the account dataloader handler."""
    mock_authorize_many_accounts.return_value = authorize_return
    mock_permissions_authorize_many_accounts.return_value = permissions_return
    mock_response = response.Response(
        message={'items': [
            {'data': {
                'account_payee_id': 1,
                'account_name': 'account_name',
                'account_payment_term_id': None,
                'created_by': '',
                'account_id': 10,
                'sap_created_at': None
            }},
            {'data': None},
        ]},
        status=200
    )
    mock_get_accounts.return_value = mock_response

    post_data = [1, 2]
    res = fixture_client.post(
        '/account/dataloader',
        json=post_data,
        headers={
            'Orchard-Requestor-Service': 'graphql-abacus',
            'Orchard-Profile-Type': profile_type,
            'Orchard-Profile-Id': '1234',
            'Orchard-Roles': 'administrator',
            'Orchard-Identity-Id': '1234'
        })

    assert res.status_code == expected_status

    if expected_status == 200:
        assert res.json == mock_response.message
        mock_get_accounts.assert_called_once_with(post_data)
    else:
        mock_get_accounts.assert_not_called()

    if authorize_return is not None:
        mock_authorize_many_accounts.assert_called_once_with([1,2])
    else:
        mock_authorize_many_accounts.assert_not_called()

    if authorize_return is not False:
        mock_permissions_authorize_many_accounts.assert_called_once_with(
            mock_ows_client,
            profile_type,
            '1234',
            [1,2]
        )
```

#### After Step 8 Checklist:
- [ ] 1 unit test exists for the updated handler.

### 9. Write functional tests

Write a functional test for the updated endpoint. The `profile_type` and mocks will differ for every endpoint and service.
For functional tests, create two (1) test with maximum (2) `pytest.param` cases. 1 case for when the `authorization` utility function return `True`, 
and another for `False`.

Here are examples from ows-abacus-account: https://github.com/theorchard/ows-abacus-account/blob/656dd9f218e6e78d8f5f3d258b6f8f0fc8b8bdcd/tests/functional/test_account.py#L629-L677

```python
@pytest.mark.parametrize(
    (
            "authorize_many_accounts_return",
            "expected_status_code"
    ),
    [
        pytest.param(
            True,
            200,
            id="Returns a 200 when authorize_many_accounts returns True",
        ),
        pytest.param(
            False,
            403,
            id="Returns a 403 when authorize_many_accounts returns False",
        ),
    ]
)
@patch('abacus_account.blueprints.account.authorize_many_accounts')
def test_account_dataloader_with_profile(
        mock_authorize_many_accounts: MagicMock,
        fixture_client: FlaskClient,
        authorize_many_accounts_return: bool,
        expected_status_code: int,
) -> None:
    """Test the /account/dataloader endpoint for different feature flag values.

    - If True, then response.status_code should be 200.
    - If False, then response.status_code should be 403.
    """

    mock_authorize_many_accounts.return_value = authorize_many_accounts_return
    res = fixture_client.post(
        '/account/dataloader',
        headers={
            'Content-Type': 'application/json',
            'Orchard-Identity-Id': 'd5ca8ac3-7e51-4793-8775-50d11282504c',
            'Orchard-Profile-Id': '35109',
            'Orchard-Profile-Type': 'NotProfile',
            'Orchard-Roles': "['administrator']",
            'Orchard-Requestor-Service': 'graphql-abacus'
        },
        json=[1, 2])

    assert res.status_code == expected_status_code
```

#### After Step 9 Checklist:
- [ ] 1 functional test exists for the updated handler.
- [ ] Environment rebuilt successfully.
- [ ] No import errors when running the tests.


### 10. Update the development dependencies to the latest jwtauth[testing]
- Let's add the `jwtauth[testing]` extra modules as a development dependency. The dependencies are typically  listed in `requirements-dev.txt`.
- if the project already has `jwtauth[testing` in `requirements-dev.txt`, then change the version to `0.5.3`.


To find the latest version here: https://pypi.theorchard.io/#/package/jwtauth

```shell
jwtauth[testing]==0.5.3
```

#### After Step 10 Checklist:
- [ ] `jwtauth[testing]` added to `requirements-dev.txt`.


### 11. Write integration tests

Write an integration test for the updated endpoint. The integration tests require the developer to create AWS
Secrets Manager secrets. That is out-of-scope for this work.

First, create test fixtures using "JWT Test Tooling guide here": https://github.com/theorchard/python-jwtauth/blob/master/README.md#jwt-test-tooling

```python
from collections.abc import Callable

import pytest

from jwtauth.testing import (
    JwtAuthSecretsManager,
    SecretLookupInfo,
)

pytest_plugins = ["jwtauth.testing.pytest_plugin"]

@pytest.fixture(scope='session')
def basic_headers():
    """Return basic headers."""
    profile_id = '35109'  # profile_id for Joe User, as set in QA cypher refresh script
    return {
        'Content-Type': 'application/json',
        'Orchard-Identity-Id': 'd5ca8ac3-7e51-4793-8775-50d11282504c',
        'Orchard-Profile-Id': profile_id,
        'Orchard-Profile-Type': 'AbacusProfile',
        'Orchard-Roles': 'administrator',
        'Orchard-Requestor-Service': 'graphql-abacus'
    }

@pytest.fixture(scope="session")
def bearer_token_test_user(
    generate_bearer_token: Callable[..., str],
    jwtauth_secrets_manager: JwtAuthSecretsManager,
) -> str:
    """
    Uses the `generate_bearer_token` and `jwtauth_secrets_manager` fixtures from python-jwtauth
    
    Follow the "JWT Test Tooling guide": https://github.com/theorchard/python-jwtauth/blob/master/README.md#jwt-test-tooling
    """
    
    return generate_bearer_token(
        get_user_creds_args=SecretLookupInfo(
            environment="qa",
            service_name="pdp-integration-test",
            secret_name="TODO_ADD_YOUR_USER_CREDENTIALS_SECRET_HERE",
        ),
        get_auth0_creds_args=SecretLookupInfo(
            environment="qa",
            service_name="pdp-integration-test",
            secret_name="TODO_ADD_YOUR_APP_CREDENTIALS_SECRET_HERE",
        ),
        secrets_manager=jwtauth_secrets_manager,
    )

@pytest.fixture(scope="session")
def bearer_token_unauthorized_test_user(
    generate_bearer_token: Callable[..., str],
    jwtauth_secrets_manager: JwtAuthSecretsManager,
) -> str:
    """
    Uses the `generate_bearer_token` and `jwtauth_secrets_manager` fixtures from python-jwtauth
    
    Follow the "JWT Test Tooling guide": https://github.com/theorchard/python-jwtauth/blob/master/README.md#jwt-test-tooling
    """
    
    return generate_bearer_token(
        get_user_creds_args=SecretLookupInfo(
            environment="qa",
            service_name="pdp-integration-test",
            secret_name="TODO_ADD_YOUR_UNAUTHORIZED_USER_CREDENTIALS_SECRET_HERE",
        ),
        get_auth0_creds_args=SecretLookupInfo(
            environment="qa",
            service_name="pdp-integration-test",
            secret_name="TODO_ADD_YOUR_APP_CREDENTIALS_SECRET_HERE",
        ),
        secrets_manager=jwtauth_secrets_manager,
    )

@pytest.fixture()
def unauthorized_headers(bearer_token_unauthorized_test_user):
    """Return headers for unauthorized user."""
    return {
        'Authorization': f'Bearer {bearer_token_unauthorized_test_user}',
        'Orchard-Requestor-Service': 'graphql-abacus'
    }

@pytest.fixture(params=['read_only', 'admin'])
def auth_headers(request, bearer_token_test_user, basic_headers):
    """Return appropriate headers based on the parameter."""
    if request.param == 'read_only':
        return {
            'Authorization': f'Bearer {bearer_token_test_user}',
            'Orchard-Requestor-Service': 'graphql-abacus'
        }
    elif request.param == 'admin':
        return basic_headers
```

Here are example integration tests from ows-abacus-account: https://github.com/theorchard/ows-abacus-account/blob/656dd9f218e6e78d8f5f3d258b6f8f0fc8b8bdcd/tests/integration/test_account.py#L8-L67

```python
import pytest
import requests
import os

QA_BASE_URL = os.environ.get(
    'QA_BASE_URL', 'http://localhost:6452')

@pytest.mark.jira('TICKET-NAME')
def test_accounts_dataloader(auth_headers, insert_account_payment_terms):
    """Test the /account/dataloader endpoint with different auth headers."""
    response = requests.post(
        f'{QA_BASE_URL}/account/dataloader',
        json=[83418, 66289, 45233],
        headers=auth_headers
    )
    assert response.status_code == 200
    expected = {
        'items': [
            {
                'data': {
                    'account_id': 83418,
                    'account_name': '!nertia',
                    'created_by': '',
                    'sap_created_at': '2024-01-22T12:14:40.000000',
                    'account_payee_id': 75320,
                    'account_payment_term_id': 108137
                }
            },
            {
                'data': {
                    'account_id': 66289,
                    'account_name': '#ERROR!',
                    'created_by': 'sax_ingestion',
                    'sap_created_at': '2022-12-07T18:41:35.000000',
                    'account_payee_id': 30895,
                    'account_payment_term_id': 30895
                }
            },
            {
                'data': {
                    'account_id': 45233,
                    'account_name': '#NAME?',
                    'created_by': 'sax_ingestion',
                    'sap_created_at': '2022-12-05T18:26:42.000000',
                    'account_payee_id': 9841,
                    'account_payment_term_id': 9841
                }
            }
        ]
    }
    assert response.json() == expected


@pytest.mark.jira('TICKET-NAME')
def test_accounts_dataloader_unauthorized(unauthorized_headers):
    """Test the POST /account/dataloader endpoint with unauthorized headers."""
    response = requests.post(
        f'{QA_BASE_URL}/account/dataloader',
        json=[83418, 66289, 45233],
        headers=unauthorized_headers
    )
    assert response.status_code == 403
    expected = {
        'code': 'authorization_error',
        'message': 'Unauthorized'
    }
    assert response.json() == expected
```

### After Step 10 Checklist:
- [ ] `integration/conftest.py` uses the `jwtauth[testing]` fixtures.
- [ ] 1 integration tests exists that verifies that an authorized requests returns a 200 http status code.
- [ ] 1 integration tests exists that verifies that an unauthorized requests returns a 200 http status code.

### 11. Prompt the developer to create atomic PRs from the Agent change

- Remind the developer to double-check that the AI Agent completed every step.
- Remind the developer to create atomic PRs from the AI Agent changes.


The AI Agent changes are for reference only. You should now create atomic PRs for each step:
* PR 1: Add dependencies (Steps 1-2)
* PR 2: Update config.py (Step 3)
* PR 3: Enable standalone checks for ALL endpoint handlers.(Step 4)
* PR 4: Create authorization utility (Step 6)
* PR 5: Update handler with PP checks (Steps 5 & 7)
* PR 6: Add unit tests (Step 8)
* PR 7: Add functional tests (Step 9)
* PR 8: Add integration test dependencies and tests (Steps 10)

# Now let's get started.

Now the developer will ask for assistance migrating 1 endpoint. Use the `Migration Steps` as guidance to
implement the change.


