Skip to content

Commit

Permalink
✨ improve acapy exception handling (#1142)
Browse files Browse the repository at this point in the history
* 🔧 set default fixture scope for pytest-asyncio

* 🚧 test acapy runtime warning changes (includes logging changes)

* 🚧

* 🚧 test `refactor-multitenant-manager-errors`

* 🧪 test acapy auth and assert invalid tokens are unauthorized

* ✅ fix up auth tests

* 🚧 test exception handling changes

* 🎨 sleep to allow record to update

* 🧪 skip auth tests in regression mode

* ✅

* ⬆️ Update lock files
  • Loading branch information
ff137 authored Oct 31, 2024
1 parent 5d2e29c commit 6881517
Show file tree
Hide file tree
Showing 9 changed files with 652 additions and 499 deletions.
288 changes: 144 additions & 144 deletions app/poetry.lock

Large diffs are not rendered by default.

152 changes: 152 additions & 0 deletions app/tests/e2e/test_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import asyncio
from uuid import uuid4

import pytest
from aries_cloudcontroller import AcaPyClient
from aries_cloudcontroller.exceptions import UnauthorizedException

from app.routes.admin.tenants import router as tenants_router
from app.routes.connections import router as connections_router
from app.tests.util.regression_testing import TestMode
from shared.constants import TENANT_FASTAPI_ENDPOINT
from shared.util.rich_async_client import RichAsyncClient

CONNECTIONS_BASE_PATH = connections_router.prefix
TENANTS_BASE_PATH = tenants_router.prefix

group_id = "TestGroup"

skip_regression_test_reason = "Don't need to cover auth tests in regression mode"


@pytest.mark.anyio
@pytest.mark.skipif(
TestMode.regression_run in TestMode.fixture_params,
reason=skip_regression_test_reason,
)
@pytest.mark.anyio
async def test_invalid_acapy_auth_header(alice_acapy_client: AcaPyClient):
# Modify the AcaPyClient to use the wrong header -- assert ACA-Py auth error
existing_token = alice_acapy_client.api_client.default_headers[
"Authorization"
].split(" ")[1]

alice_acapy_client.api_client.default_headers["Authorization"] = (
f"Barer {existing_token}"
)

with pytest.raises(UnauthorizedException) as exc_info:
await alice_acapy_client.connection.get_connections()
assert exc_info.value.status == 401
assert "Invalid Authorization header structure" == str(exc_info.value.reason)


@pytest.mark.anyio
@pytest.mark.skipif(
TestMode.regression_run in TestMode.fixture_params,
reason=skip_regression_test_reason,
)
async def test_jwt_invalid_token_error(tenant_admin_client: RichAsyncClient):
# Step 1: Create a tenant and get a valid access token
response = await tenant_admin_client.post(
TENANTS_BASE_PATH,
json={
"image_url": "https://image.example.com",
"wallet_label": uuid4().hex,
"group_id": group_id,
},
)
assert response.status_code == 200
tenant = response.json()
wallet_id = tenant["wallet_id"]
valid_token = tenant["access_token"].split("tenant.")[1]

try:
# Step 2: Corrupt the token
# Split the token into header, payload, and signature
token_parts = valid_token.split(".")
assert len(token_parts) == 3, "Invalid JWT token format"

# Corrupt the payload by altering a character
corrupted_payload = token_parts[1][:-1] + (
"A" if token_parts[1][-1] != "A" else "B"
)
corrupted_token = ".".join([token_parts[0], corrupted_payload, token_parts[2]])

# Step 3: Attempt to use the corrupted token
tenant_client = RichAsyncClient(
base_url=TENANT_FASTAPI_ENDPOINT, raise_status_error=False
)
tenant_client.headers["x-api-key"] = f"tenant.{corrupted_token}"

# Make a request to a protected endpoint
response = await tenant_client.get(CONNECTIONS_BASE_PATH)

# Step 4: Assert that the response is 401 Unauthorized
assert response.status_code == 401
assert response.json()["detail"] == "Unauthorized"

finally:
# Cleanup: Delete the created tenant
delete_response = await tenant_admin_client.delete(
f"{TENANTS_BASE_PATH}/{wallet_id}"
)
assert delete_response.status_code == 200


@pytest.mark.anyio
@pytest.mark.skipif(
TestMode.regression_run in TestMode.fixture_params,
reason=skip_regression_test_reason,
)
async def test_invalid_token_error_after_rotation(tenant_admin_client: RichAsyncClient):
# Step 1: Create a tenant and get a valid access token
response = await tenant_admin_client.post(
TENANTS_BASE_PATH,
json={
"image_url": "https://image.example.com",
"wallet_label": uuid4().hex,
"group_id": group_id,
},
)
assert response.status_code == 200
tenant = response.json()
wallet_id = tenant["wallet_id"]
old_token = tenant["access_token"]

# Prepare the tenant client and set header to original token
tenant_client = RichAsyncClient(
base_url=TENANT_FASTAPI_ENDPOINT, raise_status_error=False
)
tenant_client.headers["x-api-key"] = old_token

try:
# Step 2: Rotate the token once
rotate_response = await tenant_admin_client.get(
f"{TENANTS_BASE_PATH}/{wallet_id}/access-token?group_id={group_id}"
)
assert rotate_response.status_code == 200

await asyncio.sleep(1) # short sleep, because this test is very flaky

# Rotate the token again
rotate_response = await tenant_admin_client.get(
f"{TENANTS_BASE_PATH}/{wallet_id}/access-token?group_id={group_id}"
)
assert rotate_response.status_code == 200

await asyncio.sleep(4) # sleep, allow for storage record to update

# Step 3: Attempt to use the first token after token rotated
response = await tenant_client.get(CONNECTIONS_BASE_PATH)

# Step 4: Assert that the response is 401 Unauthorized
assert response.status_code == 401
assert response.json()["detail"] == "Token not valid."

finally:
# Cleanup: Delete the created tenant
delete_response = await tenant_admin_client.delete(
f"{TENANTS_BASE_PATH}/{wallet_id}"
)
assert delete_response.status_code == 200
2 changes: 1 addition & 1 deletion dockerfiles/agents/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM ghcr.io/didx-xyz/acapy-agent-bbs:py3.12-1.1.1b1
FROM ghcr.io/didx-xyz/acapy-agent-bbs:py3.12-1.1.1b3

COPY configuration ./configuration
COPY scripts ./scripts
4 changes: 2 additions & 2 deletions dockerfiles/agents/Dockerfile.agent
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM ghcr.io/didx-xyz/acapy-agent-bbs:py3.12-1.1.1b1
FROM ghcr.io/didx-xyz/acapy-agent-bbs:py3.12-1.1.1b3

USER root

Expand All @@ -7,7 +7,7 @@ ARG PROTOBUF_VERSION=5.28.1
RUN pip install --no-cache-dir protobuf==${PROTOBUF_VERSION}

# install nats-events plugin
RUN pip install --no-cache-dir git+https://github.com/didx-xyz/aries-acapy-plugins@fix/3306#subdirectory=nats_events
RUN pip install --no-cache-dir git+https://github.com/didx-xyz/aries-acapy-plugins@test-refactor-multitenant-manager-errors#subdirectory=nats_events

COPY scripts/startup.sh startup.sh
RUN chmod +x ./startup.sh
Expand Down
6 changes: 3 additions & 3 deletions dockerfiles/agents/Dockerfile.author.agent
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM ghcr.io/didx-xyz/acapy-agent-bbs:py3.12-1.1.1b1
FROM ghcr.io/didx-xyz/acapy-agent-bbs:py3.12-1.1.1b3

USER root

Expand All @@ -7,10 +7,10 @@ ARG PROTOBUF_VERSION=5.28.1
RUN pip install --no-cache-dir protobuf==${PROTOBUF_VERSION}

# Install wallet group id plugin
RUN pip install --no-cache-dir git+https://github.com/didx-xyz/acapy-wallet-groups-plugin@fix/3306
RUN pip install --no-cache-dir git+https://github.com/didx-xyz/acapy-wallet-groups-plugin@test-refactor-multitenant-manager-errors

# install nats-events plugin
RUN pip install --no-cache-dir git+https://github.com/didx-xyz/aries-acapy-plugins@fix/3306#subdirectory=nats_events
RUN pip install --no-cache-dir git+https://github.com/didx-xyz/aries-acapy-plugins@test-refactor-multitenant-manager-errors#subdirectory=nats_events

COPY scripts/startup.sh startup.sh
RUN chmod +x ./startup.sh
Expand Down
Loading

0 comments on commit 6881517

Please sign in to comment.