Skip to content

Commit

Permalink
Feat/split apis (#593)
Browse files Browse the repository at this point in the history
* add support to split api by role

* split apis by role

* add trust-registry split api

* remove extra info for apis that don't need it

* remove trust registry from other apis as it is now a standalone instance

* Add new fastapi components to build pipelines

* Rename build step for legibility

* Ledger nodes healthcheck timeout 10s

* fix governance-trust-registry-web typo

* Test governance splitapi

* split tenant-admin

* split tenant

* Test hardcoded base_url

* Trust Registry Base URL

* Updated comment

* charts ref

* remove test role

* codacy improvements

* Code style check fixes

* ROOT_PATH env var

* Strip prefix for multitenant and trust-registry to simplify path based routing in EKS

* path is now /tenant and URL is /admin

* 🎨 rename unauthed_client to trust_registry_client

* 🎨 organise docs descriptions and deduplicate

* 🎨 deduplicate routes per role

* 🔥 remove unused env var

* Merge charts

---------

Co-authored-by: lohanspies <[email protected]>
Co-authored-by: ff137 <[email protected]>
  • Loading branch information
3 people authored Jan 23, 2024
1 parent 604f390 commit cc6e7d3
Show file tree
Hide file tree
Showing 18 changed files with 264 additions and 75 deletions.
16 changes: 14 additions & 2 deletions .github/workflows/continuous-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ on:

jobs:
build:
if: github.event.pull_request.draft == false
name: Build images and push to GHCR
if: ${{ !github.event.pull_request.draft }}
name: Build GHCR
permissions:
id-token: write # This is required for requesting the JWT
packages: write # To push to GHCR.io
Expand All @@ -54,6 +54,9 @@ jobs:
governance-ga-agent,
governance-trust-registry,
governance-multitenant-web,
governance-ga-web,
governance-tenant-web,
governance-trust-registry-web,
governance-webhooks-web,
governance-multitenant-agent,
governance-endorser,
Expand All @@ -69,6 +72,15 @@ jobs:
- image: governance-multitenant-web
context: .
file: dockerfiles/fastapi/Dockerfile
- image: governance-ga-web
context: .
file: dockerfiles/fastapi/Dockerfile
- image: governance-tenant-web
context: .
file: dockerfiles/fastapi/Dockerfile
- image: governance-trust-registry-web
context: .
file: dockerfiles/fastapi/Dockerfile
- image: governance-webhooks-web
context: .
file: dockerfiles/webhooks/Dockerfile
Expand Down
98 changes: 64 additions & 34 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,32 @@

OPENAPI_NAME = os.getenv("OPENAPI_NAME", "OpenAPI")
PROJECT_VERSION = os.getenv("PROJECT_VERSION", "0.11.0")
ROLE = os.getenv("ROLE", "*")
ROOT_PATH = os.getenv("ROOT_PATH", "")

cloud_api_docs_description = """
Welcome to the Aries CloudAPI Python project.
In addition to the traditional HTTP-based endpoints described below, we also offer WebSocket endpoints for real-time interfacing with webhook events.
WebSocket endpoints are authenticated. This means that only users with valid authentication tokens can establish a WebSocket connection, and they can only subscribe to their own wallet's events. However, Admin users have the ability to subscribe by topic, or to any wallet.
Our WebSocket endpoints are as follows:
1. `/ws/topic/{topic}`: (Admin only) This endpoint allows admins to receive all webhook events on a specific topic (e.g. `connections`, `credentials`, `proofs`, `endorsements`).
2. `/ws/{wallet_id}`: This endpoint allows authenticated users to receive webhook events associated with a specific wallet ID.
3. `/ws/{wallet_id}/{topic}`: Similar to above, but with topic-specific subscription.
For authentication, the WebSocket headers should include `x-api-key`: `<your key>`.
Please refer to our API documentation for more details about our authentication mechanism, as well as for information about the available topics.
"""

default_docs_description = """
Welcome to the Aries CloudAPI Python project.
"""

logger = get_logger(__name__)
prod = strtobool(os.environ.get("prod", "True"))
Expand All @@ -48,50 +74,54 @@ async def lifespan(_: FastAPI):
await WebsocketManager.disconnect_all()


def create_app() -> FastAPI:
routes = [
tenants,
connections,
definitions,
issuer,
jsonld,
messaging,
oob,
trust_registry,
verifier,
wallet_credentials,
wallet_dids,
webhooks,
sse,
]

application = FastAPI(
debug=debug,
title=OPENAPI_NAME,
description="""
Welcome to the Aries CloudAPI Python project.
In addition to the traditional HTTP-based endpoints described below, we also offer WebSocket endpoints for real-time interfacing with webhook events.
WebSocket endpoints are authenticated. This means that only users with valid authentication tokens can establish a WebSocket connection, and they can only subscribe to their own wallet's events. However, Admin users have the ability to subscribe by topic, or to any wallet.
trust_registry_routes = [trust_registry]
tenant_admin_routes = [tenants]
tenant_routes = [
connections,
definitions,
issuer,
jsonld,
messaging,
oob,
verifier,
wallet_credentials,
wallet_dids,
webhooks,
sse,
]

Our WebSocket endpoints are as follows:

1. `/ws/topic/{topic}`: (Admin only) This endpoint allows admins to receive all webhook events on a specific topic (e.g. `connections`, `credentials`, `proofs`, `endorsements`).
def routes_for_role(role: str) -> list:
if role in ("governance", "tenant"):
return tenant_routes
elif ROLE == "tenant-admin":
return tenant_admin_routes
elif ROLE == "trust-registry":
return trust_registry_routes
elif ROLE == "*":
return tenant_admin_routes + tenant_routes + trust_registry_routes
else:
return []

2. `/ws/{wallet_id}`: This endpoint allows authenticated users to receive webhook events associated with a specific wallet ID.

3. `/ws/{wallet_id}/{topic}`: Similar to above, but with topic-specific subscription.
def cloud_api_description(role: str) -> str:
if role in ("governance", "tenant", "*"):
return cloud_api_docs_description
else:
return default_docs_description

For authentication, the WebSocket headers should include `x-api-key`: `<your key>`.

Please refer to our API documentation for more details about our authentication mechanism, as well as for information about the available topics.
""",
def create_app() -> FastAPI:
application = FastAPI(
root_path=ROOT_PATH,
title=OPENAPI_NAME,
version=PROJECT_VERSION,
description=cloud_api_description(ROLE),
lifespan=lifespan,
debug=debug,
)

for route in routes:
for route in routes_for_role(ROLE):
# Routes will appear in the openapi docs with the same order as defined in `routes`
application.include_router(route.router)

Expand Down
2 changes: 1 addition & 1 deletion app/routes/admin/tenants.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@

logger = get_logger(__name__)

router = APIRouter(prefix="/admin/tenants", tags=["admin: tenants"])
router = APIRouter(prefix="/tenants", tags=["admin: tenants"])


@router.post("", response_model=CreateTenantResponse)
Expand Down
2 changes: 1 addition & 1 deletion app/routes/trust_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

logger = get_logger(__name__)

router = APIRouter(prefix="/trust-registry", tags=["trust-registry"])
router = APIRouter(tags=["trust-registry"])


@router.get("/schemas", response_model=List[Schema])
Expand Down
2 changes: 1 addition & 1 deletion app/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
governance_client,
meld_co_client,
tenant_admin_client,
unauthed_client,
trust_registry_client,
)
from app.tests.util.member_wallets import (
acme_verifier,
Expand Down
4 changes: 2 additions & 2 deletions app/tests/e2e/test_exception_handler.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytest

from app.routes.connections import router
from shared import CLOUDAPI_URL
from shared import TENANT_FASTAPI_ENDPOINT
from shared.util.rich_async_client import RichAsyncClient

CONNECTIONS_BASE_PATH = router.prefix
Expand All @@ -10,7 +10,7 @@
@pytest.mark.anyio
async def test_error_handler():
async with RichAsyncClient(
base_url=CLOUDAPI_URL, raise_status_error=False
base_url=TENANT_FASTAPI_ENDPOINT, raise_status_error=False
) as client:
response = await client.get(CONNECTIONS_BASE_PATH)
assert response.status_code == 403
Expand Down
42 changes: 23 additions & 19 deletions app/tests/e2e/test_trust_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
async def test_get_schemas(
schema_definition: CredentialSchema, # pylint: disable=unused-argument
schema_definition_alt: CredentialSchema, # pylint: disable=unused-argument
unauthed_client: RichAsyncClient,
trust_registry_client: RichAsyncClient,
):
schemas_response = await unauthed_client.get(
schemas_response = await trust_registry_client.get(
f"{CLOUDAPI_TRUST_REGISTRY_PATH}/schemas"
)

Expand All @@ -28,9 +28,9 @@ async def test_get_schemas(

@pytest.mark.anyio
async def test_get_schema_by_id(
schema_definition: CredentialSchema, unauthed_client: RichAsyncClient
schema_definition: CredentialSchema, trust_registry_client: RichAsyncClient
):
schema_response = await unauthed_client.get(
schema_response = await trust_registry_client.get(
f"{CLOUDAPI_TRUST_REGISTRY_PATH}/schemas/{schema_definition.id}"
)

Expand All @@ -39,7 +39,7 @@ async def test_get_schema_by_id(
assert_that(schema).contains("did", "name", "version", "id")

with pytest.raises(HTTPException) as exc:
schema_response = await unauthed_client.get(
schema_response = await trust_registry_client.get(
f"{CLOUDAPI_TRUST_REGISTRY_PATH}/schemas/bad_schema_id"
)

Expand All @@ -50,14 +50,16 @@ async def test_get_schema_by_id(
async def test_get_actors(
faber_issuer: CreateTenantResponse,
faber_acapy_client: AcaPyClient,
unauthed_client: RichAsyncClient,
trust_registry_client: RichAsyncClient,
):
all_actors = await unauthed_client.get(f"{CLOUDAPI_TRUST_REGISTRY_PATH}/actors")
all_actors = await trust_registry_client.get(
f"{CLOUDAPI_TRUST_REGISTRY_PATH}/actors"
)
assert all_actors.status_code == 200
actors = all_actors.json()
assert_that(actors[0]).contains("id", "name", "roles", "did", "didcomm_invitation")

actors_by_id = await unauthed_client.get(
actors_by_id = await trust_registry_client.get(
f"{CLOUDAPI_TRUST_REGISTRY_PATH}/actors?actor_id={faber_issuer.wallet_id}"
)
assert actors_by_id.status_code == 200
Expand All @@ -71,15 +73,15 @@ async def test_get_actors(
assert actor_name == faber_issuer.wallet_label
assert_that(actor).contains("id", "name", "roles", "did", "didcomm_invitation")

actors_by_did = await unauthed_client.get(
actors_by_did = await trust_registry_client.get(
f"{CLOUDAPI_TRUST_REGISTRY_PATH}/actors?actor_did={actor_did}"
)
assert actors_by_did.status_code == 200
assert_that(actors_by_did.json()[0]).contains(
"id", "name", "roles", "did", "didcomm_invitation"
)

actors_by_name = await unauthed_client.get(
actors_by_name = await trust_registry_client.get(
f"{CLOUDAPI_TRUST_REGISTRY_PATH}/actors?actor_name={faber_issuer.wallet_label}"
)
assert actors_by_name.status_code == 200
Expand All @@ -89,27 +91,27 @@ async def test_get_actors(


@pytest.mark.anyio
async def test_get_actors_x(unauthed_client: RichAsyncClient):
async def test_get_actors_x(trust_registry_client: RichAsyncClient):
with pytest.raises(HTTPException) as exc:
await unauthed_client.get(
await trust_registry_client.get(
f"{CLOUDAPI_TRUST_REGISTRY_PATH}/actors?actor_name=Bad_actor_name"
)
assert exc.value.status_code == 404

with pytest.raises(HTTPException) as exc:
await unauthed_client.get(
await trust_registry_client.get(
f"{CLOUDAPI_TRUST_REGISTRY_PATH}/actors?actor_id=Bad_actor_id"
)
assert exc.value.status_code == 404

with pytest.raises(HTTPException) as exc:
await unauthed_client.get(
await trust_registry_client.get(
f"{CLOUDAPI_TRUST_REGISTRY_PATH}/actors?actor_did=Bad_actor_did"
)
assert exc.value.status_code == 404

with pytest.raises(HTTPException) as exc:
await unauthed_client.get(
await trust_registry_client.get(
f"{CLOUDAPI_TRUST_REGISTRY_PATH}/actors?actor_did=Bad&actor_id=Request"
)
assert exc.value.status_code == 400
Expand All @@ -118,18 +120,20 @@ async def test_get_actors_x(unauthed_client: RichAsyncClient):
@pytest.mark.anyio
async def test_get_issuers(
faber_issuer: CreateTenantResponse, # pylint: disable=unused-argument
unauthed_client: RichAsyncClient,
trust_registry_client: RichAsyncClient,
):
actors = await unauthed_client.get(f"{CLOUDAPI_TRUST_REGISTRY_PATH}/actors/issuers")
actors = await trust_registry_client.get(
f"{CLOUDAPI_TRUST_REGISTRY_PATH}/actors/issuers"
)
assert actors.status_code == 200


@pytest.mark.anyio
async def test_get_verifiers(
acme_verifier: CreateTenantResponse, # pylint: disable=unused-argument
unauthed_client: RichAsyncClient,
trust_registry_client: RichAsyncClient,
):
actors = await unauthed_client.get(
actors = await trust_registry_client.get(
f"{CLOUDAPI_TRUST_REGISTRY_PATH}/actors/verifiers"
)
assert actors.status_code == 200
2 changes: 1 addition & 1 deletion app/tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ def test_create_app():

# Verifying that all routes are included
routes = [route.path for route in app.routes]
expected_routes = ["/openapi.json", "/admin/tenants"]
expected_routes = ["/openapi.json", "/tenants"]
for route in expected_routes:
assert route in routes
3 changes: 2 additions & 1 deletion app/tests/util/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
GOVERNANCE_AGENT_URL,
GOVERNANCE_FASTAPI_ENDPOINT,
TENANT_ACAPY_API_KEY,
TENANT_ADMIN_FASTAPI_ENDPOINT,
TENANT_AGENT_URL,
TENANT_FASTAPI_ENDPOINT,
TEST_CLIENT_TIMEOUT,
Expand Down Expand Up @@ -43,7 +44,7 @@ def get_governance_acapy_client() -> AcaPyClient:
def get_tenant_admin_client(*, app: Optional[Any] = None) -> RichAsyncClient:
settings = get_common_settings(f"tenant-admin.{TENANT_ACAPY_API_KEY}", app)
return RichAsyncClient(
base_url=TENANT_FASTAPI_ENDPOINT, name="Tenant Admin", **settings
base_url=TENANT_ADMIN_FASTAPI_ENDPOINT, name="Tenant Admin", **settings
)


Expand Down
6 changes: 3 additions & 3 deletions app/tests/util/member_async_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
get_tenant_client,
)
from shared import RichAsyncClient
from shared.constants import CLOUDAPI_URL
from shared.constants import TRUST_REGISTRY_FASTAPI_ENDPOINT


@pytest.fixture
async def unauthed_client() -> RichAsyncClient:
async with RichAsyncClient(base_url=CLOUDAPI_URL) as client:
async def trust_registry_client() -> RichAsyncClient:
async with RichAsyncClient(base_url=TRUST_REGISTRY_FASTAPI_ENDPOINT) as client:
yield client


Expand Down
Loading

0 comments on commit cc6e7d3

Please sign in to comment.