Skip to content

Commit

Permalink
📝 💥Swagger tenant admin (#824)
Browse files Browse the repository at this point in the history
* Update docstrings for tenant endpoints

* formatting

* update docstrings

* update dacstrings

* add response model

* exclude group_id

* update create docstrings

* update delete docstrings

* add patch endpoint for access token

* update access-token docstrings

* update update Tenant docstrings

* update get by id docstrings

* update fetch all

* 🎨

* undo exclude group_id

* assert 204

* assert 204

* edits

* 🎨

* exclude group_id

* fix tests

* remove group_id and typo

* remove group_id

* 🎨

* undo remove of group_id

* change to post

* re-add group_id asserts

* 🎨

* assert group_id

* assert group_id

* 🎨 Re-add group_id to response body in docstrings

* 🎨

* 🎨 Expand docstring descriptions

* ✅ Unit test coverage for newly duplicated post access token method

* 🧪 Update expected status code to 204 in k6 scripts, for deleting tenants

---------

Co-authored-by: ff137 <[email protected]>
Co-authored-by: cl0ete <[email protected]>
  • Loading branch information
cl0ete and ff137 authored Nov 20, 2024
1 parent 8077903 commit 862518b
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 35 deletions.
253 changes: 241 additions & 12 deletions app/routes/admin/tenants.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,65 @@
)


@router.post("", response_model=CreateTenantResponse)
@router.post("", response_model=CreateTenantResponse, summary="Create New Tenant")
async def create_tenant(
body: CreateTenantRequest,
admin_auth: AcaPyAuthVerified = Depends(acapy_auth_tenant_admin),
) -> CreateTenantResponse:
"""Create a new tenant."""
"""
Create a New Tenant
---
Use this endpoint to create a new Tenant, which is the same as creating a new Wallet.
The `wallet_label` is a required field that allows you to assign an alias to the Tenant.
This label is used as the default alias in connections (i.e. the other party will see this name in their records).
If roles (issuer and/or verifier) are specified, the Tenant will be onboarded with these roles and added to the
trust registry. An issuer or a verifier is referred to as an 'actor' in the trust registry, and the `wallet_label`
will be used as the name for this actor.
If no roles are provided, then the created wallet represents a regular tenant
(also referred to as a 'user', 'wallet', 'holder', or 'prover', depending on the context).
The `wallet_label` does not have to be unique for regular tenants, but it may not match the name of an existing
actor in the trust registry, and will be blocked. Conversely, an issuer or verifier may not use a label that is
already in use by another tenant, and will also be blocked.
The `wallet_name` is an optional field that allows you to assign an additional identifier to the wallet, and is
useful with `get_tenants` to fetch Wallets by name. Wallet names must be unique.
The `image_url` is an optional field for assigning an image to the Wallet. For actors, this will also be added to
the trust registry.
`extra_settings` is an optional field intended for advanced users, which allows configuring wallet behaviour.
Request body:
---
body: CreateTenantRequest
wallet_label: str
A required alias for the Tenant.
wallet_name: Optional[str]
An optional wallet name.
roles: Optional[List[str]]
A list of roles to assign to the Tenant.
image_url: Optional[str]
An optional image URL for the Tenant.
extra_settings: Optional[dict]]
Optional per-tenant settings to configure wallet behaviour for advanced users.
Response body:
---
CreateTenantResponse
wallet_id: str
wallet_label: str
wallet_name: str
created_at: str
image_url: Optional[str]
group_id: Optional[str]
updated_at: str
access_token: str
"""
bound_logger = logger.bind(body=body)
bound_logger.debug("POST request received: Starting tenant creation")

Expand Down Expand Up @@ -187,13 +240,29 @@ async def create_tenant(
return response


@router.delete("/{wallet_id}")
@router.delete("/{wallet_id}", summary="Delete a Tenant by Wallet ID", status_code=204)
async def delete_tenant_by_id(
wallet_id: str,
group_id: Optional[str] = group_id_query,
admin_auth: AcaPyAuthVerified = Depends(acapy_auth_tenant_admin),
):
"""Delete tenant by id."""
) -> None:
"""
Delete Tenant by ID
---
Use this endpoint to delete a Tenant by its Wallet ID. This action will remove the Tenant's Wallet,
along with any associated credentials, connections, and other data. If the tenant is an issuer or verifier,
they will also be removed from the trust registry.
Request Parameters:
---
wallet_id: str
The Wallet ID of the Tenant to delete.
Response body:
---
status_code: 204 No Content
"""
bound_logger = logger.bind(body={"wallet_id": wallet_id})
bound_logger.debug("DELETE request received: Deleting tenant by id")

Expand Down Expand Up @@ -226,12 +295,85 @@ async def delete_tenant_by_id(
bound_logger.debug("Successfully deleted tenant.")


@router.get("/{wallet_id}/access-token", response_model=TenantAuth)
@router.get(
"/{wallet_id}/access-token",
response_model=TenantAuth,
summary="(Deprecated) Rotate and Get New Access Token for Wallet",
deprecated=True,
)
async def get_wallet_auth_token(
wallet_id: str,
group_id: Optional[str] = group_id_query,
admin_auth: AcaPyAuthVerified = Depends(acapy_auth_tenant_admin),
) -> TenantAuth:
"""
(Deprecated) Rotate and get access token for Wallet
---
Calling this endpoint will invalidate the previous access token for the Wallet, and return a new one.
Request Parameters:
---
wallet_id: str
The Wallet ID of the tenant for which the access token will be rotated.
Response Body:
---
TenantAuth
access_token: str
The new access token for the Wallet.
"""
bound_logger = logger.bind(body={"wallet_id": wallet_id})
bound_logger.debug("GET request received: Access token for tenant")

async with get_tenant_admin_controller(admin_auth) as admin_controller:
await get_wallet_and_assert_valid_group(
admin_controller=admin_controller,
wallet_id=wallet_id,
group_id=group_id,
logger=bound_logger,
)

bound_logger.debug("Getting auth token for wallet")
response = await handle_acapy_call(
logger=bound_logger,
acapy_call=admin_controller.multitenancy.get_auth_token,
wallet_id=wallet_id,
body=CreateWalletTokenRequest(),
)

response = TenantAuth(access_token=tenant_api_key(response.token))
bound_logger.debug("Successfully retrieved access token.")
return response


@router.post(
"/{wallet_id}/access-token",
response_model=TenantAuth,
summary="Rotate and Get New Access Token for Wallet",
)
async def post_wallet_auth_token(
wallet_id: str,
group_id: Optional[str] = group_id_query,
admin_auth: AcaPyAuthVerified = Depends(acapy_auth_tenant_admin),
) -> TenantAuth:
"""
Rotate and get new access token for Wallet
---
Calling this endpoint will invalidate the previous access token for the Wallet, and return a new one.
Request Parameters:
---
wallet_id: str
The Wallet ID of the tenant for which the access token will be rotated.
Response Body:
---
TenantAuth
access_token: str
The new access token for the Wallet.
"""
bound_logger = logger.bind(body={"wallet_id": wallet_id})
bound_logger.debug("GET request received: Access token for tenant")

Expand All @@ -256,14 +398,49 @@ async def get_wallet_auth_token(
return response


@router.put("/{wallet_id}", response_model=Tenant)
@router.put("/{wallet_id}", response_model=Tenant, summary="Update Tenant by Wallet ID")
async def update_tenant(
wallet_id: str,
body: UpdateTenantRequest,
group_id: Optional[str] = group_id_query,
admin_auth: AcaPyAuthVerified = Depends(acapy_auth_tenant_admin),
) -> Tenant:
"""Update tenant by id."""
"""
Update Tenant by Wallet ID
---
Update a Tenant's details based on their Wallet ID.
Issuers or verifiers can be promoted to hold both roles, but this endpoint does not support revoking roles.
Holders cannot have their roles updated. Attempting to assign issuer or verifier
roles to a holder will result in a 409 conflict error.
Updates to `wallet_label` or `image_url` for issuers and verifiers will be reflected on the trust registry.
Request body:
---
body: UpdateTenantRequest
wallet_label: Optional[str]
An optional alias for the Tenant.
roles: Optional[List[str]]
A list of roles to assign to the Tenant.
image_url: Optional[str]
An optional image URL for the Tenant.
extra_settings: Optional[Dict[str, Union[bool, str]]]
Optional per-tenant settings to configure wallet behaviour for advanced users.
Response body:
---
Tenant
wallet_id: str
wallet_label: str
wallet_name: str
created_at: str
updated_at: Optional[str]
image_url: Optional[str]
group_id: Optional[str]
"""
bound_logger = logger.bind(body={"wallet_id": wallet_id, "body": body})
bound_logger.debug("PUT request received: Update tenant")

Expand All @@ -284,13 +461,34 @@ async def update_tenant(
return response


@router.get("/{wallet_id}", response_model=Tenant)
@router.get("/{wallet_id}", response_model=Tenant, summary="Get Tenant by Wallet ID")
async def get_tenant(
wallet_id: str,
group_id: Optional[str] = group_id_query,
admin_auth: AcaPyAuthVerified = Depends(acapy_auth_tenant_admin),
) -> Tenant:
"""Get tenant by id."""
"""
Fetch Tenant info by ID
---
Use this endpoint to fetch Tenant info by Wallet ID.
Request parameters:
---
wallet_id: str
The Wallet ID of the Tenant to fetch.
Response body:
---
Tenant
wallet_id: str
wallet_label: str
wallet_name: str
created_at: str
updated_at: Optional[str]
image_url: Optional[str]
group_id: Optional[str]
"""
bound_logger = logger.bind(body={"wallet_id": wallet_id})
bound_logger.debug("GET request received: Fetch tenant by id")

Expand All @@ -307,7 +505,7 @@ async def get_tenant(
return response


@router.get("", response_model=List[Tenant])
@router.get("", response_model=List[Tenant], summary="Fetch Tenants")
async def get_tenants(
wallet_name: Optional[str] = None,
group_id: Optional[str] = group_id_query,
Expand All @@ -317,7 +515,38 @@ async def get_tenants(
descending: bool = descending_query_parameter,
admin_auth: AcaPyAuthVerified = Depends(acapy_auth_tenant_admin),
) -> List[Tenant]:
"""Get all tenants, or fetch by wallet name."""
"""
Fetch all Tenants (paginated), or Fetch by Wallet Name
---
Use this endpoint to fetch all tenants (using pagination), or filter by wallet name.
The default for `limit` is 1000, with a maximum of 10000.
Results are ordered by creation time (newest first), and can be controlled to be in ascending order (oldest first).
Optional Request Parameters:
---
wallet_name: str
Filter by wallet name.
limit: int
Number of results to return.
offset: int
Number of results to skip.
descending: bool
Whether to return results in descending order or not. Defaults to true (newest first).
Response body:
---
List[Tenant]
wallet_id: str
wallet_label: str
wallet_name: str
created_at: str
updated_at: Optional[str]
image_url: Optional[str]
group_id: Optional[str]
"""
bound_logger = logger.bind(body={"wallet_name": wallet_name, "group_id": group_id})
bound_logger.debug(
"GET request received: Fetch tenants by wallet name and/or group id"
Expand Down
4 changes: 2 additions & 2 deletions app/tests/e2e/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ async def test_jwt_invalid_token_error(tenant_admin_client: RichAsyncClient):
delete_response = await tenant_admin_client.delete(
f"{TENANTS_BASE_PATH}/{wallet_id}"
)
assert delete_response.status_code == 200
assert delete_response.status_code == 204


@pytest.mark.anyio
Expand Down Expand Up @@ -149,4 +149,4 @@ async def test_invalid_token_error_after_rotation(tenant_admin_client: RichAsync
delete_response = await tenant_admin_client.delete(
f"{TENANTS_BASE_PATH}/{wallet_id}"
)
assert delete_response.status_code == 200
assert delete_response.status_code == 204
Loading

0 comments on commit 862518b

Please sign in to comment.