From 0b55066be4279c54da0b21be7ef24cc3bfaaa443 Mon Sep 17 00:00:00 2001 From: Gavin Jaeger-Freeborn Date: Wed, 14 Aug 2024 11:09:51 -0700 Subject: [PATCH 1/9] delete api records and changing wallet based tokens Signed-off-by: Gavin Jaeger-Freeborn --- .../v1_0/innkeeper/models.py | 9 ++- .../v1_0/innkeeper/routes.py | 67 +++++++++++++++++++ .../v1_0/innkeeper/utils.py | 11 ++- .../traction_innkeeper/v1_0/tenant/routes.py | 25 +++++++ services/aca-py/ngrok-wait.sh | 2 +- 5 files changed, 109 insertions(+), 5 deletions(-) diff --git a/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/models.py b/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/models.py index f16eb5be6..4e8158600 100644 --- a/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/models.py +++ b/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/models.py @@ -357,12 +357,19 @@ async def soft_delete(self, session: ProfileSession): Soft delete the tenant record by setting its state to 'deleted'. Note: This method should be called on an instance of the TenantRecord. """ + # Delete api records + recs = await TenantAuthenticationApiRecord.query_by_tenant_id( + session, self.tenant_id + ) + for rec in recs: + if rec.tenant_id == self.tenant_id: + await rec.delete_record(session) + if self.state != self.STATE_DELETED: self.state = self.STATE_DELETED self.deleted_at = datetime_to_str(datetime.utcnow()) await self.save(session, reason="Soft delete") - async def restore_deleted(self, session: ProfileSession): """ Un-soft-delete the tenant record by setting its state to 'active'. diff --git a/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/routes.py b/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/routes.py index 89eac03c2..12418b1c8 100644 --- a/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/routes.py +++ b/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/routes.py @@ -450,6 +450,16 @@ async def tenant_checkin(request: web.BaseRequest): ) +async def delete_auth_tokens(context: AdminRequestContext, wallet_record: WalletRecord): + mgr = context.inject(TenantManager) + profile = mgr.profile + multitenant_mgr = profile.inject(BaseMultitenantManager) + + wallet_record.jwt_iat = None + async with multitenant_mgr._profile.session() as session: + await wallet_record.save(session) + + @docs(tags=["multitenancy"], summary="Get auth token for a tenant") @match_info_schema(TenantIdMatchInfoSchema()) @request_schema(CustomCreateWalletTokenRequestSchema) @@ -522,6 +532,60 @@ async def tenant_create_token(request: web.BaseRequest): return web.json_response({"token": token}) +@docs( + tags=["multitenancy"], + summary="Get auth token for a subwallet (innkeeper plugin override)", +) +@request_schema(CreateWalletTokenRequestSchema) +@response_schema(CreateWalletTokenResponseSchema(), 200, description="") +@error_handler +async def tenant_wallet_create_token(request: web.BaseRequest): + raise web.HTTPUnauthorized(reason="Tenant is disabled") + context: AdminRequestContext = request["context"] + wallet_id = request.match_info["wallet_id"] + wallet_key = None + + mgr = context.inject(TenantManager) + profile = mgr.profile + + # Tenants must always be fetch by their wallet id. + rec = await TenantRecord.query_by_wallet_id(profile.session(), wallet_id) + LOGGER.warn("when creating token ", rec) + if rec.state == TenantRecord.STATE_DELETED: + raise web.HTTPUnauthorized(reason="Tenant is disabled") + + # The rest is from https://github.com/hyperledger/aries-acapy-plugins/blob/main/multitenant_provider/multitenant_provider/v1_0/routes.py + LOGGER.warn(f"wallet_id = {wallet_id}") + + # "builtin" wallet_create_token uses request.has_body / can_read_body + # which do not always return true, so wallet_key wasn't getting set or passed + # into create_auth_token. + + # if there's no body or the wallet_key is not in the body, + # or wallet_key is blank, return an error + if not request.body_exists: + raise web.HTTPUnauthorized(reason="Missing wallet_key") + + body = await request.json() + wallet_key = body.get("wallet_key") + LOGGER.warn(f"wallet_key = {wallet_key}") + + # If wallet_key is not there or blank return an error + if not wallet_key: + raise web.HTTPUnauthorized(reason="Missing wallet_key") + + profile = context.profile + # TODO + # config = profile.inject(MultitenantProviderConfig) + multitenant_mgr = profile.inject(BaseMultitenantManager) + async with profile.session() as session: + wallet_record = await WalletRecord.retrieve_by_id(session, wallet_id) + + token = await multitenant_mgr.create_auth_token(wallet_record, wallet_key) + + return web.json_response({"token": token}) + + @docs( tags=[SWAGGER_CATEGORY], ) @@ -1029,6 +1093,9 @@ async def register(app: web.Application): "/multitenancy/reservations/{reservation_id}/check-in", tenant_checkin ), web.post("/multitenancy/tenant/{tenant_id}/token", tenant_create_token), + web.post( + "/multitenancy/wallet/{wallet_id}/token", tenant_wallet_create_token + ), ] ) # routes that require a tenant token for the innkeeper wallet/tenant/agent. diff --git a/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/utils.py b/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/utils.py index ed6a4741a..065cb010f 100644 --- a/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/utils.py +++ b/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/utils.py @@ -7,7 +7,7 @@ from aries_cloudagent.messaging.models.openapi import OpenAPISchema from marshmallow import fields -from .models import ReservationRecord, TenantAuthenticationApiRecord +from .models import ReservationRecord, TenantAuthenticationApiRecord, TenantRecord from . import TenantManager @@ -114,7 +114,9 @@ async def refresh_registration_token(reservation_id: str, manager: TenantManager ) except Exception as err: LOGGER.error("Failed to retrieve reservation: %s", err) - raise ReservationException("Could not retrieve reservation record.") from err + raise ReservationException( + "Could not retrieve reservation record." + ) from err if reservation.state != ReservationRecord.STATE_APPROVED: raise ReservationException("Only approved reservations can refresh tokens.") @@ -140,7 +142,8 @@ async def refresh_registration_token(reservation_id: str, manager: TenantManager LOGGER.info("Refreshed token for reservation %s", reservation_id) - return _pwd + return _pwd + def generate_api_key_data(): _key = str(uuid.uuid4().hex) @@ -156,6 +159,8 @@ def generate_api_key_data(): async def create_api_key(rec: TenantAuthenticationApiRecord, manager: TenantManager): + if rec.state == TenantRecord.STATE_DELETED: + raise ValueError("Tenant is disabled") async with manager.profile.session() as session: _key, _salt, _hash = generate_api_key_data() rec.api_key_token_salt = _salt.decode("utf-8") diff --git a/plugins/traction_innkeeper/traction_innkeeper/v1_0/tenant/routes.py b/plugins/traction_innkeeper/traction_innkeeper/v1_0/tenant/routes.py index eaf84cdbb..44196970c 100644 --- a/plugins/traction_innkeeper/traction_innkeeper/v1_0/tenant/routes.py +++ b/plugins/traction_innkeeper/traction_innkeeper/v1_0/tenant/routes.py @@ -109,6 +109,31 @@ async def setup_tenant_context(request: web.Request, handler): return await handler(request) +def active_tenant_only(func): + @functools.wraps(func) + async def wrapper(request): + print("> innkeeper_only") + context: AdminRequestContext = request["context"] + profile = context.profile + wallet_name = str(profile.settings.get("wallet.name")) + wallet_innkeeper = bool(profile.settings.get("wallet.innkeeper")) + LOGGER.info(f"wallet.name = {wallet_name}") + LOGGER.info(f"wallet.innkeeper = {wallet_innkeeper}") + if wallet_innkeeper: + try: + ret = await func(request) + return ret + finally: + print("< innkeeper_only") + else: + LOGGER.error( + f"API call is for innkeepers only. wallet.name = '{wallet_name}', wallet.innkeeper = {wallet_innkeeper}" + ) + raise web.HTTPUnauthorized() + + return wrapper + + @docs( tags=[SWAGGER_CATEGORY], ) diff --git a/services/aca-py/ngrok-wait.sh b/services/aca-py/ngrok-wait.sh index a9db533f7..f55bed2f8 100755 --- a/services/aca-py/ngrok-wait.sh +++ b/services/aca-py/ngrok-wait.sh @@ -37,8 +37,8 @@ exec aca-py start \ --wallet-storage-config "{\"url\":\"${POSTGRESQL_HOST}:5432\",\"max_connections\":5, \"wallet_scheme\":\"${TRACTION_ACAPY_WALLET_SCHEME}\"}" \ --wallet-storage-creds "{\"account\":\"${POSTGRESQL_USER}\",\"password\":\"${POSTGRESQL_PASSWORD}\",\"admin_account\":\"${POSTGRESQL_USER}\",\"admin_password\":\"${POSTGRESQL_PASSWORD}\"}" \ --admin "0.0.0.0" ${TRACTION_ACAPY_ADMIN_PORT} \ + --plugin multitenant_provider.v1_0 \ --plugin traction_plugins.traction_innkeeper.v1_0 \ --plugin basicmessage_storage.v1_0 \ --plugin connection_update.v1_0 \ - --plugin multitenant_provider.v1_0 \ --plugin rpc.v1_0 \ From d052472e07bc29f33183d652d3b3650c6c9b3b7a Mon Sep 17 00:00:00 2001 From: Gavin Jaeger-Freeborn Date: Wed, 14 Aug 2024 13:17:18 -0700 Subject: [PATCH 2/9] Properly override url Signed-off-by: Gavin Jaeger-Freeborn --- .../traction/templates/acapy/deployment.yaml | 6 ++--- .../v1_0/innkeeper/routes.py | 23 +++++++++++++++---- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/charts/traction/templates/acapy/deployment.yaml b/charts/traction/templates/acapy/deployment.yaml index a2bc962a7..48c41b8b0 100644 --- a/charts/traction/templates/acapy/deployment.yaml +++ b/charts/traction/templates/acapy/deployment.yaml @@ -51,6 +51,9 @@ spec: --endpoint https://{{ include "acapy.host" . }} \ --arg-file '/home/aries/argfile.yml' \ --plugin 'aries_cloudagent.messaging.jsonld' \ + {{- if .Values.acapy.plugins.multitenantProvider }} + --plugin multitenant_provider.v1_0 \ + {{- end }} {{- if .Values.acapy.plugins.tractionInnkeeper }} --plugin traction_plugins.traction_innkeeper.v1_0 \ --plugin-config-value traction_innkeeper.innkeeper_wallet.tenant_id=\"$(INNKEEPER_WALLET_TENANT_ID)\" \ @@ -62,9 +65,6 @@ spec: {{- if .Values.acapy.plugins.connectionUpdate }} --plugin connection_update.v1_0 \ {{- end }} - {{- if .Values.acapy.plugins.multitenantProvider }} - --plugin multitenant_provider.v1_0 \ - {{- end }} {{- if .Values.acapy.plugins.rpc }} --plugin rpc.v1_0 \ {{- end }} diff --git a/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/routes.py b/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/routes.py index 12418b1c8..093c33c88 100644 --- a/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/routes.py +++ b/plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/routes.py @@ -540,7 +540,6 @@ async def tenant_create_token(request: web.BaseRequest): @response_schema(CreateWalletTokenResponseSchema(), 200, description="") @error_handler async def tenant_wallet_create_token(request: web.BaseRequest): - raise web.HTTPUnauthorized(reason="Tenant is disabled") context: AdminRequestContext = request["context"] wallet_id = request.match_info["wallet_id"] wallet_key = None @@ -549,10 +548,11 @@ async def tenant_wallet_create_token(request: web.BaseRequest): profile = mgr.profile # Tenants must always be fetch by their wallet id. - rec = await TenantRecord.query_by_wallet_id(profile.session(), wallet_id) - LOGGER.warn("when creating token ", rec) - if rec.state == TenantRecord.STATE_DELETED: - raise web.HTTPUnauthorized(reason="Tenant is disabled") + async with profile.session() as session: + rec = await TenantRecord.query_by_wallet_id(session, wallet_id) + LOGGER.warn("when creating token ", rec) + if rec.state == TenantRecord.STATE_DELETED: + raise web.HTTPUnauthorized(reason="Tenant is disabled") # The rest is from https://github.com/hyperledger/aries-acapy-plugins/blob/main/multitenant_provider/multitenant_provider/v1_0/routes.py LOGGER.warn(f"wallet_id = {wallet_id}") @@ -1098,6 +1098,19 @@ async def register(app: web.Application): ), ] ) + for r in app.router.routes(): + if r.method == "POST": + if ( + r.resource + and r.resource.canonical == "/multitenancy/wallet/{wallet_id}/token" + ): + LOGGER.info( + f"found route: {r.method} {r.resource.canonical} ({r.handler})" + ) + LOGGER.info(f"... replacing current handler: {r.handler}") + r._handler = tenant_wallet_create_token + LOGGER.info(f"... with new handler: {r.handler}") + has_wallet_create_token = True # routes that require a tenant token for the innkeeper wallet/tenant/agent. # these require not only a tenant, but it has to be the innkeeper tenant! app.add_routes( From 2dc2bf1ac2812be7c716db2b035c1d57cbe2e5fe Mon Sep 17 00:00:00 2001 From: Gavin Jaeger-Freeborn Date: Mon, 19 Aug 2024 14:47:06 -0700 Subject: [PATCH 3/9] Improved UI options for deletion of suspended tenants Signed-off-by: Gavin Jaeger-Freeborn --- .../src/components/innkeeper/tenants/Tenants.vue | 1 + .../deleteTenant/ConfirmTenantDeletion.vue | 15 +++++++++++---- .../tenants/deleteTenant/DeleteTenant.vue | 2 ++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/services/tenant-ui/frontend/src/components/innkeeper/tenants/Tenants.vue b/services/tenant-ui/frontend/src/components/innkeeper/tenants/Tenants.vue index 8929890f6..89691cc86 100644 --- a/services/tenant-ui/frontend/src/components/innkeeper/tenants/Tenants.vue +++ b/services/tenant-ui/frontend/src/components/innkeeper/tenants/Tenants.vue @@ -52,6 +52,7 @@ {{ $t('common.deleted') }} + diff --git a/services/tenant-ui/frontend/src/components/innkeeper/tenants/deleteTenant/ConfirmTenantDeletion.vue b/services/tenant-ui/frontend/src/components/innkeeper/tenants/deleteTenant/ConfirmTenantDeletion.vue index 4b20e2930..c051460e4 100644 --- a/services/tenant-ui/frontend/src/components/innkeeper/tenants/deleteTenant/ConfirmTenantDeletion.vue +++ b/services/tenant-ui/frontend/src/components/innkeeper/tenants/deleteTenant/ConfirmTenantDeletion.vue @@ -21,7 +21,7 @@ {{ $t('tenants.settings.permanentDelete') }} -
+