From 6b0aa4ecdcc7fa6054eca07d174d7284656c02bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emiliano=20Su=C3=B1=C3=A9?= Date: Fri, 10 May 2024 09:33:16 -0700 Subject: [PATCH] Feature: use decorators for admin api authentication (#2860) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add authentication decorators for admin API Signed-off-by: Emiliano Suñé --------- Signed-off-by: Emiliano Suñé --- .pre-commit-config.yaml | 2 +- aries_cloudagent/admin/decorators/auth.py | 80 +++++ aries_cloudagent/admin/routes.py | 232 ++++++++++++ aries_cloudagent/admin/server.py | 338 ++---------------- .../admin/tests/test_admin_server.py | 101 +----- aries_cloudagent/admin/tests/test_auth.py | 138 +++++++ aries_cloudagent/anoncreds/routes.py | 11 + .../anoncreds/tests/test_revocation_setup.py | 4 - .../anoncreds/tests/test_routes.py | 24 +- aries_cloudagent/config/argparse.py | 18 +- aries_cloudagent/holder/routes.py | 10 +- aries_cloudagent/holder/tests/test_routes.py | 13 +- aries_cloudagent/ledger/routes.py | 13 +- aries_cloudagent/ledger/tests/test_routes.py | 18 +- .../credential_definitions/routes.py | 5 + .../tests/test_routes.py | 10 +- aries_cloudagent/messaging/jsonld/routes.py | 6 +- .../messaging/jsonld/tests/test_routes.py | 38 +- aries_cloudagent/messaging/schemas/routes.py | 5 + .../messaging/schemas/tests/test_routes.py | 10 +- aries_cloudagent/multitenant/admin/routes.py | 7 + .../multitenant/admin/tests/test_routes.py | 9 +- .../protocols/actionmenu/v1_0/routes.py | 7 +- .../actionmenu/v1_0/tests/test_routes.py | 11 +- .../protocols/basicmessage/v1_0/routes.py | 3 +- .../basicmessage/v1_0/tests/test_routes.py | 11 +- .../protocols/connections/v1_0/routes.py | 15 +- .../connections/v1_0/tests/test_routes.py | 14 +- .../coordinate_mediation/v1_0/routes.py | 15 +- .../v1_0/tests/test_routes.py | 14 +- .../v1_0/messages/tests/test_rotate.py | 1 - .../protocols/did_rotate/v1_0/routes.py | 3 + .../did_rotate/v1_0/tests/test_routes.py | 13 +- .../protocols/didexchange/v1_0/routes.py | 8 +- .../didexchange/v1_0/tests/test_routes.py | 12 +- .../protocols/discovery/v1_0/routes.py | 4 +- .../discovery/v1_0/tests/test_routes.py | 12 +- .../protocols/discovery/v2_0/routes.py | 4 +- .../discovery/v2_0/tests/test_routes.py | 12 +- .../endorse_transaction/v1_0/routes.py | 11 + .../v1_0/tests/test_routes.py | 9 +- .../protocols/introduction/v0_1/routes.py | 3 +- .../introduction/v0_1/tests/test_routes.py | 13 +- .../protocols/issue_credential/v1_0/routes.py | 14 + .../v1_0/tests/test_routes.py | 14 +- .../protocols/issue_credential/v2_0/routes.py | 15 + .../v2_0/tests/test_routes.py | 16 +- .../protocols/out_of_band/v1_0/routes.py | 6 +- .../out_of_band/v1_0/tests/test_routes.py | 9 +- .../protocols/present_proof/v1_0/routes.py | 13 +- .../present_proof/v1_0/tests/test_routes.py | 14 +- .../protocols/present_proof/v2_0/routes.py | 15 +- .../present_proof/v2_0/tests/test_routes.py | 19 +- .../v2_0/tests/test_routes_anoncreds.py | 23 +- .../protocols/trustping/v1_0/routes.py | 3 +- .../trustping/v1_0/tests/test_routes.py | 11 +- aries_cloudagent/resolver/routes.py | 5 +- .../resolver/tests/test_routes.py | 19 +- aries_cloudagent/revocation/routes.py | 21 ++ .../revocation/tests/test_routes.py | 17 +- .../revocation_anoncreds/routes.py | 14 + .../revocation_anoncreds/tests/test_routes.py | 9 +- .../settings/tests/test_routes.py | 9 +- aries_cloudagent/utils/general.py | 10 + aries_cloudagent/vc/routes.py | 24 +- aries_cloudagent/wallet/routes.py | 13 + aries_cloudagent/wallet/tests/test_routes.py | 5 +- poetry.lock | 50 +-- pyproject.toml | 2 +- 69 files changed, 1070 insertions(+), 587 deletions(-) create mode 100644 aries_cloudagent/admin/decorators/auth.py create mode 100644 aries_cloudagent/admin/routes.py create mode 100644 aries_cloudagent/admin/tests/test_auth.py create mode 100644 aries_cloudagent/utils/general.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 038ab3fb1f..713f8bb445 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: additional_dependencies: ['@commitlint/config-conventional'] - repo: https://github.com/psf/black # Ensure this is synced with pyproject.toml - rev: 24.1.1 + rev: 24.4.0 hooks: - id: black stages: [commit] diff --git a/aries_cloudagent/admin/decorators/auth.py b/aries_cloudagent/admin/decorators/auth.py new file mode 100644 index 0000000000..818f297d40 --- /dev/null +++ b/aries_cloudagent/admin/decorators/auth.py @@ -0,0 +1,80 @@ +"""Authentication decorators for the admin API.""" + +import functools + +from aiohttp import web + +from ...utils import general as general_utils +from ..request_context import AdminRequestContext + + +def admin_authentication(handler): + """Decorator to require authentication via admin API key. + + The decorator will check for a valid x-api-key header and + reject the request if it is missing or invalid. + If the agent is running in insecure mode, the request will be allowed without a key. + """ + + @functools.wraps(handler) + async def admin_auth(request): + context: AdminRequestContext = request["context"] + profile = context.profile + header_admin_api_key = request.headers.get("x-api-key") + valid_key = general_utils.const_compare( + profile.settings.get("admin.admin_api_key"), header_admin_api_key + ) + insecure_mode = bool(profile.settings.get("admin.admin_insecure_mode")) + + # We have to allow OPTIONS method access to paths without a key since + # browsers performing CORS requests will never include the original + # x-api-key header from the method that triggered the preflight + # OPTIONS check. + if insecure_mode or valid_key or (request.method == "OPTIONS"): + return await handler(request) + else: + raise web.HTTPUnauthorized( + reason="API Key invalid or missing", + text="API Key invalid or missing", + ) + + return admin_auth + + +def tenant_authentication(handler): + """Decorator to enable non-admin authentication. + + The decorator will: + - check for a valid bearer token in the Autorization header if running + in multi-tenant mode + - check for a valid x-api-key header if running in single-tenant mode + """ + + @functools.wraps(handler) + async def tenant_auth(request): + context: AdminRequestContext = request["context"] + profile = context.profile + authorization_header = request.headers.get("Authorization") + header_admin_api_key = request.headers.get("x-api-key") + valid_key = general_utils.const_compare( + profile.settings.get("admin.admin_api_key"), header_admin_api_key + ) + insecure_mode = bool(profile.settings.get("admin.admin_insecure_mode")) + multitenant_enabled = profile.settings.get("multitenant.enabled") + + # CORS fix: allow OPTIONS method access to paths without a token + if ( + (multitenant_enabled and authorization_header) + or (not multitenant_enabled and valid_key) + or insecure_mode + or request.method == "OPTIONS" + ): + return await handler(request) + else: + auth_mode = "Authorization token" if multitenant_enabled else "API key" + raise web.HTTPUnauthorized( + reason=f"{auth_mode} missing or invalid", + text=f"{auth_mode} missing or invalid", + ) + + return tenant_auth diff --git a/aries_cloudagent/admin/routes.py b/aries_cloudagent/admin/routes.py new file mode 100644 index 0000000000..62abab842a --- /dev/null +++ b/aries_cloudagent/admin/routes.py @@ -0,0 +1,232 @@ +"""Admin server routes.""" + +import asyncio +import re + +from aiohttp import web +from aiohttp_apispec import ( + docs, + response_schema, +) +from marshmallow import fields + +from ..core.plugin_registry import PluginRegistry +from ..messaging.models.openapi import OpenAPISchema +from ..utils.stats import Collector +from ..version import __version__ +from .decorators.auth import admin_authentication + + +class AdminModulesSchema(OpenAPISchema): + """Schema for the modules endpoint.""" + + result = fields.List( + fields.Str(metadata={"description": "admin module"}), + metadata={"description": "List of admin modules"}, + ) + + +class AdminConfigSchema(OpenAPISchema): + """Schema for the config endpoint.""" + + config = fields.Dict( + required=True, metadata={"description": "Configuration settings"} + ) + + +class AdminStatusSchema(OpenAPISchema): + """Schema for the status endpoint.""" + + version = fields.Str(metadata={"description": "Version code"}) + label = fields.Str(allow_none=True, metadata={"description": "Default label"}) + timing = fields.Dict(required=False, metadata={"description": "Timing results"}) + conductor = fields.Dict( + required=False, metadata={"description": "Conductor statistics"} + ) + + +class AdminResetSchema(OpenAPISchema): + """Schema for the reset endpoint.""" + + +class AdminStatusLivelinessSchema(OpenAPISchema): + """Schema for the liveliness endpoint.""" + + alive = fields.Boolean( + metadata={"description": "Liveliness status", "example": True} + ) + + +class AdminStatusReadinessSchema(OpenAPISchema): + """Schema for the readiness endpoint.""" + + ready = fields.Boolean( + metadata={"description": "Readiness status", "example": True} + ) + + +class AdminShutdownSchema(OpenAPISchema): + """Response schema for admin Module.""" + + +@docs(tags=["server"], summary="Fetch the list of loaded plugins") +@response_schema(AdminModulesSchema(), 200, description="") +@admin_authentication +async def plugins_handler(request: web.BaseRequest): + """Request handler for the loaded plugins list. + + Args: + request: aiohttp request object + + Returns: + The module list response + + """ + registry = request.app["context"].inject_or(PluginRegistry) + plugins = registry and sorted(registry.plugin_names) or [] + return web.json_response({"result": plugins}) + + +@docs(tags=["server"], summary="Fetch the server configuration") +@response_schema(AdminConfigSchema(), 200, description="") +@admin_authentication +async def config_handler(request: web.BaseRequest): + """Request handler for the server configuration. + + Args: + request: aiohttp request object + + Returns: + The web response + + """ + config = { + k: ( + request.app["context"].settings[k] + if (isinstance(request.app["context"].settings[k], (str, int))) + else request.app["context"].settings[k].copy() + ) + for k in request.app["context"].settings + if k + not in [ + "admin.admin_api_key", + "multitenant.jwt_secret", + "wallet.key", + "wallet.rekey", + "wallet.seed", + "wallet.storage_creds", + ] + } + for index in range(len(config.get("admin.webhook_urls", []))): + config["admin.webhook_urls"][index] = re.sub( + r"#.*", + "", + config["admin.webhook_urls"][index], + ) + + return web.json_response({"config": config}) + + +@docs(tags=["server"], summary="Fetch the server status") +@response_schema(AdminStatusSchema(), 200, description="") +@admin_authentication +async def status_handler(request: web.BaseRequest): + """Request handler for the server status information. + + Args: + request: aiohttp request object + + Returns: + The web response + + """ + status = {"version": __version__} + status["label"] = request.app["context"].settings.get("default_label") + collector = request.app["context"].inject_or(Collector) + if collector: + status["timing"] = collector.results + if request.app["conductor_stats"]: + status["conductor"] = await request.app["conductor_stats"]() + return web.json_response(status) + + +@docs(tags=["server"], summary="Reset statistics") +@response_schema(AdminResetSchema(), 200, description="") +@admin_authentication +async def status_reset_handler(request: web.BaseRequest): + """Request handler for resetting the timing statistics. + + Args: + request: aiohttp request object + + Returns: + The web response + + """ + collector = request.app["context"].inject_or(Collector) + if collector: + collector.reset() + return web.json_response({}) + + +async def redirect_handler(request: web.BaseRequest): + """Perform redirect to documentation.""" + raise web.HTTPFound("/api/doc") + + +@docs(tags=["server"], summary="Liveliness check") +@response_schema(AdminStatusLivelinessSchema(), 200, description="") +async def liveliness_handler(request: web.BaseRequest): + """Request handler for liveliness check. + + Args: + request: aiohttp request object + + Returns: + The web response, always indicating True + + """ + app_live = request.app._state["alive"] + if app_live: + return web.json_response({"alive": app_live}) + else: + raise web.HTTPServiceUnavailable(reason="Service not available") + + +@docs(tags=["server"], summary="Readiness check") +@response_schema(AdminStatusReadinessSchema(), 200, description="") +async def readiness_handler(request: web.BaseRequest): + """Request handler for liveliness check. + + Args: + request: aiohttp request object + + Returns: + The web response, indicating readiness for further calls + + """ + app_ready = request.app._state["ready"] and request.app._state["alive"] + if app_ready: + return web.json_response({"ready": app_ready}) + else: + raise web.HTTPServiceUnavailable(reason="Service not ready") + + +@docs(tags=["server"], summary="Shut down server") +@response_schema(AdminShutdownSchema(), description="") +@admin_authentication +async def shutdown_handler(request: web.BaseRequest): + """Request handler for server shutdown. + + Args: + request: aiohttp request object + + Returns: + The web response (empty production) + + """ + request.app._state["ready"] = False + loop = asyncio.get_event_loop() + asyncio.ensure_future(request.app["conductor_stop"](), loop=loop) + + return web.json_response({}) diff --git a/aries_cloudagent/admin/server.py b/aries_cloudagent/admin/server.py index 2fe7dd1b61..d9d58fa92b 100644 --- a/aries_cloudagent/admin/server.py +++ b/aries_cloudagent/admin/server.py @@ -6,19 +6,15 @@ import uuid import warnings import weakref -from hmac import compare_digest from typing import Callable, Coroutine, Optional, Pattern, Sequence, cast import aiohttp_cors import jwt from aiohttp import web from aiohttp_apispec import ( - docs, - response_schema, setup_aiohttp_apispec, validation_middleware, ) -from marshmallow import fields from aries_cloudagent.wallet import singletons @@ -28,9 +24,7 @@ from ..core.plugin_registry import PluginRegistry from ..core.profile import Profile from ..ledger.error import LedgerConfigError, LedgerTransactionError -from ..messaging.models.openapi import OpenAPISchema from ..messaging.responder import BaseResponder -from ..messaging.valid import UUIDFour from ..multitenant.base import BaseMultitenantManager, MultitenantManagerError from ..storage.base import BaseStorage from ..storage.error import StorageNotFoundError @@ -38,6 +32,7 @@ from ..transport.outbound.message import OutboundMessage from ..transport.outbound.status import OutboundSendStatus from ..transport.queue.basic import BasicMessageQueue +from ..utils import general as general_utils from ..utils.stats import Collector from ..utils.task_queue import TaskQueue from ..version import __version__ @@ -45,6 +40,16 @@ from .base_server import BaseAdminServer from .error import AdminSetupError from .request_context import AdminRequestContext +from .routes import ( + config_handler, + liveliness_handler, + plugins_handler, + readiness_handler, + redirect_handler, + shutdown_handler, + status_handler, + status_reset_handler, +) LOGGER = logging.getLogger(__name__) @@ -66,58 +71,6 @@ in_progress_upgrades = singletons.UpgradeInProgressSingleton() -class AdminModulesSchema(OpenAPISchema): - """Schema for the modules endpoint.""" - - result = fields.List( - fields.Str(metadata={"description": "admin module"}), - metadata={"description": "List of admin modules"}, - ) - - -class AdminConfigSchema(OpenAPISchema): - """Schema for the config endpoint.""" - - config = fields.Dict( - required=True, metadata={"description": "Configuration settings"} - ) - - -class AdminStatusSchema(OpenAPISchema): - """Schema for the status endpoint.""" - - version = fields.Str(metadata={"description": "Version code"}) - label = fields.Str(allow_none=True, metadata={"description": "Default label"}) - timing = fields.Dict(required=False, metadata={"description": "Timing results"}) - conductor = fields.Dict( - required=False, metadata={"description": "Conductor statistics"} - ) - - -class AdminResetSchema(OpenAPISchema): - """Schema for the reset endpoint.""" - - -class AdminStatusLivelinessSchema(OpenAPISchema): - """Schema for the liveliness endpoint.""" - - alive = fields.Boolean( - metadata={"description": "Liveliness status", "example": True} - ) - - -class AdminStatusReadinessSchema(OpenAPISchema): - """Schema for the readiness endpoint.""" - - ready = fields.Boolean( - metadata={"description": "Readiness status", "example": True} - ) - - -class AdminShutdownSchema(OpenAPISchema): - """Response schema for admin Module.""" - - class AdminResponder(BaseResponder): """Handle outgoing messages from message handlers.""" @@ -259,13 +212,6 @@ async def debug_middleware(request: web.BaseRequest, handler: Coroutine): return await handler(request) -def const_compare(string1, string2): - """Compare two strings in constant time.""" - if string1 is None or string2 is None: - return False - return compare_digest(string1.encode(), string2.encode()) - - class AdminServer(BaseAdminServer): """Admin HTTP server class.""" @@ -313,8 +259,6 @@ def __init__( self.multitenant_manager = context.inject_or(BaseMultitenantManager) self._additional_route_pattern: Optional[Pattern] = None - self.server_paths = [] - @property def additional_routes_pattern(self) -> Optional[Pattern]: """Pattern for configured additional routes to permit base wallet to access.""" @@ -347,90 +291,8 @@ async def make_application(self) -> web.Application: # we check here. assert self.admin_insecure_mode ^ bool(self.admin_api_key) - def is_unprotected_path(path: str): - return path in [ - "/api/doc", - "/api/docs/swagger.json", - "/favicon.ico", - "/ws", # ws handler checks authentication - "/status/live", - "/status/ready", - ] or path.startswith("/static/swagger/") - - # If admin_api_key is None, then admin_insecure_mode must be set so - # we can safely enable the admin server with no security - if self.admin_api_key: - - @web.middleware - async def check_token(request: web.Request, handler): - header_admin_api_key = request.headers.get("x-api-key") - valid_key = const_compare(self.admin_api_key, header_admin_api_key) - - # We have to allow OPTIONS method access to paths without a key since - # browsers performing CORS requests will never include the original - # x-api-key header from the method that triggered the preflight - # OPTIONS check. - if ( - valid_key - or is_unprotected_path(request.path) - or (request.method == "OPTIONS") - ): - return await handler(request) - else: - raise web.HTTPUnauthorized() - - middlewares.append(check_token) - collector = self.context.inject_or(Collector) - if self.multitenant_manager: - - @web.middleware - async def check_multitenant_authorization(request: web.Request, handler): - authorization_header = request.headers.get("Authorization") - path = request.path - - is_multitenancy_path = path.startswith("/multitenancy") - is_server_path = path in self.server_paths or path == "/features" - # allow base wallets to trigger update through api - is_upgrade_path = path.startswith("/anoncreds/wallet/upgrade") - - # subwallets are not allowed to access multitenancy routes - if authorization_header and is_multitenancy_path: - raise web.HTTPUnauthorized() - - base_limited_access_path = ( - re.match( - f"^/connections/(?:receive-invitation|{UUIDFour.PATTERN})", path - ) - or path.startswith("/out-of-band/receive-invitation") - or path.startswith("/mediation/requests/") - or re.match( - f"/mediation/(?:request/{UUIDFour.PATTERN}|" - f"{UUIDFour.PATTERN}/default-mediator)", - path, - ) - or path.startswith("/mediation/default-mediator") - or self._matches_additional_routes(path) - ) - - # base wallet is not allowed to perform ssi related actions. - # Only multitenancy and general server actions - if ( - not authorization_header - and not is_multitenancy_path - and not is_server_path - and not is_unprotected_path(path) - and not base_limited_access_path - and not (request.method == "OPTIONS") # CORS fix - and not is_upgrade_path - ): - raise web.HTTPUnauthorized() - - return await handler(request) - - middlewares.append(check_multitenant_authorization) - @web.middleware async def setup_context(request: web.Request, handler): authorization_header = request.headers.get("Authorization") @@ -513,19 +375,16 @@ async def setup_context(request: web.Request, handler): ) server_routes = [ - web.get("/", self.redirect_handler, allow_head=True), - web.get("/plugins", self.plugins_handler, allow_head=False), - web.get("/status", self.status_handler, allow_head=False), - web.get("/status/config", self.config_handler, allow_head=False), - web.post("/status/reset", self.status_reset_handler), - web.get("/status/live", self.liveliness_handler, allow_head=False), - web.get("/status/ready", self.readiness_handler, allow_head=False), - web.get("/shutdown", self.shutdown_handler, allow_head=False), + web.get("/", redirect_handler, allow_head=True), + web.get("/plugins", plugins_handler, allow_head=False), + web.get("/status", status_handler, allow_head=False), + web.get("/status/config", config_handler, allow_head=False), + web.post("/status/reset", status_reset_handler), + web.get("/status/live", liveliness_handler, allow_head=False), + web.get("/status/ready", readiness_handler, allow_head=False), + web.get("/shutdown", shutdown_handler, allow_head=False), web.get("/ws", self.websocket_handler, allow_head=False), ] - - # Store server_paths for multitenant authorization handling - self.server_paths = [route.path for route in server_routes] app.add_routes(server_routes) plugin_registry = self.context.inject_or(PluginRegistry) @@ -558,6 +417,11 @@ async def setup_context(request: web.Request, handler): app._state["ready"] = False app._state["alive"] = False + # set global-like variables + app["context"] = self.context + app["conductor_stats"] = self.conductor_stats + app["conductor_stop"] = self.conductor_stop + return app async def start(self) -> None: @@ -673,156 +537,6 @@ async def on_startup(self, app: web.Application): swagger["securityDefinitions"] = security_definitions swagger["security"] = security - @docs(tags=["server"], summary="Fetch the list of loaded plugins") - @response_schema(AdminModulesSchema(), 200, description="") - async def plugins_handler(self, request: web.BaseRequest): - """Request handler for the loaded plugins list. - - Args: - request: aiohttp request object - - Returns: - The module list response - - """ - registry = self.context.inject_or(PluginRegistry) - plugins = registry and sorted(registry.plugin_names) or [] - return web.json_response({"result": plugins}) - - @docs(tags=["server"], summary="Fetch the server configuration") - @response_schema(AdminConfigSchema(), 200, description="") - async def config_handler(self, request: web.BaseRequest): - """Request handler for the server configuration. - - Args: - request: aiohttp request object - - Returns: - The web response - - """ - config = { - k: ( - self.context.settings[k] - if (isinstance(self.context.settings[k], (str, int))) - else self.context.settings[k].copy() - ) - for k in self.context.settings - if k - not in [ - "admin.admin_api_key", - "multitenant.jwt_secret", - "wallet.key", - "wallet.rekey", - "wallet.seed", - "wallet.storage_creds", - ] - } - for index in range(len(config.get("admin.webhook_urls", []))): - config["admin.webhook_urls"][index] = re.sub( - r"#.*", - "", - config["admin.webhook_urls"][index], - ) - - return web.json_response({"config": config}) - - @docs(tags=["server"], summary="Fetch the server status") - @response_schema(AdminStatusSchema(), 200, description="") - async def status_handler(self, request: web.BaseRequest): - """Request handler for the server status information. - - Args: - request: aiohttp request object - - Returns: - The web response - - """ - status = {"version": __version__} - status["label"] = self.context.settings.get("default_label") - collector = self.context.inject_or(Collector) - if collector: - status["timing"] = collector.results - if self.conductor_stats: - status["conductor"] = await self.conductor_stats() - return web.json_response(status) - - @docs(tags=["server"], summary="Reset statistics") - @response_schema(AdminResetSchema(), 200, description="") - async def status_reset_handler(self, request: web.BaseRequest): - """Request handler for resetting the timing statistics. - - Args: - request: aiohttp request object - - Returns: - The web response - - """ - collector = self.context.inject_or(Collector) - if collector: - collector.reset() - return web.json_response({}) - - async def redirect_handler(self, request: web.BaseRequest): - """Perform redirect to documentation.""" - raise web.HTTPFound("/api/doc") - - @docs(tags=["server"], summary="Liveliness check") - @response_schema(AdminStatusLivelinessSchema(), 200, description="") - async def liveliness_handler(self, request: web.BaseRequest): - """Request handler for liveliness check. - - Args: - request: aiohttp request object - - Returns: - The web response, always indicating True - - """ - app_live = self.app._state["alive"] - if app_live: - return web.json_response({"alive": app_live}) - else: - raise web.HTTPServiceUnavailable(reason="Service not available") - - @docs(tags=["server"], summary="Readiness check") - @response_schema(AdminStatusReadinessSchema(), 200, description="") - async def readiness_handler(self, request: web.BaseRequest): - """Request handler for liveliness check. - - Args: - request: aiohttp request object - - Returns: - The web response, indicating readiness for further calls - - """ - app_ready = self.app._state["ready"] and self.app._state["alive"] - if app_ready: - return web.json_response({"ready": app_ready}) - else: - raise web.HTTPServiceUnavailable(reason="Service not ready") - - @docs(tags=["server"], summary="Shut down server") - @response_schema(AdminShutdownSchema(), description="") - async def shutdown_handler(self, request: web.BaseRequest): - """Request handler for server shutdown. - - Args: - request: aiohttp request object - - Returns: - The web response (empty production) - - """ - self.app._state["ready"] = False - loop = asyncio.get_event_loop() - asyncio.ensure_future(self.conductor_stop(), loop=loop) - - return web.json_response({}) - def notify_fatal_error(self): """Set our readiness flags to force a restart (openshift).""" LOGGER.error("Received shutdown request notify_fatal_error()") @@ -844,7 +558,7 @@ async def websocket_handler(self, request): else: header_admin_api_key = request.headers.get("x-api-key") # authenticated via http header? - queue.authenticated = const_compare( + queue.authenticated = general_utils.const_compare( header_admin_api_key, self.admin_api_key ) @@ -889,7 +603,7 @@ async def websocket_handler(self, request): LOGGER.exception( "Exception in websocket receiving task:" ) - if self.admin_api_key and const_compare( + if self.admin_api_key and general_utils.const_compare( self.admin_api_key, msg_api_key ): # authenticated via websocket message diff --git a/aries_cloudagent/admin/tests/test_admin_server.py b/aries_cloudagent/admin/tests/test_admin_server.py index d74b9d1139..24c8ebfe6c 100644 --- a/aries_cloudagent/admin/tests/test_admin_server.py +++ b/aries_cloudagent/admin/tests/test_admin_server.py @@ -125,7 +125,7 @@ def get_admin_server( collector = Collector() context.injector.bind_instance(test_module.Collector, collector) - profile = InMemoryProfile.test_profile() + profile = InMemoryProfile.test_profile(settings=settings) self.port = unused_port() return AdminServer( @@ -196,105 +196,6 @@ async def test_import_routes(self): server = self.get_admin_server({"admin.admin_insecure_mode": True}, context) app = await server.make_application() - async def test_import_routes_multitenant_middleware(self): - # imports all default admin routes - context = InjectionContext( - settings={"multitenant.base_wallet_routes": ["/test"]} - ) - context.injector.bind_instance(ProtocolRegistry, ProtocolRegistry()) - context.injector.bind_instance(GoalCodeRegistry, GoalCodeRegistry()) - context.injector.bind_instance( - test_module.BaseMultitenantManager, - mock.MagicMock(spec=test_module.BaseMultitenantManager), - ) - await DefaultContextBuilder().load_plugins(context) - server = self.get_admin_server( - { - "admin.admin_insecure_mode": False, - "admin.admin_api_key": "test-api-key", - }, - context, - ) - - # cover multitenancy start code - app = await server.make_application() - app["swagger_dict"] = {} - await server.on_startup(app) - - # multitenant authz - [mt_authz_middle] = [ - m for m in app.middlewares if ".check_multitenant_authorization" in str(m) - ] - - mock_request = mock.MagicMock( - method="GET", - headers={"Authorization": "Bearer ..."}, - path="/multitenancy/etc", - text=mock.CoroutineMock(return_value="abc123"), - ) - with self.assertRaises(test_module.web.HTTPUnauthorized): - await mt_authz_middle(mock_request, None) - - mock_request = mock.MagicMock( - method="GET", - headers={}, - path="/protected/non-multitenancy/non-server", - text=mock.CoroutineMock(return_value="abc123"), - ) - with self.assertRaises(test_module.web.HTTPUnauthorized): - await mt_authz_middle(mock_request, None) - - mock_request = mock.MagicMock( - method="GET", - headers={"Authorization": "Bearer ..."}, - path="/protected/non-multitenancy/non-server", - text=mock.CoroutineMock(return_value="abc123"), - ) - mock_handler = mock.CoroutineMock() - await mt_authz_middle(mock_request, mock_handler) - mock_handler.assert_called_once_with(mock_request) - - mock_request = mock.MagicMock( - method="GET", - headers={"Authorization": "Non-bearer ..."}, - path="/test", - text=mock.CoroutineMock(return_value="abc123"), - ) - mock_handler = mock.CoroutineMock() - await mt_authz_middle(mock_request, mock_handler) - mock_handler.assert_called_once_with(mock_request) - - # multitenant setup context exception paths - [setup_ctx_middle] = [m for m in app.middlewares if ".setup_context" in str(m)] - - mock_request = mock.MagicMock( - method="GET", - headers={"Authorization": "Non-bearer ..."}, - path="/protected/non-multitenancy/non-server", - text=mock.CoroutineMock(return_value="abc123"), - ) - with self.assertRaises(test_module.web.HTTPUnauthorized): - await setup_ctx_middle(mock_request, None) - - mock_request = mock.MagicMock( - method="GET", - headers={"Authorization": "Bearer ..."}, - path="/protected/non-multitenancy/non-server", - text=mock.CoroutineMock(return_value="abc123"), - ) - with mock.patch.object( - server.multitenant_manager, - "get_profile_for_token", - mock.CoroutineMock(), - ) as mock_get_profile: - mock_get_profile.side_effect = [ - test_module.MultitenantManagerError("corrupt token"), - test_module.StorageNotFoundError("out of memory"), - ] - for i in range(2): - with self.assertRaises(test_module.web.HTTPUnauthorized): - await setup_ctx_middle(mock_request, None) - async def test_register_external_plugin_x(self): context = InjectionContext() context.injector.bind_instance(ProtocolRegistry, ProtocolRegistry()) diff --git a/aries_cloudagent/admin/tests/test_auth.py b/aries_cloudagent/admin/tests/test_auth.py new file mode 100644 index 0000000000..2d6700a147 --- /dev/null +++ b/aries_cloudagent/admin/tests/test_auth.py @@ -0,0 +1,138 @@ +from unittest import IsolatedAsyncioTestCase + +from aiohttp import web + +from aries_cloudagent.tests import mock + +from ...core.in_memory.profile import InMemoryProfile +from ..decorators.auth import admin_authentication, tenant_authentication +from ..request_context import AdminRequestContext + + +class TestAdminAuthentication(IsolatedAsyncioTestCase): + def setUp(self) -> None: + + self.profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "admin_api_key", + "admin.admin_insecure_mode": False, + } + ) + self.context = AdminRequestContext.test_context({}, self.profile) + self.request_dict = { + "context": self.context, + } + self.request = mock.MagicMock( + __getitem__=lambda _, k: self.request_dict[k], headers={}, method="POST" + ) + self.decorated_handler = mock.CoroutineMock() + + async def test_options_request(self): + self.request = mock.MagicMock( + __getitem__=lambda _, k: self.request_dict[k], headers={}, method="OPTIONS" + ) + decor_func = admin_authentication(self.decorated_handler) + await decor_func(self.request) + self.decorated_handler.assert_called_once_with(self.request) + + async def test_insecure_mode(self): + self.profile.settings["admin.admin_insecure_mode"] = True + decor_func = admin_authentication(self.decorated_handler) + await decor_func(self.request) + self.decorated_handler.assert_called_once_with(self.request) + + async def test_invalid_api_key(self): + self.request = mock.MagicMock( + __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "wrong-key"}, + method="POST", + ) + decor_func = admin_authentication(self.decorated_handler) + with self.assertRaises(web.HTTPUnauthorized): + await decor_func(self.request) + + async def test_valid_api_key(self): + self.request = mock.MagicMock( + __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "admin_api_key"}, + method="POST", + ) + decor_func = admin_authentication(self.decorated_handler) + await decor_func(self.request) + self.decorated_handler.assert_called_once_with(self.request) + + +class TestTenantAuthentication(IsolatedAsyncioTestCase): + def setUp(self) -> None: + + self.profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "admin_api_key", + "admin.admin_insecure_mode": False, + "multitenant.enabled": True, + } + ) + self.context = AdminRequestContext.test_context({}, self.profile) + self.request_dict = { + "context": self.context, + } + self.request = mock.MagicMock( + __getitem__=lambda _, k: self.request_dict[k], headers={}, method="POST" + ) + self.decorated_handler = mock.CoroutineMock() + + async def test_options_request(self): + self.request = mock.MagicMock( + __getitem__=lambda _, k: self.request_dict[k], headers={}, method="OPTIONS" + ) + decor_func = tenant_authentication(self.decorated_handler) + await decor_func(self.request) + self.decorated_handler.assert_called_once_with(self.request) + + async def test_insecure_mode(self): + self.profile.settings["admin.admin_insecure_mode"] = True + decor_func = tenant_authentication(self.decorated_handler) + await decor_func(self.request) + self.decorated_handler.assert_called_once_with(self.request) + + async def test_single_tenant_invalid_api_key(self): + self.profile.settings["multitenant.enabled"] = False + self.request = mock.MagicMock( + __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "wrong-key"}, + method="POST", + ) + decor_func = tenant_authentication(self.decorated_handler) + with self.assertRaises(web.HTTPUnauthorized): + await decor_func(self.request) + + async def test_single_tenant_valid_api_key(self): + self.profile.settings["multitenant.enabled"] = False + self.request = mock.MagicMock( + __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "admin_api_key"}, + method="POST", + ) + decor_func = tenant_authentication(self.decorated_handler) + await decor_func(self.request) + self.decorated_handler.assert_called_once_with(self.request) + + async def test_multi_tenant_missing_auth_header(self): + self.request = mock.MagicMock( + __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "wrong-key"}, + method="POST", + ) + decor_func = tenant_authentication(self.decorated_handler) + with self.assertRaises(web.HTTPUnauthorized): + await decor_func(self.request) + + async def test_multi_tenant_valid_auth_header(self): + self.request = mock.MagicMock( + __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "admin_api_key", "Authorization": "Bearer my-jwt"}, + method="POST", + ) + decor_func = tenant_authentication(self.decorated_handler) + await decor_func(self.request) + self.decorated_handler.assert_called_once_with(self.request) diff --git a/aries_cloudagent/anoncreds/routes.py b/aries_cloudagent/anoncreds/routes.py index 316cc208ba..2eec01d12e 100644 --- a/aries_cloudagent/anoncreds/routes.py +++ b/aries_cloudagent/anoncreds/routes.py @@ -13,6 +13,7 @@ ) from marshmallow import fields +from ..admin.decorators.auth import tenant_authentication from ..admin.request_context import AdminRequestContext from ..core.event_bus import EventBus from ..ledger.error import LedgerError @@ -145,6 +146,7 @@ class SchemaPostRequestSchema(OpenAPISchema): @docs(tags=["anoncreds - schemas"], summary="Create a schema on the connected ledger") @request_schema(SchemaPostRequestSchema()) @response_schema(SchemaResultSchema(), 200, description="") +@tenant_authentication async def schemas_post(request: web.BaseRequest): """Request handler for creating a schema. @@ -216,6 +218,7 @@ async def schemas_post(request: web.BaseRequest): @docs(tags=["anoncreds - schemas"], summary="Retrieve an individual schemas details") @match_info_schema(SchemaIdMatchInfo()) @response_schema(GetSchemaResultSchema(), 200, description="") +@tenant_authentication async def schema_get(request: web.BaseRequest): """Request handler for getting a schema. @@ -245,6 +248,7 @@ async def schema_get(request: web.BaseRequest): @docs(tags=["anoncreds - schemas"], summary="Retrieve all schema ids") @querystring_schema(SchemasQueryStringSchema()) @response_schema(GetSchemasResponseSchema(), 200, description="") +@tenant_authentication async def schemas_get(request: web.BaseRequest): """Request handler for getting all schemas. @@ -388,6 +392,7 @@ class CredDefsQueryStringSchema(OpenAPISchema): ) @request_schema(CredDefPostRequestSchema()) @response_schema(CredDefResultSchema(), 200, description="") +@tenant_authentication async def cred_def_post(request: web.BaseRequest): """Request handler for creating . @@ -439,6 +444,7 @@ async def cred_def_post(request: web.BaseRequest): ) @match_info_schema(CredIdMatchInfo()) @response_schema(GetCredDefResultSchema(), 200, description="") +@tenant_authentication async def cred_def_get(request: web.BaseRequest): """Request handler for getting credential definition. @@ -486,6 +492,7 @@ class GetCredDefsResponseSchema(OpenAPISchema): ) @querystring_schema(CredDefsQueryStringSchema()) @response_schema(GetCredDefsResponseSchema(), 200, description="") +@tenant_authentication async def cred_defs_get(request: web.BaseRequest): """Request handler for getting all credential definitions. @@ -576,6 +583,7 @@ class RevRegCreateRequestSchemaAnoncreds(OpenAPISchema): ) @request_schema(RevRegCreateRequestSchemaAnoncreds()) @response_schema(RevRegDefResultSchema(), 200, description="") +@tenant_authentication async def rev_reg_def_post(request: web.BaseRequest): """Request handler for creating revocation registry definition.""" context: AdminRequestContext = request["context"] @@ -659,6 +667,7 @@ class RevListCreateRequestSchema(OpenAPISchema): ) @request_schema(RevListCreateRequestSchema()) @response_schema(RevListResultSchema(), 200, description="") +@tenant_authentication async def rev_list_post(request: web.BaseRequest): """Request handler for creating registering a revocation list.""" context: AdminRequestContext = request["context"] @@ -694,6 +703,7 @@ async def rev_list_post(request: web.BaseRequest): ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(RevocationModuleResponseSchema(), description="") +@tenant_authentication async def upload_tails_file(request: web.BaseRequest): """Request handler to upload local tails file for revocation registry. @@ -729,6 +739,7 @@ async def upload_tails_file(request: web.BaseRequest): ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(RevocationModuleResponseSchema(), description="") +@tenant_authentication async def set_active_registry(request: web.BaseRequest): """Request handler to set the active registry. diff --git a/aries_cloudagent/anoncreds/tests/test_revocation_setup.py b/aries_cloudagent/anoncreds/tests/test_revocation_setup.py index e21c09f305..24a6f471a6 100644 --- a/aries_cloudagent/anoncreds/tests/test_revocation_setup.py +++ b/aries_cloudagent/anoncreds/tests/test_revocation_setup.py @@ -37,7 +37,6 @@ async def asyncSetUp(self) -> None: async def test_on_cred_def_support_revocation_registers_revocation_def( self, mock_register_revocation_registry_definition ): - event = CredDefFinishedEvent( CredDefFinishedPayload( schema_id="schema_id", @@ -60,7 +59,6 @@ async def test_on_cred_def_support_revocation_registers_revocation_def( async def test_on_cred_def_author_with_auto_create_rev_reg_config_registers_reg_def( self, mock_register_revocation_registry_definition ): - self.profile.settings["endorser.author"] = True self.profile.settings["endorser.auto_create_rev_reg"] = True event = CredDefFinishedEvent( @@ -85,7 +83,6 @@ async def test_on_cred_def_author_with_auto_create_rev_reg_config_registers_reg_ async def test_on_cred_def_author_with_auto_create_rev_reg_config_and_support_revoc_option_registers_reg_def( self, mock_register_revocation_registry_definition ): - self.profile.settings["endorser.author"] = True self.profile.settings["endorser.auto_create_rev_reg"] = True event = CredDefFinishedEvent( @@ -110,7 +107,6 @@ async def test_on_cred_def_author_with_auto_create_rev_reg_config_and_support_re async def test_on_cred_def_not_author_or_support_rev_option( self, mock_register_revocation_registry_definition ): - event = CredDefFinishedEvent( CredDefFinishedPayload( schema_id="schema_id", diff --git a/aries_cloudagent/anoncreds/tests/test_routes.py b/aries_cloudagent/anoncreds/tests/test_routes.py index 5e90463b54..288f7f7eba 100644 --- a/aries_cloudagent/anoncreds/tests/test_routes.py +++ b/aries_cloudagent/anoncreds/tests/test_routes.py @@ -54,7 +54,10 @@ class TestAnoncredsRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self) -> None: self.session_inject = {} self.profile = InMemoryProfile.test_profile( - settings={"wallet.type": "askar-anoncreds"}, + settings={ + "wallet.type": "askar-anoncreds", + "admin.admin_api_key": "secret-key", + }, profile_class=AskarAnoncredsProfile, ) self.context = AdminRequestContext.test_context( @@ -69,6 +72,7 @@ async def asyncSetUp(self) -> None: query={}, __getitem__=lambda _, k: self.request_dict[k], context=self.context, + headers={"x-api-key": "secret-key"}, ) @mock.patch.object( @@ -342,7 +346,7 @@ async def test_set_active_registry(self, mock_set): async def test_schema_endpoints_wrong_profile_403(self): self.profile = InMemoryProfile.test_profile( - settings={"wallet-type": "askar"}, + settings={"wallet-type": "askar", "admin.admin_api_key": "secret-key"}, profile_class=AskarProfile, ) self.context = AdminRequestContext.test_context({}, self.profile) @@ -355,6 +359,7 @@ async def test_schema_endpoints_wrong_profile_403(self): query={}, __getitem__=lambda _, k: self.request_dict[k], context=self.context, + headers={"x-api-key": "secret-key"}, ) # POST schema @@ -382,7 +387,7 @@ async def test_schema_endpoints_wrong_profile_403(self): async def test_cred_def_endpoints_wrong_profile_403(self): self.profile = InMemoryProfile.test_profile( - settings={"wallet-type": "askar"}, + settings={"wallet-type": "askar", "admin.admin_api_key": "secret-key"}, profile_class=AskarProfile, ) self.context = AdminRequestContext.test_context({}, self.profile) @@ -395,6 +400,7 @@ async def test_cred_def_endpoints_wrong_profile_403(self): query={}, __getitem__=lambda _, k: self.request_dict[k], context=self.context, + headers={"x-api-key": "secret-key"}, ) # POST cred def @@ -425,7 +431,7 @@ async def test_cred_def_endpoints_wrong_profile_403(self): async def test_rev_reg_wrong_profile_403(self): self.profile = InMemoryProfile.test_profile( - settings={"wallet-type": "askar"}, + settings={"wallet-type": "askar", "admin.admin_api_key": "secret-key"}, profile_class=AskarProfile, ) self.context = AdminRequestContext.test_context({}, self.profile) @@ -438,6 +444,7 @@ async def test_rev_reg_wrong_profile_403(self): query={}, __getitem__=lambda _, k: self.request_dict[k], context=self.context, + headers={"x-api-key": "secret-key"}, ) self.request.json = mock.CoroutineMock( @@ -458,7 +465,7 @@ async def test_rev_reg_wrong_profile_403(self): async def test_rev_list_wrong_profile_403(self): self.profile = InMemoryProfile.test_profile( - settings={"wallet-type": "askar"}, + settings={"wallet-type": "askar", "admin.admin_api_key": "secret-key"}, profile_class=AskarProfile, ) self.context = AdminRequestContext.test_context({}, self.profile) @@ -471,6 +478,7 @@ async def test_rev_list_wrong_profile_403(self): query={}, __getitem__=lambda _, k: self.request_dict[k], context=self.context, + headers={"x-api-key": "secret-key"}, ) self.request.json = mock.CoroutineMock( @@ -481,7 +489,7 @@ async def test_rev_list_wrong_profile_403(self): async def test_uploads_tails_wrong_profile_403(self): self.profile = InMemoryProfile.test_profile( - settings={"wallet-type": "askar"}, + settings={"wallet-type": "askar", "admin.admin_api_key": "secret-key"}, profile_class=AskarProfile, ) self.context = AdminRequestContext.test_context({}, self.profile) @@ -494,6 +502,7 @@ async def test_uploads_tails_wrong_profile_403(self): query={}, __getitem__=lambda _, k: self.request_dict[k], context=self.context, + headers={"x-api-key": "secret-key"}, ) self.request.match_info = {"rev_reg_id": "rev_reg_id"} @@ -502,7 +511,7 @@ async def test_uploads_tails_wrong_profile_403(self): async def test_active_registry_wrong_profile_403(self): self.profile = InMemoryProfile.test_profile( - settings={"wallet-type": "askar"}, + settings={"wallet-type": "askar", "admin.admin_api_key": "secret-key"}, profile_class=AskarProfile, ) self.context = AdminRequestContext.test_context({}, self.profile) @@ -515,6 +524,7 @@ async def test_active_registry_wrong_profile_403(self): query={}, __getitem__=lambda _, k: self.request_dict[k], context=self.context, + headers={"x-api-key": "secret-key"}, ) self.request.match_info = {"rev_reg_id": "rev_reg_id"} diff --git a/aries_cloudagent/config/argparse.py b/aries_cloudagent/config/argparse.py index ed8b1fe948..860ba19dd1 100644 --- a/aries_cloudagent/config/argparse.py +++ b/aries_cloudagent/config/argparse.py @@ -640,14 +640,16 @@ def add_arguments(self, parser: ArgumentParser): "resolver instance." ), ) - parser.add_argument( - "--universal-resolver-bearer-token", - type=str, - nargs="?", - metavar="", - env_var="ACAPY_UNIVERSAL_RESOLVER_BEARER_TOKEN", - help="Bearer token if universal resolver instance requires authentication.", - ), + ( + parser.add_argument( + "--universal-resolver-bearer-token", + type=str, + nargs="?", + metavar="", + env_var="ACAPY_UNIVERSAL_RESOLVER_BEARER_TOKEN", + help="Bearer token if universal resolver instance requires authentication.", # noqa: E501 + ), + ) def get_settings(self, args: Namespace) -> dict: """Extract general settings.""" diff --git a/aries_cloudagent/holder/routes.py b/aries_cloudagent/holder/routes.py index 7b7ac9a198..97d354497d 100644 --- a/aries_cloudagent/holder/routes.py +++ b/aries_cloudagent/holder/routes.py @@ -10,9 +10,9 @@ request_schema, response_schema, ) - from marshmallow import fields +from ..admin.decorators.auth import tenant_authentication from ..admin.request_context import AdminRequestContext from ..indy.holder import IndyHolder, IndyHolderError from ..indy.models.cred_precis import IndyCredInfoSchema @@ -193,6 +193,7 @@ class CredRevokedResultSchema(OpenAPISchema): @docs(tags=["credentials"], summary="Fetch credential from wallet by id") @match_info_schema(HolderCredIdMatchInfoSchema()) @response_schema(IndyCredInfoSchema(), 200, description="") +@tenant_authentication async def credentials_get(request: web.BaseRequest): """Request handler for retrieving credential. @@ -220,6 +221,7 @@ async def credentials_get(request: web.BaseRequest): @match_info_schema(HolderCredIdMatchInfoSchema()) @querystring_schema(CredRevokedQueryStringSchema()) @response_schema(CredRevokedResultSchema(), 200, description="") +@tenant_authentication async def credentials_revoked(request: web.BaseRequest): """Request handler for querying revocation status of credential. @@ -263,6 +265,7 @@ async def credentials_revoked(request: web.BaseRequest): @docs(tags=["credentials"], summary="Get attribute MIME types from wallet") @match_info_schema(HolderCredIdMatchInfoSchema()) @response_schema(AttributeMimeTypesResultSchema(), 200, description="") +@tenant_authentication async def credentials_attr_mime_types_get(request: web.BaseRequest): """Request handler for getting credential attribute MIME types. @@ -285,6 +288,7 @@ async def credentials_attr_mime_types_get(request: web.BaseRequest): @docs(tags=["credentials"], summary="Remove credential from wallet by id") @match_info_schema(HolderCredIdMatchInfoSchema()) @response_schema(HolderModuleResponseSchema(), description="") +@tenant_authentication async def credentials_remove(request: web.BaseRequest): """Request handler for searching connection records. @@ -316,6 +320,7 @@ async def credentials_remove(request: web.BaseRequest): ) @querystring_schema(CredentialsListQueryStringSchema()) @response_schema(CredInfoListSchema(), 200, description="") +@tenant_authentication async def credentials_list(request: web.BaseRequest): """Request handler for searching credential records. @@ -354,6 +359,7 @@ async def credentials_list(request: web.BaseRequest): ) @match_info_schema(HolderCredIdMatchInfoSchema()) @response_schema(VCRecordSchema(), 200, description="") +@tenant_authentication async def w3c_cred_get(request: web.BaseRequest): """Request handler for retrieving W3C credential. @@ -385,6 +391,7 @@ async def w3c_cred_get(request: web.BaseRequest): ) @match_info_schema(HolderCredIdMatchInfoSchema()) @response_schema(HolderModuleResponseSchema(), 200, description="") +@tenant_authentication async def w3c_cred_remove(request: web.BaseRequest): """Request handler for deleting W3C credential. @@ -422,6 +429,7 @@ async def w3c_cred_remove(request: web.BaseRequest): @request_schema(W3CCredentialsListRequestSchema()) @querystring_schema(CredentialsListQueryStringSchema()) @response_schema(VCRecordListSchema(), 200, description="") +@tenant_authentication async def w3c_creds_list(request: web.BaseRequest): """Request handler for searching W3C credential records. diff --git a/aries_cloudagent/holder/tests/test_routes.py b/aries_cloudagent/holder/tests/test_routes.py index 88323ce8e0..be0f941d07 100644 --- a/aries_cloudagent/holder/tests/test_routes.py +++ b/aries_cloudagent/holder/tests/test_routes.py @@ -1,15 +1,13 @@ import json +from unittest import IsolatedAsyncioTestCase from aries_cloudagent.tests import mock -from unittest import IsolatedAsyncioTestCase from ...core.in_memory import InMemoryProfile -from ...ledger.base import BaseLedger - from ...indy.holder import IndyHolder +from ...ledger.base import BaseLedger from ...storage.vc_holder.base import VCHolder from ...storage.vc_holder.vc_record import VCRecord - from .. import routes as test_module VC_RECORD = VCRecord( @@ -33,7 +31,11 @@ class TestHolderRoutes(IsolatedAsyncioTestCase): def setUp(self): - self.profile = InMemoryProfile.test_profile() + self.profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) self.context = self.profile.context setattr(self.context, "profile", self.profile) @@ -43,6 +45,7 @@ def setUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_credentials_get(self): diff --git a/aries_cloudagent/ledger/routes.py b/aries_cloudagent/ledger/routes.py index 4d1f270afe..8fbf8ee102 100644 --- a/aries_cloudagent/ledger/routes.py +++ b/aries_cloudagent/ledger/routes.py @@ -11,9 +11,9 @@ request_schema, response_schema, ) - from marshmallow import fields, validate +from ..admin.decorators.auth import tenant_authentication from ..admin.request_context import AdminRequestContext from ..connections.models.conn_record import ConnRecord from ..messaging.models.base import BaseModelError @@ -262,6 +262,7 @@ class WriteLedgerRequestSchema(OpenAPISchema): @querystring_schema(CreateDidTxnForEndorserOptionSchema()) @querystring_schema(SchemaConnIdMatchInfoSchema()) @response_schema(TxnOrRegisterLedgerNymResponseSchema(), 200, description="") +@tenant_authentication async def register_ledger_nym(request: web.BaseRequest): """Request handler for registering a NYM with the ledger. @@ -425,6 +426,7 @@ async def register_ledger_nym(request: web.BaseRequest): ) @querystring_schema(QueryStringDIDSchema) @response_schema(GetNymRoleResponseSchema(), 200, description="") +@tenant_authentication async def get_nym_role(request: web.BaseRequest): """Request handler for getting the role from the NYM registration of a public DID. @@ -471,6 +473,7 @@ async def get_nym_role(request: web.BaseRequest): @docs(tags=["ledger"], summary="Rotate key pair for public DID.") @response_schema(LedgerModulesResultSchema(), 200, description="") +@tenant_authentication async def rotate_public_did_keypair(request: web.BaseRequest): """Request handler for rotating key pair associated with public DID. @@ -500,6 +503,7 @@ async def rotate_public_did_keypair(request: web.BaseRequest): ) @querystring_schema(QueryStringDIDSchema()) @response_schema(GetDIDVerkeyResponseSchema(), 200, description="") +@tenant_authentication async def get_did_verkey(request: web.BaseRequest): """Request handler for getting a verkey for a DID from the ledger. @@ -548,6 +552,7 @@ async def get_did_verkey(request: web.BaseRequest): ) @querystring_schema(QueryStringEndpointSchema()) @response_schema(GetDIDEndpointResponseSchema(), 200, description="") +@tenant_authentication async def get_did_endpoint(request: web.BaseRequest): """Request handler for getting a verkey for a DID from the ledger. @@ -593,6 +598,7 @@ async def get_did_endpoint(request: web.BaseRequest): @docs(tags=["ledger"], summary="Fetch the current transaction author agreement, if any") @response_schema(TAAResultSchema, 200, description="") +@tenant_authentication async def ledger_get_taa(request: web.BaseRequest): """Request handler for fetching the transaction author agreement. @@ -633,6 +639,7 @@ async def ledger_get_taa(request: web.BaseRequest): @docs(tags=["ledger"], summary="Accept the transaction author agreement") @request_schema(TAAAcceptSchema) @response_schema(LedgerModulesResultSchema(), 200, description="") +@tenant_authentication async def ledger_accept_taa(request: web.BaseRequest): """Request handler for accepting the current transaction author agreement. @@ -693,6 +700,7 @@ async def ledger_accept_taa(request: web.BaseRequest): @docs(tags=["ledger"], summary="Fetch list of available write ledgers") @response_schema(ConfigurableWriteLedgersSchema, 200, description="") +@tenant_authentication async def get_write_ledgers(request: web.BaseRequest): """Request handler for fetching the list of available write ledgers. @@ -714,6 +722,7 @@ async def get_write_ledgers(request: web.BaseRequest): @docs(tags=["ledger"], summary="Fetch the current write ledger") @response_schema(WriteLedgerSchema, 200, description="") +@tenant_authentication async def get_write_ledger(request: web.BaseRequest): """Request handler for fetching the currently set write ledger. @@ -739,6 +748,7 @@ async def get_write_ledger(request: web.BaseRequest): @docs(tags=["ledger"], summary="Set write ledger") @match_info_schema(WriteLedgerRequestSchema()) @response_schema(WriteLedgerSchema, 200, description="") +@tenant_authentication async def set_write_ledger(request: web.BaseRequest): """Request handler for setting write ledger. @@ -769,6 +779,7 @@ async def set_write_ledger(request: web.BaseRequest): tags=["ledger"], summary="Fetch the multiple ledger configuration currently in use" ) @response_schema(LedgerConfigListSchema, 200, description="") +@tenant_authentication async def get_ledger_config(request: web.BaseRequest): """Request handler for fetching the ledger configuration list in use. diff --git a/aries_cloudagent/ledger/tests/test_routes.py b/aries_cloudagent/ledger/tests/test_routes.py index 4347823376..8a26a72fdf 100644 --- a/aries_cloudagent/ledger/tests/test_routes.py +++ b/aries_cloudagent/ledger/tests/test_routes.py @@ -1,30 +1,33 @@ from typing import Tuple - from unittest import IsolatedAsyncioTestCase + from aries_cloudagent.tests import mock +from ...connections.models.conn_record import ConnRecord from ...core.in_memory import InMemoryProfile from ...ledger.base import BaseLedger from ...ledger.endpoint_type import EndpointType -from ...ledger.multiple_ledger.ledger_requests_executor import ( - IndyLedgerRequestsExecutor, -) from ...ledger.multiple_ledger.base_manager import ( BaseMultipleLedgerManager, ) +from ...ledger.multiple_ledger.ledger_requests_executor import ( + IndyLedgerRequestsExecutor, +) from ...multitenant.base import BaseMultitenantManager from ...multitenant.manager import MultitenantManager - from .. import routes as test_module from ..indy import Role -from ...connections.models.conn_record import ConnRecord class TestLedgerRoutes(IsolatedAsyncioTestCase): def setUp(self): self.ledger = mock.create_autospec(BaseLedger) self.ledger.pool_name = "pool.0" - self.profile = InMemoryProfile.test_profile() + self.profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) self.context = self.profile.context setattr(self.context, "profile", self.profile) self.profile.context.injector.bind_instance(BaseLedger, self.ledger) @@ -37,6 +40,7 @@ def setUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) self.test_did = "did" diff --git a/aries_cloudagent/messaging/credential_definitions/routes.py b/aries_cloudagent/messaging/credential_definitions/routes.py index f70bac14e5..6a84a89ca5 100644 --- a/aries_cloudagent/messaging/credential_definitions/routes.py +++ b/aries_cloudagent/messaging/credential_definitions/routes.py @@ -16,6 +16,7 @@ ) from marshmallow import fields +from ...admin.decorators.auth import tenant_authentication from ...admin.request_context import AdminRequestContext from ...connections.models.conn_record import ConnRecord from ...core.event_bus import Event, EventBus @@ -183,6 +184,7 @@ class CredDefConnIdMatchInfoSchema(OpenAPISchema): @querystring_schema(CreateCredDefTxnForEndorserOptionSchema()) @querystring_schema(CredDefConnIdMatchInfoSchema()) @response_schema(TxnOrCredentialDefinitionSendResultSchema(), 200, description="") +@tenant_authentication async def credential_definitions_send_credential_definition(request: web.BaseRequest): """Request handler for sending a credential definition to the ledger. @@ -378,6 +380,7 @@ async def credential_definitions_send_credential_definition(request: web.BaseReq ) @querystring_schema(CredDefQueryStringSchema()) @response_schema(CredentialDefinitionsCreatedResultSchema(), 200, description="") +@tenant_authentication async def credential_definitions_created(request: web.BaseRequest): """Request handler for retrieving credential definitions that current agent created. @@ -412,6 +415,7 @@ async def credential_definitions_created(request: web.BaseRequest): ) @match_info_schema(CredDefIdMatchInfoSchema()) @response_schema(CredentialDefinitionGetResultSchema(), 200, description="") +@tenant_authentication async def credential_definitions_get_credential_definition(request: web.BaseRequest): """Request handler for getting a credential definition from the ledger. @@ -462,6 +466,7 @@ async def credential_definitions_get_credential_definition(request: web.BaseRequ ) @match_info_schema(CredDefIdMatchInfoSchema()) @response_schema(CredentialDefinitionGetResultSchema(), 200, description="") +@tenant_authentication async def credential_definitions_fix_cred_def_wallet_record(request: web.BaseRequest): """Request handler for fixing a credential definition wallet non-secret record. diff --git a/aries_cloudagent/messaging/credential_definitions/tests/test_routes.py b/aries_cloudagent/messaging/credential_definitions/tests/test_routes.py index b88e7bf2fa..90043bdfef 100644 --- a/aries_cloudagent/messaging/credential_definitions/tests/test_routes.py +++ b/aries_cloudagent/messaging/credential_definitions/tests/test_routes.py @@ -23,7 +23,11 @@ class TestCredentialDefinitionRoutes(IsolatedAsyncioTestCase): def setUp(self): self.session_inject = {} - self.profile = InMemoryProfile.test_profile() + self.profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) self.profile_injector = self.profile.context.injector self.ledger = mock.create_autospec(BaseLedger) @@ -61,6 +65,7 @@ def setUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_send_credential_definition(self): @@ -391,7 +396,7 @@ async def test_get_credential_definition_no_ledger(self): async def test_credential_definition_endpoints_wrong_profile_403(self): self.profile = InMemoryProfile.test_profile( - settings={"wallet-type": "askar"}, + settings={"wallet-type": "askar", "admin.admin_api_key": "secret-key"}, profile_class=AskarAnoncredsProfile, ) self.context = AdminRequestContext.test_context({}, self.profile) @@ -404,6 +409,7 @@ async def test_credential_definition_endpoints_wrong_profile_403(self): query={}, __getitem__=lambda _, k: self.request_dict[k], context=self.context, + headers={"x-api-key": "secret-key"}, ) self.request.json = mock.CoroutineMock( return_value={ diff --git a/aries_cloudagent/messaging/jsonld/routes.py b/aries_cloudagent/messaging/jsonld/routes.py index 12c8105571..72cfd62fcb 100644 --- a/aries_cloudagent/messaging/jsonld/routes.py +++ b/aries_cloudagent/messaging/jsonld/routes.py @@ -2,10 +2,10 @@ from aiohttp import web from aiohttp_apispec import docs, request_schema, response_schema -from pydid.verification_method import Ed25519VerificationKey2018 - from marshmallow import INCLUDE, Schema, fields +from pydid.verification_method import Ed25519VerificationKey2018 +from ...admin.decorators.auth import tenant_authentication from ...admin.request_context import AdminRequestContext from ...config.base import InjectionError from ...resolver.base import ResolverError @@ -66,6 +66,7 @@ class SignResponseSchema(OpenAPISchema): ) @request_schema(SignRequestSchema()) @response_schema(SignResponseSchema(), 200, description="") +@tenant_authentication async def sign(request: web.BaseRequest): """Request handler for signing a jsonld doc. @@ -130,6 +131,7 @@ class VerifyResponseSchema(OpenAPISchema): ) @request_schema(VerifyRequestSchema()) @response_schema(VerifyResponseSchema(), 200, description="") +@tenant_authentication async def verify(request: web.BaseRequest): """Request handler for signing a jsonld doc. diff --git a/aries_cloudagent/messaging/jsonld/tests/test_routes.py b/aries_cloudagent/messaging/jsonld/tests/test_routes.py index b36a21b162..f48afb3dcb 100644 --- a/aries_cloudagent/messaging/jsonld/tests/test_routes.py +++ b/aries_cloudagent/messaging/jsonld/tests/test_routes.py @@ -1,15 +1,16 @@ -from copy import deepcopy import json +from copy import deepcopy +from unittest import IsolatedAsyncioTestCase +import pytest from aiohttp import web -from unittest import IsolatedAsyncioTestCase -from aries_cloudagent.tests import mock from pyld import jsonld -import pytest -from .. import routes as test_module +from aries_cloudagent.tests import mock + from ....admin.request_context import AdminRequestContext from ....config.base import InjectionError +from ....core.in_memory import InMemoryProfile from ....resolver.base import DIDMethodNotSupported, DIDNotFound, ResolverError from ....resolver.did_resolver import DIDResolver from ....vc.ld_proofs.document_loader import DocumentLoader @@ -17,6 +18,7 @@ from ....wallet.did_method import SOV, DIDMethods from ....wallet.error import WalletError from ....wallet.key_type import ED25519 +from .. import routes as test_module from ..error import ( BadJWSHeaderError, DroppedAttributeError, @@ -84,7 +86,12 @@ def mock_verify_credential(): @pytest.fixture def mock_sign_request(mock_sign_credential): - context = AdminRequestContext.test_context() + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + context = AdminRequestContext.test_context({}, profile) outbound_message_router = mock.CoroutineMock() request_dict = { "context": context, @@ -110,6 +117,7 @@ def mock_sign_request(mock_sign_credential): }, ), __getitem__=lambda _, k: request_dict[k], + headers={"x-api-key": "secret-key"}, ) yield request @@ -137,7 +145,14 @@ def request_body(): @pytest.fixture def mock_verify_request(mock_verify_credential, mock_resolver, request_body): def _mock_verify_request(request_body=request_body): - context = AdminRequestContext.test_context({DIDResolver: mock_resolver}) + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + context = AdminRequestContext.test_context( + {DIDResolver: mock_resolver}, profile + ) outbound_message_router = mock.CoroutineMock() request_dict = { "context": context, @@ -148,6 +163,7 @@ def _mock_verify_request(request_body=request_body): query={}, json=mock.CoroutineMock(return_value=request_body), __getitem__=lambda _, k: request_dict[k], + headers={"x-api-key": "secret-key"}, ) return request @@ -270,7 +286,12 @@ def test_post_process_routes(): class TestJSONLDRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self): - self.context = AdminRequestContext.test_context() + self.profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context({}, self.profile) self.context.profile.context.injector.bind_instance( DocumentLoader, custom_document_loader ) @@ -287,6 +308,7 @@ async def asyncSetUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_verify_credential(self): diff --git a/aries_cloudagent/messaging/schemas/routes.py b/aries_cloudagent/messaging/schemas/routes.py index 5b8fa38147..6d4e50f9c2 100644 --- a/aries_cloudagent/messaging/schemas/routes.py +++ b/aries_cloudagent/messaging/schemas/routes.py @@ -15,6 +15,7 @@ from marshmallow import fields from marshmallow.validate import Regexp +from ...admin.decorators.auth import tenant_authentication from ...admin.request_context import AdminRequestContext from ...connections.models.conn_record import ConnRecord from ...core.event_bus import Event, EventBus @@ -166,6 +167,7 @@ class SchemaConnIdMatchInfoSchema(OpenAPISchema): @querystring_schema(CreateSchemaTxnForEndorserOptionSchema()) @querystring_schema(SchemaConnIdMatchInfoSchema()) @response_schema(TxnOrSchemaSendResultSchema(), 200, description="") +@tenant_authentication async def schemas_send_schema(request: web.BaseRequest): """Request handler for creating a schema. @@ -340,6 +342,7 @@ async def schemas_send_schema(request: web.BaseRequest): ) @querystring_schema(SchemaQueryStringSchema()) @response_schema(SchemasCreatedResultSchema(), 200, description="") +@tenant_authentication async def schemas_created(request: web.BaseRequest): """Request handler for retrieving schemas that current agent created. @@ -369,6 +372,7 @@ async def schemas_created(request: web.BaseRequest): @docs(tags=["schema"], summary="Gets a schema from the ledger") @match_info_schema(SchemaIdMatchInfoSchema()) @response_schema(SchemaGetResultSchema(), 200, description="") +@tenant_authentication async def schemas_get_schema(request: web.BaseRequest): """Request handler for sending a credential offer. @@ -419,6 +423,7 @@ async def schemas_get_schema(request: web.BaseRequest): @docs(tags=["schema"], summary="Writes a schema non-secret record to the wallet") @match_info_schema(SchemaIdMatchInfoSchema()) @response_schema(SchemaGetResultSchema(), 200, description="") +@tenant_authentication async def schemas_fix_schema_wallet_record(request: web.BaseRequest): """Request handler for fixing a schema's wallet non-secrets records. diff --git a/aries_cloudagent/messaging/schemas/tests/test_routes.py b/aries_cloudagent/messaging/schemas/tests/test_routes.py index 6e42b4c190..411a951d9e 100644 --- a/aries_cloudagent/messaging/schemas/tests/test_routes.py +++ b/aries_cloudagent/messaging/schemas/tests/test_routes.py @@ -22,7 +22,11 @@ class TestSchemaRoutes(IsolatedAsyncioTestCase): def setUp(self): self.session_inject = {} - self.profile = InMemoryProfile.test_profile() + self.profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) self.profile_injector = self.profile.context.injector self.ledger = mock.create_autospec(BaseLedger) self.ledger.__aenter__ = mock.CoroutineMock(return_value=self.ledger) @@ -54,6 +58,7 @@ def setUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_send_schema(self): @@ -402,7 +407,7 @@ async def test_get_schema_x_ledger(self): async def test_schema_endpoints_wrong_profile_403(self): self.profile = InMemoryProfile.test_profile( - settings={"wallet-type": "askar"}, + settings={"wallet-type": "askar", "admin.admin_api_key": "secret-key"}, profile_class=AskarAnoncredsProfile, ) self.context = AdminRequestContext.test_context({}, self.profile) @@ -415,6 +420,7 @@ async def test_schema_endpoints_wrong_profile_403(self): query={}, __getitem__=lambda _, k: self.request_dict[k], context=self.context, + headers={"x-api-key": "secret-key"}, ) self.request.json = mock.CoroutineMock( diff --git a/aries_cloudagent/multitenant/admin/routes.py b/aries_cloudagent/multitenant/admin/routes.py index 4948a7e518..1e84020d98 100644 --- a/aries_cloudagent/multitenant/admin/routes.py +++ b/aries_cloudagent/multitenant/admin/routes.py @@ -10,6 +10,7 @@ ) from marshmallow import ValidationError, fields, validate, validates_schema +from ...admin.decorators.auth import admin_authentication from ...admin.request_context import AdminRequestContext from ...core.error import BaseError from ...core.profile import ProfileManagerProvider @@ -363,6 +364,7 @@ class WalletListQueryStringSchema(OpenAPISchema): @docs(tags=["multitenancy"], summary="Query subwallets") @querystring_schema(WalletListQueryStringSchema()) @response_schema(WalletListSchema(), 200, description="") +@admin_authentication async def wallets_list(request: web.BaseRequest): """Request handler for listing all internal subwallets. @@ -392,6 +394,7 @@ async def wallets_list(request: web.BaseRequest): @docs(tags=["multitenancy"], summary="Get a single subwallet") @match_info_schema(WalletIdMatchInfoSchema()) @response_schema(WalletRecordSchema(), 200, description="") +@admin_authentication async def wallet_get(request: web.BaseRequest): """Request handler for getting a single subwallet. @@ -422,6 +425,7 @@ async def wallet_get(request: web.BaseRequest): @docs(tags=["multitenancy"], summary="Create a subwallet") @request_schema(CreateWalletRequestSchema) @response_schema(CreateWalletResponseSchema(), 200, description="") +@admin_authentication async def wallet_create(request: web.BaseRequest): """Request handler for adding a new subwallet for handling by the agent. @@ -495,6 +499,7 @@ async def wallet_create(request: web.BaseRequest): @match_info_schema(WalletIdMatchInfoSchema()) @request_schema(UpdateWalletRequestSchema) @response_schema(WalletRecordSchema(), 200, description="") +@admin_authentication async def wallet_update(request: web.BaseRequest): """Request handler for updating a existing subwallet for handling by the agent. @@ -559,6 +564,7 @@ async def wallet_update(request: web.BaseRequest): @docs(tags=["multitenancy"], summary="Get auth token for a subwallet") @request_schema(CreateWalletTokenRequestSchema) @response_schema(CreateWalletTokenResponseSchema(), 200, description="") +@admin_authentication async def wallet_create_token(request: web.BaseRequest): """Request handler for creating an authorization token for a specific subwallet. @@ -603,6 +609,7 @@ async def wallet_create_token(request: web.BaseRequest): @match_info_schema(WalletIdMatchInfoSchema()) @request_schema(RemoveWalletRequestSchema) @response_schema(MultitenantModuleResponseSchema(), 200, description="") +@admin_authentication async def wallet_remove(request: web.BaseRequest): """Request handler to remove a subwallet from agent and storage. diff --git a/aries_cloudagent/multitenant/admin/tests/test_routes.py b/aries_cloudagent/multitenant/admin/tests/test_routes.py index 9f4a6d32ba..7576591968 100644 --- a/aries_cloudagent/multitenant/admin/tests/test_routes.py +++ b/aries_cloudagent/multitenant/admin/tests/test_routes.py @@ -24,7 +24,7 @@ async def asyncSetUp(self): return_value=self.mock_multitenant_mgr ) self.profile = InMemoryProfile.test_profile( - settings={"wallet.type": "askar"}, + settings={"wallet.type": "askar", "admin.admin_api_key": "secret-key"}, profile_class=AskarProfile, ) self.context = AdminRequestContext.test_context({}, self.profile) @@ -45,13 +45,18 @@ async def asyncSetUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_format_wallet_record_removes_wallet_key(self): wallet_record = WalletRecord( wallet_id="test", key_management_mode=WalletRecord.MODE_MANAGED, - settings={"wallet.name": "wallet_name", "wallet.key": "wallet_key"}, + settings={ + "wallet.name": "wallet_name", + "wallet.key": "wallet_key", + "admin.admin_api_key": "secret-key", + }, ) formatted = test_module.format_wallet_record(wallet_record) diff --git a/aries_cloudagent/protocols/actionmenu/v1_0/routes.py b/aries_cloudagent/protocols/actionmenu/v1_0/routes.py index c9c94af9d6..802fa75ebf 100644 --- a/aries_cloudagent/protocols/actionmenu/v1_0/routes.py +++ b/aries_cloudagent/protocols/actionmenu/v1_0/routes.py @@ -4,9 +4,9 @@ from aiohttp import web from aiohttp_apispec import docs, match_info_schema, request_schema, response_schema - from marshmallow import fields +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....connections.models.conn_record import ConnRecord from ....messaging.models.base import BaseModelError @@ -95,6 +95,7 @@ class ActionMenuFetchResultSchema(OpenAPISchema): ) @match_info_schema(MenuConnIdMatchInfoSchema()) @response_schema(ActionMenuModulesResultSchema(), 200, description="") +@tenant_authentication async def actionmenu_close(request: web.BaseRequest): """Request handler for closing the menu associated with a connection. @@ -122,6 +123,7 @@ async def actionmenu_close(request: web.BaseRequest): @docs(tags=["action-menu"], summary="Fetch the active menu") @match_info_schema(MenuConnIdMatchInfoSchema()) @response_schema(ActionMenuFetchResultSchema(), 200, description="") +@tenant_authentication async def actionmenu_fetch(request: web.BaseRequest): """Request handler for fetching the previously-received menu for a connection. @@ -141,6 +143,7 @@ async def actionmenu_fetch(request: web.BaseRequest): @match_info_schema(MenuConnIdMatchInfoSchema()) @request_schema(PerformRequestSchema()) @response_schema(ActionMenuModulesResultSchema(), 200, description="") +@tenant_authentication async def actionmenu_perform(request: web.BaseRequest): """Request handler for performing a menu action. @@ -170,6 +173,7 @@ async def actionmenu_perform(request: web.BaseRequest): @docs(tags=["action-menu"], summary="Request the active menu") @match_info_schema(MenuConnIdMatchInfoSchema()) @response_schema(ActionMenuModulesResultSchema(), 200, description="") +@tenant_authentication async def actionmenu_request(request: web.BaseRequest): """Request handler for requesting a menu from the connection target. @@ -200,6 +204,7 @@ async def actionmenu_request(request: web.BaseRequest): @match_info_schema(MenuConnIdMatchInfoSchema()) @request_schema(SendMenuSchema()) @response_schema(ActionMenuModulesResultSchema(), 200, description="") +@tenant_authentication async def actionmenu_send(request: web.BaseRequest): """Request handler for requesting a menu from the connection target. diff --git a/aries_cloudagent/protocols/actionmenu/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/actionmenu/v1_0/tests/test_routes.py index 0d157842e8..31a1e00d85 100644 --- a/aries_cloudagent/protocols/actionmenu/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/actionmenu/v1_0/tests/test_routes.py @@ -1,16 +1,22 @@ from unittest import IsolatedAsyncioTestCase + from aries_cloudagent.tests import mock from .....admin.request_context import AdminRequestContext +from .....core.in_memory import InMemoryProfile from .....storage.error import StorageNotFoundError - from .. import routes as test_module class TestActionMenuRoutes(IsolatedAsyncioTestCase): def setUp(self): self.session_inject = {} - self.context = AdminRequestContext.test_context(self.session_inject) + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(self.session_inject, profile) self.request_dict = { "context": self.context, "outbound_message_router": mock.CoroutineMock(), @@ -20,6 +26,7 @@ def setUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_actionmenu_close(self): diff --git a/aries_cloudagent/protocols/basicmessage/v1_0/routes.py b/aries_cloudagent/protocols/basicmessage/v1_0/routes.py index 7fffa930e1..015318eb5f 100644 --- a/aries_cloudagent/protocols/basicmessage/v1_0/routes.py +++ b/aries_cloudagent/protocols/basicmessage/v1_0/routes.py @@ -2,9 +2,9 @@ from aiohttp import web from aiohttp_apispec import docs, match_info_schema, request_schema, response_schema - from marshmallow import fields +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....connections.models.conn_record import ConnRecord from ....messaging.models.openapi import OpenAPISchema @@ -39,6 +39,7 @@ class BasicConnIdMatchInfoSchema(OpenAPISchema): @match_info_schema(BasicConnIdMatchInfoSchema()) @request_schema(SendMessageSchema()) @response_schema(BasicMessageModuleResponseSchema(), 200, description="") +@tenant_authentication async def connections_send_message(request: web.BaseRequest): """Request handler for sending a basic message to a connection. diff --git a/aries_cloudagent/protocols/basicmessage/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/basicmessage/v1_0/tests/test_routes.py index de3373f053..7d6c5b069c 100644 --- a/aries_cloudagent/protocols/basicmessage/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/basicmessage/v1_0/tests/test_routes.py @@ -1,16 +1,22 @@ from unittest import IsolatedAsyncioTestCase + from aries_cloudagent.tests import mock from .....admin.request_context import AdminRequestContext +from .....core.in_memory import InMemoryProfile from .....storage.error import StorageNotFoundError - from .. import routes as test_module class TestBasicMessageRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self): self.session_inject = {} - self.context = AdminRequestContext.test_context(self.session_inject) + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(self.session_inject, profile) self.request_dict = { "context": self.context, "outbound_message_router": mock.CoroutineMock(), @@ -20,6 +26,7 @@ async def asyncSetUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) self.test_conn_id = "connection-id" diff --git a/aries_cloudagent/protocols/connections/v1_0/routes.py b/aries_cloudagent/protocols/connections/v1_0/routes.py index 5d3a0c6e66..e067b547af 100644 --- a/aries_cloudagent/protocols/connections/v1_0/routes.py +++ b/aries_cloudagent/protocols/connections/v1_0/routes.py @@ -11,9 +11,9 @@ request_schema, response_schema, ) - from marshmallow import fields, validate, validates_schema +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....cache.base import BaseCache from ....connections.models.conn_record import ConnRecord, ConnRecordSchema @@ -22,13 +22,13 @@ from ....messaging.valid import ( ENDPOINT_EXAMPLE, ENDPOINT_VALIDATE, + GENERIC_DID_VALIDATE, INDY_DID_EXAMPLE, INDY_DID_VALIDATE, INDY_RAW_PUBLIC_KEY_EXAMPLE, INDY_RAW_PUBLIC_KEY_VALIDATE, UUID4_EXAMPLE, UUID4_VALIDATE, - GENERIC_DID_VALIDATE, ) from ....storage.error import StorageError, StorageNotFoundError from ....wallet.error import WalletError @@ -430,6 +430,7 @@ def connection_sort_key(conn): ) @querystring_schema(ConnectionsListQueryStringSchema()) @response_schema(ConnectionListSchema(), 200, description="") +@tenant_authentication async def connections_list(request: web.BaseRequest): """Request handler for searching connection records. @@ -484,6 +485,7 @@ async def connections_list(request: web.BaseRequest): @docs(tags=["connection"], summary="Fetch a single connection record") @match_info_schema(ConnectionsConnIdMatchInfoSchema()) @response_schema(ConnRecordSchema(), 200, description="") +@tenant_authentication async def connections_retrieve(request: web.BaseRequest): """Request handler for fetching a single connection record. @@ -513,6 +515,7 @@ async def connections_retrieve(request: web.BaseRequest): @docs(tags=["connection"], summary="Fetch connection remote endpoint") @match_info_schema(ConnectionsConnIdMatchInfoSchema()) @response_schema(EndpointsResultSchema(), 200, description="") +@tenant_authentication async def connections_endpoints(request: web.BaseRequest): """Request handler for fetching connection endpoints. @@ -542,6 +545,7 @@ async def connections_endpoints(request: web.BaseRequest): @match_info_schema(ConnectionsConnIdMatchInfoSchema()) @querystring_schema(ConnectionMetadataQuerySchema()) @response_schema(ConnectionMetadataSchema(), 200, description="") +@tenant_authentication async def connections_metadata(request: web.BaseRequest): """Handle fetching metadata associated with a single connection record.""" context: AdminRequestContext = request["context"] @@ -568,6 +572,7 @@ async def connections_metadata(request: web.BaseRequest): @match_info_schema(ConnectionsConnIdMatchInfoSchema()) @request_schema(ConnectionMetadataSetRequestSchema()) @response_schema(ConnectionMetadataSchema(), 200, description="") +@tenant_authentication async def connections_metadata_set(request: web.BaseRequest): """Handle fetching metadata associated with a single connection record.""" context: AdminRequestContext = request["context"] @@ -597,6 +602,7 @@ async def connections_metadata_set(request: web.BaseRequest): @querystring_schema(CreateInvitationQueryStringSchema()) @request_schema(CreateInvitationRequestSchema()) @response_schema(InvitationResultSchema(), 200, description="") +@tenant_authentication async def connections_create_invitation(request: web.BaseRequest): """Request handler for creating a new connection invitation. @@ -671,6 +677,7 @@ async def connections_create_invitation(request: web.BaseRequest): @querystring_schema(ReceiveInvitationQueryStringSchema()) @request_schema(ReceiveInvitationRequestSchema()) @response_schema(ConnRecordSchema(), 200, description="") +@tenant_authentication async def connections_receive_invitation(request: web.BaseRequest): """Request handler for receiving a new connection invitation. @@ -713,6 +720,7 @@ async def connections_receive_invitation(request: web.BaseRequest): @match_info_schema(ConnectionsConnIdMatchInfoSchema()) @querystring_schema(AcceptInvitationQueryStringSchema()) @response_schema(ConnRecordSchema(), 200, description="") +@tenant_authentication async def connections_accept_invitation(request: web.BaseRequest): """Request handler for accepting a stored connection invitation. @@ -764,6 +772,7 @@ async def connections_accept_invitation(request: web.BaseRequest): @match_info_schema(ConnectionsConnIdMatchInfoSchema()) @querystring_schema(AcceptRequestQueryStringSchema()) @response_schema(ConnRecordSchema(), 200, description="") +@tenant_authentication async def connections_accept_request(request: web.BaseRequest): """Request handler for accepting a stored connection request. @@ -798,6 +807,7 @@ async def connections_accept_request(request: web.BaseRequest): @docs(tags=["connection"], summary="Remove an existing connection record") @match_info_schema(ConnectionsConnIdMatchInfoSchema()) @response_schema(ConnectionModuleResponseSchema, 200, description="") +@tenant_authentication async def connections_remove(request: web.BaseRequest): """Request handler for removing a connection record. @@ -826,6 +836,7 @@ async def connections_remove(request: web.BaseRequest): @docs(tags=["connection"], summary="Create a new static connection") @request_schema(ConnectionStaticRequestSchema()) @response_schema(ConnectionStaticResultSchema(), 200, description="") +@tenant_authentication async def connections_create_static(request: web.BaseRequest): """Request handler for creating a new static connection. diff --git a/aries_cloudagent/protocols/connections/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/connections/v1_0/tests/test_routes.py index d880f17e59..d561e8f0a0 100644 --- a/aries_cloudagent/protocols/connections/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/connections/v1_0/tests/test_routes.py @@ -1,22 +1,27 @@ import json - -from unittest.mock import ANY from unittest import IsolatedAsyncioTestCase +from unittest.mock import ANY + from aries_cloudagent.tests import mock from .....admin.request_context import AdminRequestContext from .....cache.base import BaseCache from .....cache.in_memory import InMemoryCache from .....connections.models.conn_record import ConnRecord +from .....core.in_memory import InMemoryProfile from .....storage.error import StorageNotFoundError - from .. import routes as test_module class TestConnectionRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self): self.session_inject = {} - self.context = AdminRequestContext.test_context(self.session_inject) + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(self.session_inject, profile) self.request_dict = { "context": self.context, "outbound_message_router": mock.CoroutineMock(), @@ -26,6 +31,7 @@ async def asyncSetUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_connections_list(self): diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/routes.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/routes.py index dd4e100081..8d8c97231e 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/v1_0/routes.py +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/routes.py @@ -8,9 +8,9 @@ request_schema, response_schema, ) - from marshmallow import fields, validate +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....connections.models.conn_record import ConnRecord from ....messaging.models.base import BaseModelError @@ -169,6 +169,7 @@ def mediation_sort_key(mediation: dict): ) @querystring_schema(MediationListQueryStringSchema()) @response_schema(MediationListSchema(), 200) +@tenant_authentication async def list_mediation_requests(request: web.BaseRequest): """List mediation requests for either client or server role.""" context: AdminRequestContext = request["context"] @@ -194,6 +195,7 @@ async def list_mediation_requests(request: web.BaseRequest): @docs(tags=["mediation"], summary="Retrieve mediation request record") @match_info_schema(MediationIdMatchInfoSchema()) @response_schema(MediationRecordSchema(), 200) +@tenant_authentication async def retrieve_mediation_request(request: web.BaseRequest): """Retrieve a single mediation request.""" context: AdminRequestContext = request["context"] @@ -216,6 +218,7 @@ async def retrieve_mediation_request(request: web.BaseRequest): @docs(tags=["mediation"], summary="Delete mediation request by ID") @match_info_schema(MediationIdMatchInfoSchema()) @response_schema(MediationRecordSchema, 200) +@tenant_authentication async def delete_mediation_request(request: web.BaseRequest): """Delete a mediation request by ID.""" context: AdminRequestContext = request["context"] @@ -241,6 +244,7 @@ async def delete_mediation_request(request: web.BaseRequest): @match_info_schema(ConnectionsConnIdMatchInfoSchema()) @request_schema(MediationCreateRequestSchema()) @response_schema(MediationRecordSchema(), 201) +@tenant_authentication async def request_mediation(request: web.BaseRequest): """Request mediation from connection.""" context: AdminRequestContext = request["context"] @@ -280,6 +284,7 @@ async def request_mediation(request: web.BaseRequest): @docs(tags=["mediation"], summary="Grant received mediation") @match_info_schema(MediationIdMatchInfoSchema()) @response_schema(MediationGrantSchema(), 201) +@tenant_authentication async def mediation_request_grant(request: web.BaseRequest): """Grant a stored mediation request.""" context: AdminRequestContext = request["context"] @@ -303,6 +308,7 @@ async def mediation_request_grant(request: web.BaseRequest): @match_info_schema(MediationIdMatchInfoSchema()) @request_schema(AdminMediationDenySchema()) @response_schema(MediationDenySchema(), 201) +@tenant_authentication async def mediation_request_deny(request: web.BaseRequest): """Deny a stored mediation request.""" context: AdminRequestContext = request["context"] @@ -329,6 +335,7 @@ async def mediation_request_deny(request: web.BaseRequest): ) @querystring_schema(GetKeylistQuerySchema()) @response_schema(KeylistSchema(), 200) +@tenant_authentication async def get_keylist(request: web.BaseRequest): """Retrieve keylists by connection or role.""" context: AdminRequestContext = request["context"] @@ -358,6 +365,7 @@ async def get_keylist(request: web.BaseRequest): @querystring_schema(KeylistQueryPaginateQuerySchema()) @request_schema(KeylistQueryFilterRequestSchema()) @response_schema(KeylistQuerySchema(), 201) +@tenant_authentication async def send_keylist_query(request: web.BaseRequest): """Send keylist query to mediator.""" context: AdminRequestContext = request["context"] @@ -394,6 +402,7 @@ async def send_keylist_query(request: web.BaseRequest): @match_info_schema(MediationIdMatchInfoSchema()) @request_schema(KeylistUpdateRequestSchema()) @response_schema(KeylistUpdateSchema(), 201) +@tenant_authentication async def send_keylist_update(request: web.BaseRequest): """Send keylist update to mediator.""" context: AdminRequestContext = request["context"] @@ -439,6 +448,7 @@ async def send_keylist_update(request: web.BaseRequest): @docs(tags=["mediation"], summary="Get default mediator") @response_schema(MediationRecordSchema(), 200) +@tenant_authentication async def get_default_mediator(request: web.BaseRequest): """Get default mediator.""" context: AdminRequestContext = request["context"] @@ -455,6 +465,7 @@ async def get_default_mediator(request: web.BaseRequest): @docs(tags=["mediation"], summary="Set default mediator") @match_info_schema(MediationIdMatchInfoSchema()) @response_schema(MediationRecordSchema(), 201) +@tenant_authentication async def set_default_mediator(request: web.BaseRequest): """Set default mediator.""" context: AdminRequestContext = request["context"] @@ -471,6 +482,7 @@ async def set_default_mediator(request: web.BaseRequest): @docs(tags=["mediation"], summary="Clear default mediator") @response_schema(MediationRecordSchema(), 201) +@tenant_authentication async def clear_default_mediator(request: web.BaseRequest): """Clear set default mediator.""" context: AdminRequestContext = request["context"] @@ -489,6 +501,7 @@ async def clear_default_mediator(request: web.BaseRequest): @request_schema(MediationIdMatchInfoSchema()) # TODO Fix this response so that it adequately represents Optionals @response_schema(KeylistUpdateSchema(), 200) +@tenant_authentication async def update_keylist_for_connection(request: web.BaseRequest): """Update keylist for a connection.""" context: AdminRequestContext = request["context"] diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_routes.py index bdf9911c32..2b3ecd3c04 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_routes.py @@ -1,18 +1,23 @@ -from aries_cloudagent.tests import mock from unittest import IsolatedAsyncioTestCase -from .. import routes as test_module +from aries_cloudagent.tests import mock + from .....admin.request_context import AdminRequestContext from .....core.in_memory import InMemoryProfile from .....storage.error import StorageError, StorageNotFoundError +from .....wallet.did_method import DIDMethods +from .. import routes as test_module from ..models.mediation_record import MediationRecord from ..route_manager import RouteManager -from .....wallet.did_method import DIDMethods class TestCoordinateMediationRoutes(IsolatedAsyncioTestCase): def setUp(self): - self.profile = InMemoryProfile.test_profile() + self.profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) self.profile.context.injector.bind_instance(DIDMethods, DIDMethods()) self.context = AdminRequestContext.test_context(profile=self.profile) self.outbound_message_router = mock.CoroutineMock() @@ -28,6 +33,7 @@ def setUp(self): query={}, json=mock.CoroutineMock(return_value={}), __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) serialized = { "mediation_id": "fake_id", diff --git a/aries_cloudagent/protocols/did_rotate/v1_0/messages/tests/test_rotate.py b/aries_cloudagent/protocols/did_rotate/v1_0/messages/tests/test_rotate.py index 66e1558fce..b26c65785c 100644 --- a/aries_cloudagent/protocols/did_rotate/v1_0/messages/tests/test_rotate.py +++ b/aries_cloudagent/protocols/did_rotate/v1_0/messages/tests/test_rotate.py @@ -8,7 +8,6 @@ class TestRotate(TestCase): - def test_init_type(self): """Test initializer.""" diff --git a/aries_cloudagent/protocols/did_rotate/v1_0/routes.py b/aries_cloudagent/protocols/did_rotate/v1_0/routes.py index be72612b7f..f441ded27b 100644 --- a/aries_cloudagent/protocols/did_rotate/v1_0/routes.py +++ b/aries_cloudagent/protocols/did_rotate/v1_0/routes.py @@ -6,6 +6,7 @@ from aiohttp_apispec import docs, json_schema, match_info_schema, response_schema from marshmallow import fields +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....connections.models.conn_record import ConnRecord from ....messaging.models.openapi import OpenAPISchema @@ -46,6 +47,7 @@ class DIDRotateRequestJSONSchema(OpenAPISchema): @response_schema( RotateMessageSchema(), 200, description="Rotate agent message for observer" ) +@tenant_authentication async def rotate(request: web.BaseRequest): """Request to rotate a DID.""" @@ -77,6 +79,7 @@ async def rotate(request: web.BaseRequest): @response_schema( HangupMessageSchema(), 200, description="Hangup agent message for observer" ) +@tenant_authentication async def hangup(request: web.BaseRequest): """Hangup a DID rotation.""" diff --git a/aries_cloudagent/protocols/did_rotate/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/did_rotate/v1_0/tests/test_routes.py index a4b68f08c4..f596005cf3 100644 --- a/aries_cloudagent/protocols/did_rotate/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/did_rotate/v1_0/tests/test_routes.py @@ -2,12 +2,13 @@ from unittest import IsolatedAsyncioTestCase from .....admin.request_context import AdminRequestContext +from .....core.in_memory import InMemoryProfile from .....protocols.didcomm_prefix import DIDCommPrefix from .....storage.error import StorageNotFoundError from .....tests import mock -from ..messages import Hangup, Rotate from .. import message_types as test_message_types from .. import routes as test_module +from ..messages import Hangup, Rotate from ..tests import MockConnRecord, test_conn_id test_valid_rotate_request = { @@ -28,8 +29,12 @@ def generate_mock_rotate_message(): class TestDIDRotateRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self): self.session_inject = {} - - self.context = AdminRequestContext.test_context(self.session_inject) + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(self.session_inject, profile) self.request_dict = { "context": self.context, "outbound_message_router": mock.CoroutineMock(), @@ -39,6 +44,7 @@ async def asyncSetUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) @mock.patch.object( @@ -107,7 +113,6 @@ async def test_rotate_conn_not_found(self): "retrieve_by_id", mock.CoroutineMock(side_effect=StorageNotFoundError()), ) as mock_retrieve_by_id: - with self.assertRaises(test_module.web.HTTPNotFound): await test_module.rotate(self.request) diff --git a/aries_cloudagent/protocols/didexchange/v1_0/routes.py b/aries_cloudagent/protocols/didexchange/v1_0/routes.py index 7826aacbd4..f9c21e8266 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/routes.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/routes.py @@ -12,6 +12,7 @@ ) from marshmallow import fields, validate +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....connections.models.conn_record import ConnRecord, ConnRecordSchema from ....messaging.models.base import BaseModelError @@ -238,7 +239,8 @@ class DIDXRejectRequestSchema(OpenAPISchema): @match_info_schema(DIDXConnIdMatchInfoSchema()) @querystring_schema(DIDXAcceptInvitationQueryStringSchema()) @response_schema(ConnRecordSchema(), 200, description="") -async def didx_accept_invitation(request: web.Request): +@tenant_authentication +async def didx_accept_invitation(request: web.BaseRequest): """Request handler for accepting a stored connection invitation. Args: @@ -300,6 +302,7 @@ async def didx_accept_invitation(request: web.Request): ) @querystring_schema(DIDXCreateRequestImplicitQueryStringSchema()) @response_schema(ConnRecordSchema(), 200, description="") +@tenant_authentication async def didx_create_request_implicit(request: web.BaseRequest): """Request handler for creating and sending a request to an implicit invitation. @@ -358,6 +361,7 @@ async def didx_create_request_implicit(request: web.BaseRequest): @querystring_schema(DIDXReceiveRequestImplicitQueryStringSchema()) @request_schema(DIDXRequestSchema()) @response_schema(ConnRecordSchema(), 200, description="") +@tenant_authentication async def didx_receive_request_implicit(request: web.BaseRequest): """Request handler for receiving a request against public DID's implicit invitation. @@ -400,6 +404,7 @@ async def didx_receive_request_implicit(request: web.BaseRequest): @match_info_schema(DIDXConnIdMatchInfoSchema()) @querystring_schema(DIDXAcceptRequestQueryStringSchema()) @response_schema(ConnRecordSchema(), 200, description="") +@tenant_authentication async def didx_accept_request(request: web.BaseRequest): """Request handler for accepting a stored connection request. @@ -445,6 +450,7 @@ async def didx_accept_request(request: web.BaseRequest): @match_info_schema(DIDXConnIdMatchInfoSchema()) @request_schema(DIDXRejectRequestSchema()) @response_schema(ConnRecordSchema(), 200, description="") +@tenant_authentication async def didx_reject(request: web.BaseRequest): """Abandon or reject a DID Exchange.""" context: AdminRequestContext = request["context"] diff --git a/aries_cloudagent/protocols/didexchange/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/didexchange/v1_0/tests/test_routes.py index 2888c91166..a06edc2bcb 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/tests/test_routes.py @@ -1,16 +1,23 @@ from unittest import IsolatedAsyncioTestCase + from aries_cloudagent.tests import mock -from .. import routes as test_module from .....admin.request_context import AdminRequestContext +from .....core.in_memory import InMemoryProfile from .....storage.error import StorageNotFoundError from ....coordinate_mediation.v1_0.route_manager import RouteManager +from .. import routes as test_module class TestDIDExchangeConnRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self): self.session_inject = {} - self.context = AdminRequestContext.test_context(self.session_inject) + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(self.session_inject, profile) self.profile = self.context.profile self.request_dict = { "context": self.context, @@ -21,6 +28,7 @@ async def asyncSetUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) self.profile.context.injector.bind_instance(RouteManager, mock.MagicMock()) diff --git a/aries_cloudagent/protocols/discovery/v1_0/routes.py b/aries_cloudagent/protocols/discovery/v1_0/routes.py index f9d282b4cc..04416b8045 100644 --- a/aries_cloudagent/protocols/discovery/v1_0/routes.py +++ b/aries_cloudagent/protocols/discovery/v1_0/routes.py @@ -2,9 +2,9 @@ from aiohttp import web from aiohttp_apispec import docs, querystring_schema, response_schema - from marshmallow import fields +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....messaging.models.base import BaseModelError from ....messaging.models.openapi import OpenAPISchema @@ -66,6 +66,7 @@ class QueryDiscoveryExchRecordsSchema(OpenAPISchema): ) @querystring_schema(QueryFeaturesQueryStringSchema()) @response_schema(V10DiscoveryRecordSchema(), 200, description="") +@tenant_authentication async def query_features(request: web.BaseRequest): """Request handler for creating and sending feature query. @@ -96,6 +97,7 @@ async def query_features(request: web.BaseRequest): ) @querystring_schema(QueryDiscoveryExchRecordsSchema()) @response_schema(V10DiscoveryExchangeListResultSchema(), 200, description="") +@tenant_authentication async def query_records(request: web.BaseRequest): """Request handler for looking up V10DiscoveryExchangeRecord. diff --git a/aries_cloudagent/protocols/discovery/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/discovery/v1_0/tests/test_routes.py index ce4a4a34e6..4af4af5a8c 100644 --- a/aries_cloudagent/protocols/discovery/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/discovery/v1_0/tests/test_routes.py @@ -1,10 +1,10 @@ from unittest import IsolatedAsyncioTestCase -from aries_cloudagent.tests import mock +from aries_cloudagent.tests import mock from .....admin.request_context import AdminRequestContext +from .....core.in_memory import InMemoryProfile from .....storage.error import StorageError - from .. import routes as test_module from ..manager import V10DiscoveryMgr from ..messages.query import Query @@ -14,7 +14,12 @@ class TestDiscoveryRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self): self.session_inject = {} - self.context = AdminRequestContext.test_context(self.session_inject) + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(self.session_inject, profile) self.profile = self.context.profile self.request_dict = { "context": self.context, @@ -25,6 +30,7 @@ async def asyncSetUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_query_features(self): diff --git a/aries_cloudagent/protocols/discovery/v2_0/routes.py b/aries_cloudagent/protocols/discovery/v2_0/routes.py index aeac69a424..bf2adb78b0 100644 --- a/aries_cloudagent/protocols/discovery/v2_0/routes.py +++ b/aries_cloudagent/protocols/discovery/v2_0/routes.py @@ -2,9 +2,9 @@ from aiohttp import web from aiohttp_apispec import docs, querystring_schema, response_schema - from marshmallow import fields +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....messaging.models.base import BaseModelError from ....messaging.models.openapi import OpenAPISchema @@ -76,6 +76,7 @@ class QueryDiscoveryExchRecordsSchema(OpenAPISchema): ) @querystring_schema(QueryFeaturesQueryStringSchema()) @response_schema(V20DiscoveryExchangeResultSchema(), 200, description="") +@tenant_authentication async def query_features(request: web.BaseRequest): """Request handler for creating and sending feature queries. @@ -106,6 +107,7 @@ async def query_features(request: web.BaseRequest): ) @querystring_schema(QueryDiscoveryExchRecordsSchema()) @response_schema(V20DiscoveryExchangeListResultSchema(), 200, description="") +@tenant_authentication async def query_records(request: web.BaseRequest): """Request handler for looking up V20DiscoveryExchangeRecord. diff --git a/aries_cloudagent/protocols/discovery/v2_0/tests/test_routes.py b/aries_cloudagent/protocols/discovery/v2_0/tests/test_routes.py index d6c5ecd2f6..bcd542227e 100644 --- a/aries_cloudagent/protocols/discovery/v2_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/discovery/v2_0/tests/test_routes.py @@ -1,10 +1,10 @@ from unittest import IsolatedAsyncioTestCase -from aries_cloudagent.tests import mock +from aries_cloudagent.tests import mock from .....admin.request_context import AdminRequestContext +from .....core.in_memory import InMemoryProfile from .....storage.error import StorageError - from .. import routes as test_module from ..manager import V20DiscoveryMgr from ..messages.queries import Queries, QueryItem @@ -14,7 +14,12 @@ class TestDiscoveryRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self): self.session_inject = {} - self.context = AdminRequestContext.test_context(self.session_inject) + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(self.session_inject, profile) self.profile = self.context.profile self.request_dict = { "context": self.context, @@ -25,6 +30,7 @@ async def asyncSetUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_query_features(self): diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py index 5631c161e2..f4ab0f2ebc 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py @@ -12,6 +12,7 @@ ) from marshmallow import fields, validate +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....connections.models.conn_record import ConnRecord from ....core.event_bus import Event, EventBus @@ -124,6 +125,7 @@ class EndorserInfoSchema(OpenAPISchema): ) @querystring_schema(TransactionsListQueryStringSchema()) @response_schema(TransactionListSchema(), 200) +@tenant_authentication async def transactions_list(request: web.BaseRequest): """Request handler for searching transaction records. @@ -153,6 +155,7 @@ async def transactions_list(request: web.BaseRequest): @docs(tags=["endorse-transaction"], summary="Fetch a single transaction record") @match_info_schema(TranIdMatchInfoSchema()) @response_schema(TransactionRecordSchema(), 200) +@tenant_authentication async def transactions_retrieve(request: web.BaseRequest): """Request handler for fetching a single transaction record. @@ -186,6 +189,7 @@ async def transactions_retrieve(request: web.BaseRequest): @querystring_schema(TranIdMatchInfoSchema()) @request_schema(DateSchema()) @response_schema(TransactionRecordSchema(), 200) +@tenant_authentication async def transaction_create_request(request: web.BaseRequest): """Request handler for creating a new transaction record and request. @@ -276,6 +280,7 @@ async def transaction_create_request(request: web.BaseRequest): @querystring_schema(EndorserDIDInfoSchema()) @match_info_schema(TranIdMatchInfoSchema()) @response_schema(TransactionRecordSchema(), 200) +@tenant_authentication async def endorse_transaction_response(request: web.BaseRequest): """Request handler for creating an endorsed transaction response. @@ -347,6 +352,7 @@ async def endorse_transaction_response(request: web.BaseRequest): ) @match_info_schema(TranIdMatchInfoSchema()) @response_schema(TransactionRecordSchema(), 200) +@tenant_authentication async def refuse_transaction_response(request: web.BaseRequest): """Request handler for creating a refused transaction response. @@ -413,6 +419,7 @@ async def refuse_transaction_response(request: web.BaseRequest): ) @match_info_schema(TranIdMatchInfoSchema()) @response_schema(TransactionRecordSchema(), 200) +@tenant_authentication async def cancel_transaction(request: web.BaseRequest): """Request handler for cancelling a Transaction request. @@ -477,6 +484,7 @@ async def cancel_transaction(request: web.BaseRequest): ) @match_info_schema(TranIdMatchInfoSchema()) @response_schema(TransactionRecordSchema(), 200) +@tenant_authentication async def transaction_resend(request: web.BaseRequest): """Request handler for resending a transaction request. @@ -541,6 +549,7 @@ async def transaction_resend(request: web.BaseRequest): @querystring_schema(AssignTransactionJobsSchema()) @match_info_schema(TransactionConnIdMatchInfoSchema()) @response_schema(TransactionJobsSchema(), 200) +@tenant_authentication async def set_endorser_role(request: web.BaseRequest): """Request handler for assigning transaction jobs. @@ -581,6 +590,7 @@ async def set_endorser_role(request: web.BaseRequest): @querystring_schema(EndorserInfoSchema()) @match_info_schema(TransactionConnIdMatchInfoSchema()) @response_schema(EndorserInfoSchema(), 200) +@tenant_authentication async def set_endorser_info(request: web.BaseRequest): """Request handler for assigning endorser information. @@ -644,6 +654,7 @@ async def set_endorser_info(request: web.BaseRequest): ) @match_info_schema(TranIdMatchInfoSchema()) @response_schema(TransactionRecordSchema(), 200) +@tenant_authentication async def transaction_write(request: web.BaseRequest): """Request handler for writing an endorsed transaction to the ledger. diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_routes.py index ad79131aa4..d924b93216 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_routes.py @@ -1,7 +1,7 @@ import asyncio import json - from unittest import IsolatedAsyncioTestCase + from aries_cloudagent.tests import mock from .....connections.models.conn_record import ConnRecord @@ -23,7 +23,11 @@ class TestEndorseTransactionRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self): - self.profile = InMemoryProfile.test_profile() + self.profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) self.context = self.profile.context setattr(self.context, "profile", self.profile) self.session = await self.profile.session() @@ -67,6 +71,7 @@ async def asyncSetUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) self.test_did = "sample-did" diff --git a/aries_cloudagent/protocols/introduction/v0_1/routes.py b/aries_cloudagent/protocols/introduction/v0_1/routes.py index ed1e9ea226..591b14811c 100644 --- a/aries_cloudagent/protocols/introduction/v0_1/routes.py +++ b/aries_cloudagent/protocols/introduction/v0_1/routes.py @@ -5,9 +5,9 @@ from aiohttp import web from aiohttp_apispec import docs, match_info_schema, querystring_schema, response_schema - from marshmallow import fields +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....messaging.models.openapi import OpenAPISchema from ....messaging.valid import UUID4_EXAMPLE @@ -53,6 +53,7 @@ class IntroConnIdMatchInfoSchema(OpenAPISchema): @match_info_schema(IntroConnIdMatchInfoSchema()) @querystring_schema(IntroStartQueryStringSchema()) @response_schema(IntroModuleResponseSchema, description="") +@tenant_authentication async def introduction_start(request: web.BaseRequest): """Request handler for starting an introduction. diff --git a/aries_cloudagent/protocols/introduction/v0_1/tests/test_routes.py b/aries_cloudagent/protocols/introduction/v0_1/tests/test_routes.py index 9ace9b497a..aa5b64437d 100644 --- a/aries_cloudagent/protocols/introduction/v0_1/tests/test_routes.py +++ b/aries_cloudagent/protocols/introduction/v0_1/tests/test_routes.py @@ -1,15 +1,21 @@ -from aries_cloudagent.tests import mock from unittest import IsolatedAsyncioTestCase -from .....admin.request_context import AdminRequestContext +from aries_cloudagent.tests import mock +from .....admin.request_context import AdminRequestContext +from .....core.in_memory import InMemoryProfile from .. import routes as test_module class TestIntroductionRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self): self.session_inject = {} - self.context = AdminRequestContext.test_context(self.session_inject) + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(self.session_inject, profile) self.request_dict = { "context": self.context, "outbound_message_router": mock.CoroutineMock(), @@ -19,6 +25,7 @@ async def asyncSetUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_introduction_start_no_service(self): diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/routes.py b/aries_cloudagent/protocols/issue_credential/v1_0/routes.py index 3d3b68c3d9..e05e039bea 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/routes.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/routes.py @@ -12,6 +12,7 @@ ) from marshmallow import fields, validate +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....connections.models.conn_record import ConnRecord from ....core.profile import Profile @@ -381,6 +382,7 @@ class V10CredentialExchangeAutoRemoveRequestSchema(OpenAPISchema): ) @querystring_schema(V10CredentialExchangeListQueryStringSchema) @response_schema(V10CredentialExchangeListResultSchema(), 200, description="") +@tenant_authentication async def credential_exchange_list(request: web.BaseRequest): """Request handler for searching credential exchange records. @@ -422,6 +424,7 @@ async def credential_exchange_list(request: web.BaseRequest): ) @match_info_schema(CredExIdMatchInfoSchema()) @response_schema(V10CredentialExchangeSchema(), 200, description="") +@tenant_authentication async def credential_exchange_retrieve(request: web.BaseRequest): """Request handler for fetching single credential exchange record. @@ -469,6 +472,7 @@ async def credential_exchange_retrieve(request: web.BaseRequest): ) @request_schema(V10CredentialCreateSchema()) @response_schema(V10CredentialExchangeSchema(), 200, description="") +@tenant_authentication async def credential_exchange_create(request: web.BaseRequest): """Request handler for creating a credential from attr values. @@ -548,6 +552,7 @@ async def credential_exchange_create(request: web.BaseRequest): ) @request_schema(V10CredentialProposalRequestMandSchema()) @response_schema(V10CredentialExchangeSchema(), 200, description="") +@tenant_authentication async def credential_exchange_send(request: web.BaseRequest): """Request handler for sending credential from issuer to holder from attr values. @@ -650,6 +655,7 @@ async def credential_exchange_send(request: web.BaseRequest): ) @request_schema(V10CredentialProposalRequestOptSchema()) @response_schema(V10CredentialExchangeSchema(), 200, description="") +@tenant_authentication async def credential_exchange_send_proposal(request: web.BaseRequest): """Request handler for sending credential proposal. @@ -773,6 +779,7 @@ async def _create_free_offer( ) @request_schema(V10CredentialConnFreeOfferRequestSchema()) @response_schema(V10CredentialExchangeSchema(), 200, description="") +@tenant_authentication async def credential_exchange_create_free_offer(request: web.BaseRequest): """Request handler for creating free credential offer. @@ -847,6 +854,7 @@ async def credential_exchange_create_free_offer(request: web.BaseRequest): ) @request_schema(V10CredentialFreeOfferRequestSchema()) @response_schema(V10CredentialExchangeSchema(), 200, description="") +@tenant_authentication async def credential_exchange_send_free_offer(request: web.BaseRequest): """Request handler for sending free credential offer. @@ -937,6 +945,7 @@ async def credential_exchange_send_free_offer(request: web.BaseRequest): @match_info_schema(CredExIdMatchInfoSchema()) @request_schema(V10CredentialBoundOfferRequestSchema()) @response_schema(V10CredentialExchangeSchema(), 200, description="") +@tenant_authentication async def credential_exchange_send_bound_offer(request: web.BaseRequest): """Request handler for sending bound credential offer. @@ -1037,6 +1046,7 @@ async def credential_exchange_send_bound_offer(request: web.BaseRequest): @match_info_schema(CredExIdMatchInfoSchema()) @request_schema(V10CredentialExchangeAutoRemoveRequestSchema()) @response_schema(V10CredentialExchangeSchema(), 200, description="") +@tenant_authentication async def credential_exchange_send_request(request: web.BaseRequest): """Request handler for sending credential request. @@ -1153,6 +1163,7 @@ async def credential_exchange_send_request(request: web.BaseRequest): @match_info_schema(CredExIdMatchInfoSchema()) @request_schema(V10CredentialIssueRequestSchema()) @response_schema(V10CredentialExchangeSchema(), 200, description="") +@tenant_authentication async def credential_exchange_issue(request: web.BaseRequest): """Request handler for sending credential. @@ -1249,6 +1260,7 @@ async def credential_exchange_issue(request: web.BaseRequest): @match_info_schema(CredExIdMatchInfoSchema()) @request_schema(V10CredentialStoreRequestSchema()) @response_schema(V10CredentialExchangeSchema(), 200, description="") +@tenant_authentication async def credential_exchange_store(request: web.BaseRequest): """Request handler for storing credential. @@ -1354,6 +1366,7 @@ async def credential_exchange_store(request: web.BaseRequest): @match_info_schema(CredExIdMatchInfoSchema()) @request_schema(V10CredentialProblemReportRequestSchema()) @response_schema(IssueCredentialModuleResponseSchema(), 200, description="") +@tenant_authentication async def credential_exchange_problem_report(request: web.BaseRequest): """Request handler for sending problem report. @@ -1400,6 +1413,7 @@ async def credential_exchange_problem_report(request: web.BaseRequest): ) @match_info_schema(CredExIdMatchInfoSchema()) @response_schema(IssueCredentialModuleResponseSchema(), 200, description="") +@tenant_authentication async def credential_exchange_remove(request: web.BaseRequest): """Request handler for removing a credential exchange record. diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_routes.py index e100a50a58..01c06e76a5 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_routes.py @@ -1,18 +1,23 @@ -from aries_cloudagent.tests import mock from unittest import IsolatedAsyncioTestCase +from aries_cloudagent.tests import mock + from .....admin.request_context import AdminRequestContext +from .....core.in_memory import InMemoryProfile from .....wallet.base import BaseWallet - from .. import routes as test_module - from . import CRED_DEF_ID class TestCredentialRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self): self.session_inject = {} - self.context = AdminRequestContext.test_context(self.session_inject) + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(self.session_inject, profile) self.request_dict = { "context": self.context, "outbound_message_router": mock.CoroutineMock(), @@ -22,6 +27,7 @@ async def asyncSetUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_credential_exchange_list(self): diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/routes.py b/aries_cloudagent/protocols/issue_credential/v2_0/routes.py index a966b63690..bf15b91df2 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/routes.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/routes.py @@ -14,6 +14,7 @@ ) from marshmallow import ValidationError, fields, validate, validates_schema +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....anoncreds.holder import AnonCredsHolderError from ....anoncreds.issuer import AnonCredsIssuerError @@ -543,6 +544,7 @@ def _format_result_with_details( ) @querystring_schema(V20CredExRecordListQueryStringSchema) @response_schema(V20CredExRecordListResultSchema(), 200, description="") +@tenant_authentication async def credential_exchange_list(request: web.BaseRequest): """Request handler for searching credential exchange records. @@ -590,6 +592,7 @@ async def credential_exchange_list(request: web.BaseRequest): ) @match_info_schema(V20CredExIdMatchInfoSchema()) @response_schema(V20CredExRecordDetailSchema(), 200, description="") +@tenant_authentication async def credential_exchange_retrieve(request: web.BaseRequest): """Request handler for fetching single credential exchange record. @@ -637,6 +640,7 @@ async def credential_exchange_retrieve(request: web.BaseRequest): ) @request_schema(V20IssueCredSchemaCore()) @response_schema(V20CredExRecordSchema(), 200, description="") +@tenant_authentication async def credential_exchange_create(request: web.BaseRequest): """Request handler for creating a credential from attr values. @@ -713,6 +717,7 @@ async def credential_exchange_create(request: web.BaseRequest): ) @request_schema(V20CredExFreeSchema()) @response_schema(V20CredExRecordSchema(), 200, description="") +@tenant_authentication async def credential_exchange_send(request: web.BaseRequest): """Request handler for sending credential from issuer to holder from attr values. @@ -829,6 +834,7 @@ async def credential_exchange_send(request: web.BaseRequest): ) @request_schema(V20CredExFreeSchema()) @response_schema(V20CredExRecordSchema(), 200, description="") +@tenant_authentication async def credential_exchange_send_proposal(request: web.BaseRequest): """Request handler for sending credential proposal. @@ -955,6 +961,7 @@ async def _create_free_offer( ) @request_schema(V20CredOfferConnFreeRequestSchema()) @response_schema(V20CredExRecordSchema(), 200, description="") +@tenant_authentication async def credential_exchange_create_free_offer(request: web.BaseRequest): """Request handler for creating free credential offer. @@ -1027,6 +1034,7 @@ async def credential_exchange_create_free_offer(request: web.BaseRequest): ) @request_schema(V20CredOfferRequestSchema()) @response_schema(V20CredExRecordSchema(), 200, description="") +@tenant_authentication async def credential_exchange_send_free_offer(request: web.BaseRequest): """Request handler for sending free credential offer. @@ -1119,6 +1127,7 @@ async def credential_exchange_send_free_offer(request: web.BaseRequest): @match_info_schema(V20CredExIdMatchInfoSchema()) @request_schema(V20CredBoundOfferRequestSchema()) @response_schema(V20CredExRecordSchema(), 200, description="") +@tenant_authentication async def credential_exchange_send_bound_offer(request: web.BaseRequest): """Request handler for sending bound credential offer. @@ -1230,6 +1239,7 @@ async def credential_exchange_send_bound_offer(request: web.BaseRequest): ) @request_schema(V20CredRequestFreeSchema()) @response_schema(V20CredExRecordSchema(), 200, description="") +@tenant_authentication async def credential_exchange_send_free_request(request: web.BaseRequest): """Request handler for sending free credential request. @@ -1328,6 +1338,7 @@ async def credential_exchange_send_free_request(request: web.BaseRequest): @match_info_schema(V20CredExIdMatchInfoSchema()) @request_schema(V20CredRequestRequestSchema()) @response_schema(V20CredExRecordSchema(), 200, description="") +@tenant_authentication async def credential_exchange_send_bound_request(request: web.BaseRequest): """Request handler for sending credential request. @@ -1447,6 +1458,7 @@ async def credential_exchange_send_bound_request(request: web.BaseRequest): @match_info_schema(V20CredExIdMatchInfoSchema()) @request_schema(V20CredIssueRequestSchema()) @response_schema(V20CredExRecordDetailSchema(), 200, description="") +@tenant_authentication async def credential_exchange_issue(request: web.BaseRequest): """Request handler for sending credential. @@ -1541,6 +1553,7 @@ async def credential_exchange_issue(request: web.BaseRequest): @match_info_schema(V20CredExIdMatchInfoSchema()) @request_schema(V20CredStoreRequestSchema()) @response_schema(V20CredExRecordDetailSchema(), 200, description="") +@tenant_authentication async def credential_exchange_store(request: web.BaseRequest): """Request handler for storing credential. @@ -1644,6 +1657,7 @@ async def credential_exchange_store(request: web.BaseRequest): ) @match_info_schema(V20CredExIdMatchInfoSchema()) @response_schema(V20IssueCredentialModuleResponseSchema(), 200, description="") +@tenant_authentication async def credential_exchange_remove(request: web.BaseRequest): """Request handler for removing a credential exchange record. @@ -1672,6 +1686,7 @@ async def credential_exchange_remove(request: web.BaseRequest): @match_info_schema(V20CredExIdMatchInfoSchema()) @request_schema(V20CredIssueProblemReportRequestSchema()) @response_schema(V20IssueCredentialModuleResponseSchema(), 200, description="") +@tenant_authentication async def credential_exchange_problem_report(request: web.BaseRequest): """Request handler for sending problem report. diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/tests/test_routes.py b/aries_cloudagent/protocols/issue_credential/v2_0/tests/test_routes.py index 96b282eff7..e25e088635 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/tests/test_routes.py @@ -1,14 +1,14 @@ -from .....vc.ld_proofs.error import LinkedDataProofException -from aries_cloudagent.tests import mock from unittest import IsolatedAsyncioTestCase -from .....admin.request_context import AdminRequestContext +from aries_cloudagent.tests import mock +from .....admin.request_context import AdminRequestContext +from .....core.in_memory import InMemoryProfile +from .....vc.ld_proofs.error import LinkedDataProofException from .. import routes as test_module from ..formats.indy.handler import IndyCredFormatHandler from ..formats.ld_proof.handler import LDProofCredFormatHandler from ..messages.cred_format import V20CredFormat - from . import ( LD_PROOF_VC_DETAIL, TEST_DID, @@ -18,7 +18,12 @@ class TestV20CredRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self): self.session_inject = {} - self.context = AdminRequestContext.test_context(self.session_inject) + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(self.session_inject, profile) self.request_dict = { "context": self.context, "outbound_message_router": mock.CoroutineMock(), @@ -28,6 +33,7 @@ async def asyncSetUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_validate_cred_filter_schema(self): diff --git a/aries_cloudagent/protocols/out_of_band/v1_0/routes.py b/aries_cloudagent/protocols/out_of_band/v1_0/routes.py index 96aeea265a..7fcd42c1e4 100644 --- a/aries_cloudagent/protocols/out_of_band/v1_0/routes.py +++ b/aries_cloudagent/protocols/out_of_band/v1_0/routes.py @@ -6,14 +6,15 @@ from aiohttp import web from aiohttp_apispec import ( docs, + match_info_schema, querystring_schema, request_schema, - match_info_schema, response_schema, ) from marshmallow import fields, validate from marshmallow.exceptions import ValidationError +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....messaging.models.base import BaseModelError from ....messaging.models.openapi import OpenAPISchema @@ -225,6 +226,7 @@ class InvitationRecordMatchInfoSchema(OpenAPISchema): @querystring_schema(InvitationCreateQueryStringSchema()) @request_schema(InvitationCreateRequestSchema()) @response_schema(InvitationRecordSchema(), description="") +@tenant_authentication async def invitation_create(request: web.BaseRequest): """Request handler for creating a new connection invitation. @@ -293,6 +295,7 @@ async def invitation_create(request: web.BaseRequest): @querystring_schema(InvitationReceiveQueryStringSchema()) @request_schema(InvitationMessageSchema()) @response_schema(OobRecordSchema(), 200, description="") +@tenant_authentication async def invitation_receive(request: web.BaseRequest): """Request handler for receiving a new connection invitation. @@ -337,6 +340,7 @@ async def invitation_receive(request: web.BaseRequest): @docs(tags=["out-of-band"], summary="Delete records associated with invitation") @match_info_schema(InvitationRecordMatchInfoSchema()) @response_schema(InvitationRecordResponseSchema(), description="") +@tenant_authentication async def invitation_remove(request: web.BaseRequest): """Request handler for removing a invitation related conn and oob records. diff --git a/aries_cloudagent/protocols/out_of_band/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/out_of_band/v1_0/tests/test_routes.py index 7a9384f1cc..fa61be97a2 100644 --- a/aries_cloudagent/protocols/out_of_band/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/out_of_band/v1_0/tests/test_routes.py @@ -1,16 +1,20 @@ from unittest import IsolatedAsyncioTestCase + from aries_cloudagent.tests import mock from .....admin.request_context import AdminRequestContext from .....connections.models.conn_record import ConnRecord from .....core.in_memory import InMemoryProfile - from .. import routes as test_module class TestOutOfBandRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self): - self.profile = InMemoryProfile.test_profile() + self.profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) self.context = AdminRequestContext.test_context(profile=self.profile) self.request_dict = { "context": self.context, @@ -21,6 +25,7 @@ async def asyncSetUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_invitation_create(self): diff --git a/aries_cloudagent/protocols/present_proof/v1_0/routes.py b/aries_cloudagent/protocols/present_proof/v1_0/routes.py index a606e0ff89..3cf4ae38ee 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/routes.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/routes.py @@ -10,9 +10,9 @@ request_schema, response_schema, ) - from marshmallow import fields, validate +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....connections.models.conn_record import ConnRecord from ....indy.holder import IndyHolder, IndyHolderError @@ -289,6 +289,7 @@ class V10PresExIdMatchInfoSchema(OpenAPISchema): ) @querystring_schema(V10PresentationExchangeListQueryStringSchema) @response_schema(V10PresentationExchangeListSchema(), 200, description="") +@tenant_authentication async def presentation_exchange_list(request: web.BaseRequest): """Request handler for searching presentation exchange records. @@ -330,6 +331,7 @@ async def presentation_exchange_list(request: web.BaseRequest): ) @match_info_schema(V10PresExIdMatchInfoSchema()) @response_schema(V10PresentationExchangeSchema(), 200, description="") +@tenant_authentication async def presentation_exchange_retrieve(request: web.BaseRequest): """Request handler for fetching a single presentation exchange record. @@ -379,6 +381,7 @@ async def presentation_exchange_retrieve(request: web.BaseRequest): @match_info_schema(V10PresExIdMatchInfoSchema()) @querystring_schema(CredentialsFetchQueryStringSchema()) @response_schema(IndyCredPrecisSchema(many=True), 200, description="") +@tenant_authentication async def presentation_exchange_credentials_list(request: web.BaseRequest): """Request handler for searching applicable credential records. @@ -459,6 +462,7 @@ async def presentation_exchange_credentials_list(request: web.BaseRequest): ) @request_schema(V10PresentationProposalRequestSchema()) @response_schema(V10PresentationExchangeSchema(), 200, description="") +@tenant_authentication async def presentation_exchange_send_proposal(request: web.BaseRequest): """Request handler for sending a presentation proposal. @@ -543,6 +547,7 @@ async def presentation_exchange_send_proposal(request: web.BaseRequest): ) @request_schema(V10PresentationCreateRequestRequestSchema()) @response_schema(V10PresentationExchangeSchema(), 200, description="") +@tenant_authentication async def presentation_exchange_create_request(request: web.BaseRequest): """Request handler for creating a free presentation request. @@ -621,6 +626,7 @@ async def presentation_exchange_create_request(request: web.BaseRequest): ) @request_schema(V10PresentationSendRequestRequestSchema()) @response_schema(V10PresentationExchangeSchema(), 200, description="") +@tenant_authentication async def presentation_exchange_send_free_request(request: web.BaseRequest): """Request handler for sending a presentation request free from any proposal. @@ -710,6 +716,7 @@ async def presentation_exchange_send_free_request(request: web.BaseRequest): @match_info_schema(V10PresExIdMatchInfoSchema()) @request_schema(V10PresentationSendRequestToProposalSchema()) @response_schema(V10PresentationExchangeSchema(), 200, description="") +@tenant_authentication async def presentation_exchange_send_bound_request(request: web.BaseRequest): """Request handler for sending a presentation request bound to a proposal. @@ -806,6 +813,7 @@ async def presentation_exchange_send_bound_request(request: web.BaseRequest): @match_info_schema(V10PresExIdMatchInfoSchema()) @request_schema(V10PresentationSendRequestSchema()) @response_schema(V10PresentationExchangeSchema(), description="") +@tenant_authentication async def presentation_exchange_send_presentation(request: web.BaseRequest): """Request handler for sending a presentation. @@ -923,6 +931,7 @@ async def presentation_exchange_send_presentation(request: web.BaseRequest): ) @match_info_schema(V10PresExIdMatchInfoSchema()) @response_schema(V10PresentationExchangeSchema(), description="") +@tenant_authentication async def presentation_exchange_verify_presentation(request: web.BaseRequest): """Request handler for verifying a presentation request. @@ -998,6 +1007,7 @@ async def presentation_exchange_verify_presentation(request: web.BaseRequest): @match_info_schema(V10PresExIdMatchInfoSchema()) @request_schema(V10PresentationProblemReportRequestSchema()) @response_schema(V10PresentProofModuleResponseSchema(), 200, description="") +@tenant_authentication async def presentation_exchange_problem_report(request: web.BaseRequest): """Request handler for sending problem report. @@ -1039,6 +1049,7 @@ async def presentation_exchange_problem_report(request: web.BaseRequest): ) @match_info_schema(V10PresExIdMatchInfoSchema()) @response_schema(V10PresentProofModuleResponseSchema(), description="") +@tenant_authentication async def presentation_exchange_remove(request: web.BaseRequest): """Request handler for removing a presentation exchange record. diff --git a/aries_cloudagent/protocols/present_proof/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/present_proof/v1_0/tests/test_routes.py index ca3d8e6927..9b5889b973 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/tests/test_routes.py @@ -1,23 +1,28 @@ import importlib - -from aries_cloudagent.tests import mock from unittest import IsolatedAsyncioTestCase from marshmallow import ValidationError +from aries_cloudagent.tests import mock + from .....admin.request_context import AdminRequestContext +from .....core.in_memory import InMemoryProfile from .....indy.holder import IndyHolder from .....indy.models.proof_request import IndyProofReqAttrSpecSchema from .....indy.verifier import IndyVerifier from .....ledger.base import BaseLedger from .....storage.error import StorageNotFoundError - from .. import routes as test_module class TestProofRoutes(IsolatedAsyncioTestCase): def setUp(self): - self.context = AdminRequestContext.test_context() + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(profile=profile) self.profile = self.context.profile self.request_dict = { "context": self.context, @@ -28,6 +33,7 @@ def setUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_validate_proof_req_attr_spec(self): diff --git a/aries_cloudagent/protocols/present_proof/v2_0/routes.py b/aries_cloudagent/protocols/present_proof/v2_0/routes.py index 22ec098c33..943494522d 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/routes.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/routes.py @@ -11,12 +11,12 @@ request_schema, response_schema, ) - from marshmallow import ValidationError, fields, validate, validates_schema +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext -from ....connections.models.conn_record import ConnRecord from ....anoncreds.holder import AnonCredsHolder, AnonCredsHolderError +from ....connections.models.conn_record import ConnRecord from ....indy.holder import IndyHolder, IndyHolderError from ....indy.models.cred_precis import IndyCredPrecisSchema from ....indy.models.proof import IndyPresSpecSchema @@ -425,6 +425,7 @@ def _formats_attach(by_format: Mapping, msg_type: str, spec: str) -> Mapping: @docs(tags=["present-proof v2.0"], summary="Fetch all present-proof exchange records") @querystring_schema(V20PresExRecordListQueryStringSchema) @response_schema(V20PresExRecordListSchema(), 200, description="") +@tenant_authentication async def present_proof_list(request: web.BaseRequest): """Request handler for searching presentation exchange records. @@ -467,6 +468,7 @@ async def present_proof_list(request: web.BaseRequest): ) @match_info_schema(V20PresExIdMatchInfoSchema()) @response_schema(V20PresExRecordSchema(), 200, description="") +@tenant_authentication async def present_proof_retrieve(request: web.BaseRequest): """Request handler for fetching a single presentation exchange record. @@ -513,6 +515,7 @@ async def present_proof_retrieve(request: web.BaseRequest): @match_info_schema(V20PresExIdMatchInfoSchema()) @querystring_schema(V20CredentialsFetchQueryStringSchema()) @response_schema(IndyCredPrecisSchema(many=True), 200, description="") +@tenant_authentication async def present_proof_credentials_list(request: web.BaseRequest): """Request handler for searching applicable credential records. @@ -802,6 +805,7 @@ async def retrieve_uri_list_from_schema_filter( @docs(tags=["present-proof v2.0"], summary="Sends a presentation proposal") @request_schema(V20PresProposalRequestSchema()) @response_schema(V20PresExRecordSchema(), 200, description="") +@tenant_authentication async def present_proof_send_proposal(request: web.BaseRequest): """Request handler for sending a presentation proposal. @@ -884,6 +888,7 @@ async def present_proof_send_proposal(request: web.BaseRequest): ) @request_schema(V20PresCreateRequestRequestSchema()) @response_schema(V20PresExRecordSchema(), 200, description="") +@tenant_authentication async def present_proof_create_request(request: web.BaseRequest): """Request handler for creating a free presentation request. @@ -960,6 +965,7 @@ async def present_proof_create_request(request: web.BaseRequest): ) @request_schema(V20PresSendRequestRequestSchema()) @response_schema(V20PresExRecordSchema(), 200, description="") +@tenant_authentication async def present_proof_send_free_request(request: web.BaseRequest): """Request handler for sending a presentation request free from any proposal. @@ -1043,6 +1049,7 @@ async def present_proof_send_free_request(request: web.BaseRequest): @match_info_schema(V20PresExIdMatchInfoSchema()) @request_schema(V20PresentationSendRequestToProposalSchema()) @response_schema(V20PresExRecordSchema(), 200, description="") +@tenant_authentication async def present_proof_send_bound_request(request: web.BaseRequest): """Request handler for sending a presentation request bound to a proposal. @@ -1133,6 +1140,7 @@ async def present_proof_send_bound_request(request: web.BaseRequest): @match_info_schema(V20PresExIdMatchInfoSchema()) @request_schema(V20PresSpecByFormatRequestSchema()) @response_schema(V20PresExRecordSchema(), description="") +@tenant_authentication async def present_proof_send_presentation(request: web.BaseRequest): """Request handler for sending a presentation. @@ -1246,6 +1254,7 @@ async def present_proof_send_presentation(request: web.BaseRequest): @docs(tags=["present-proof v2.0"], summary="Verify a received presentation") @match_info_schema(V20PresExIdMatchInfoSchema()) @response_schema(V20PresExRecordSchema(), description="") +@tenant_authentication async def present_proof_verify_presentation(request: web.BaseRequest): """Request handler for verifying a presentation request. @@ -1314,6 +1323,7 @@ async def present_proof_verify_presentation(request: web.BaseRequest): @match_info_schema(V20PresExIdMatchInfoSchema()) @request_schema(V20PresProblemReportRequestSchema()) @response_schema(V20PresentProofModuleResponseSchema(), 200, description="") +@tenant_authentication async def present_proof_problem_report(request: web.BaseRequest): """Request handler for sending problem report. @@ -1352,6 +1362,7 @@ async def present_proof_problem_report(request: web.BaseRequest): ) @match_info_schema(V20PresExIdMatchInfoSchema()) @response_schema(V20PresentProofModuleResponseSchema(), description="") +@tenant_authentication async def present_proof_remove(request: web.BaseRequest): """Request handler for removing a presentation exchange record. diff --git a/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py index 90ccebce43..328b2bf878 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py @@ -1,11 +1,14 @@ from copy import deepcopy -from unittest import IsolatedAsyncioTestCase -from aries_cloudagent.tests import mock -from marshmallow import ValidationError from time import time +from unittest import IsolatedAsyncioTestCase from unittest.mock import ANY +from marshmallow import ValidationError + +from aries_cloudagent.tests import mock + from .....admin.request_context import AdminRequestContext +from .....core.in_memory import InMemoryProfile from .....indy.holder import IndyHolder from .....indy.models.proof_request import IndyProofReqAttrSpecSchema from .....indy.verifier import IndyVerifier @@ -13,9 +16,7 @@ from .....storage.error import StorageNotFoundError from .....storage.vc_holder.base import VCHolder from .....storage.vc_holder.vc_record import VCRecord - from ...dif.pres_exch import SchemaInputDescriptor - from .. import routes as test_module from ..messages.pres_format import V20PresFormat from ..models.pres_exchange import V20PresExRecord @@ -126,7 +127,12 @@ class TestPresentProofRoutes(IsolatedAsyncioTestCase): def setUp(self): - self.context = AdminRequestContext.test_context() + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(profile=profile) self.profile = self.context.profile injector = self.profile.context.injector @@ -181,6 +187,7 @@ def setUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_validate(self): diff --git a/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes_anoncreds.py b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes_anoncreds.py index 4740a46f3e..e79f0e0c74 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes_anoncreds.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes_anoncreds.py @@ -1,22 +1,23 @@ -import pytest from copy import deepcopy -from unittest import IsolatedAsyncioTestCase -from aries_cloudagent.tests import mock -from marshmallow import ValidationError from time import time +from unittest import IsolatedAsyncioTestCase from unittest.mock import ANY +import pytest +from marshmallow import ValidationError + +from aries_cloudagent.tests import mock + from .....admin.request_context import AdminRequestContext from .....anoncreds.holder import AnonCredsHolder -from .....indy.models.proof_request import IndyProofReqAttrSpecSchema from .....anoncreds.verifier import AnonCredsVerifier +from .....core.in_memory import InMemoryProfile +from .....indy.models.proof_request import IndyProofReqAttrSpecSchema from .....ledger.base import BaseLedger from .....storage.error import StorageNotFoundError from .....storage.vc_holder.base import VCHolder from .....storage.vc_holder.vc_record import VCRecord - from ...dif.pres_exch import SchemaInputDescriptor - from .. import routes as test_module from ..messages.pres_format import V20PresFormat from ..models.pres_exchange import V20PresExRecord @@ -127,7 +128,12 @@ class TestPresentProofRoutesAnonCreds(IsolatedAsyncioTestCase): def setUp(self): - self.context = AdminRequestContext.test_context() + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(profile=profile) self.context.profile.settings.set_value("wallet.type", "askar-anoncreds") self.profile = self.context.profile injector = self.profile.context.injector @@ -183,6 +189,7 @@ def setUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_validate(self): diff --git a/aries_cloudagent/protocols/trustping/v1_0/routes.py b/aries_cloudagent/protocols/trustping/v1_0/routes.py index f8a41fd412..b1e850515f 100644 --- a/aries_cloudagent/protocols/trustping/v1_0/routes.py +++ b/aries_cloudagent/protocols/trustping/v1_0/routes.py @@ -2,9 +2,9 @@ from aiohttp import web from aiohttp_apispec import docs, match_info_schema, request_schema, response_schema - from marshmallow import fields +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....connections.models.conn_record import ConnRecord from ....messaging.models.openapi import OpenAPISchema @@ -45,6 +45,7 @@ class PingConnIdMatchInfoSchema(OpenAPISchema): @match_info_schema(PingConnIdMatchInfoSchema()) @request_schema(PingRequestSchema()) @response_schema(PingRequestResponseSchema(), 200, description="") +@tenant_authentication async def connections_send_ping(request: web.BaseRequest): """Request handler for sending a trust ping to a connection. diff --git a/aries_cloudagent/protocols/trustping/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/trustping/v1_0/tests/test_routes.py index 97cd67993a..541c4e7abe 100644 --- a/aries_cloudagent/protocols/trustping/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/trustping/v1_0/tests/test_routes.py @@ -1,15 +1,21 @@ from unittest import IsolatedAsyncioTestCase + from aries_cloudagent.tests import mock from .....admin.request_context import AdminRequestContext - +from .....core.in_memory import InMemoryProfile from .. import routes as test_module class TestTrustpingRoutes(IsolatedAsyncioTestCase): def setUp(self): self.session_inject = {} - self.context = AdminRequestContext.test_context(self.session_inject) + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(self.session_inject, profile) self.request_dict = { "context": self.context, "outbound_message_router": mock.CoroutineMock(), @@ -19,6 +25,7 @@ def setUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_connections_send_ping(self): diff --git a/aries_cloudagent/resolver/routes.py b/aries_cloudagent/resolver/routes.py index 85fdc2522c..a027577556 100644 --- a/aries_cloudagent/resolver/routes.py +++ b/aries_cloudagent/resolver/routes.py @@ -2,10 +2,10 @@ from aiohttp import web from aiohttp_apispec import docs, match_info_schema, response_schema -from pydid.common import DID_PATTERN - from marshmallow import fields, validate +from pydid.common import DID_PATTERN +from ..admin.decorators.auth import tenant_authentication from ..admin.request_context import AdminRequestContext from ..messaging.models.openapi import OpenAPISchema from .base import DIDMethodNotSupported, DIDNotFound, ResolutionResult, ResolverError @@ -49,6 +49,7 @@ class DIDMatchInfoSchema(OpenAPISchema): @docs(tags=["resolver"], summary="Retrieve doc for requested did") @match_info_schema(DIDMatchInfoSchema()) @response_schema(ResolutionResultSchema(), 200) +@tenant_authentication async def resolve_did(request: web.Request): """Retrieve a did document.""" context: AdminRequestContext = request["context"] diff --git a/aries_cloudagent/resolver/tests/test_routes.py b/aries_cloudagent/resolver/tests/test_routes.py index bdb1c2fd73..311f60fbb2 100644 --- a/aries_cloudagent/resolver/tests/test_routes.py +++ b/aries_cloudagent/resolver/tests/test_routes.py @@ -3,11 +3,11 @@ # pylint: disable=redefined-outer-name import pytest -from aries_cloudagent.tests import mock from pydid import DIDDocument -from ...core.in_memory import InMemoryProfile +from aries_cloudagent.tests import mock +from ...core.in_memory import InMemoryProfile from .. import routes as test_module from ..base import ( DIDMethodNotSupported, @@ -18,7 +18,6 @@ ResolverType, ) from ..did_resolver import DIDResolver - from . import DOC @@ -59,7 +58,11 @@ def mock_resolver(resolution_result): @pytest.mark.asyncio async def test_resolver(mock_resolver, mock_response: mock.MagicMock, did_doc): - profile = InMemoryProfile.test_profile() + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) context = profile.context setattr(context, "profile", profile) session = await profile.session() @@ -77,6 +80,7 @@ async def test_resolver(mock_resolver, mock_response: mock.MagicMock, did_doc): query={}, json=mock.CoroutineMock(return_value={}), __getitem__=lambda _, k: request_dict[k], + headers={"x-api-key": "secret-key"}, ) with mock.patch.object( context.profile, @@ -100,7 +104,11 @@ async def test_resolver(mock_resolver, mock_response: mock.MagicMock, did_doc): async def test_resolver_not_found_error(mock_resolver, side_effect, error): mock_resolver.resolve_with_metadata = mock.CoroutineMock(side_effect=side_effect()) - profile = InMemoryProfile.test_profile() + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) context = profile.context setattr(context, "profile", profile) session = await profile.session() @@ -118,6 +126,7 @@ async def test_resolver_not_found_error(mock_resolver, side_effect, error): query={}, json=mock.CoroutineMock(return_value={}), __getitem__=lambda _, k: request_dict[k], + headers={"x-api-key": "secret-key"}, ) with mock.patch.object( context.profile, diff --git a/aries_cloudagent/revocation/routes.py b/aries_cloudagent/revocation/routes.py index 3ff56d6cbf..c2e0c13782 100644 --- a/aries_cloudagent/revocation/routes.py +++ b/aries_cloudagent/revocation/routes.py @@ -18,6 +18,7 @@ from marshmallow import fields, validate, validates_schema from marshmallow.exceptions import ValidationError +from ..admin.decorators.auth import tenant_authentication from ..admin.request_context import AdminRequestContext from ..connections.models.conn_record import ConnRecord from ..core.event_bus import Event, EventBus @@ -507,6 +508,7 @@ class RevRegConnIdMatchInfoSchema(OpenAPISchema): @querystring_schema(CreateRevRegTxnForEndorserOptionSchema()) @querystring_schema(RevRegConnIdMatchInfoSchema()) @response_schema(RevocationModuleResponseSchema(), description="") +@tenant_authentication async def revoke(request: web.BaseRequest): """Request handler for storing a credential revocation. @@ -617,6 +619,7 @@ async def revoke(request: web.BaseRequest): @querystring_schema(CreateRevRegTxnForEndorserOptionSchema()) @querystring_schema(RevRegConnIdMatchInfoSchema()) @response_schema(TxnOrPublishRevocationsResultSchema(), 200, description="") +@tenant_authentication async def publish_revocations(request: web.BaseRequest): """Request handler for publishing pending revocations to the ledger. @@ -687,6 +690,7 @@ async def publish_revocations(request: web.BaseRequest): @docs(tags=["revocation"], summary="Clear pending revocations") @request_schema(ClearPendingRevocationsRequestSchema()) @response_schema(PublishRevocationsSchema(), 200, description="") +@tenant_authentication async def clear_pending_revocations(request: web.BaseRequest): """Request handler for clearing pending revocations. @@ -717,6 +721,7 @@ async def clear_pending_revocations(request: web.BaseRequest): @docs(tags=["revocation"], summary="Rotate revocation registry") @match_info_schema(RevocationCredDefIdMatchInfoSchema()) @response_schema(RevRegsCreatedSchema(), 200, description="") +@tenant_authentication async def rotate_rev_reg(request: web.BaseRequest): """Request handler to rotate the active revocation registries for cred. def. @@ -749,6 +754,7 @@ async def rotate_rev_reg(request: web.BaseRequest): @docs(tags=["revocation"], summary="Creates a new revocation registry") @request_schema(RevRegCreateRequestSchema()) @response_schema(RevRegResultSchema(), 200, description="") +@tenant_authentication async def create_rev_reg(request: web.BaseRequest): """Request handler to create a new revocation registry. @@ -802,6 +808,7 @@ async def create_rev_reg(request: web.BaseRequest): ) @querystring_schema(RevRegsCreatedQueryStringSchema()) @response_schema(RevRegsCreatedSchema(), 200, description="") +@tenant_authentication async def rev_regs_created(request: web.BaseRequest): """Request handler to get revocation registries that current agent created. @@ -842,6 +849,7 @@ async def rev_regs_created(request: web.BaseRequest): ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(RevRegResultSchema(), 200, description="") +@tenant_authentication async def get_rev_reg(request: web.BaseRequest): """Request handler to get a revocation registry by rev reg id. @@ -874,6 +882,7 @@ async def get_rev_reg(request: web.BaseRequest): ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(RevRegIssuedResultSchema(), 200, description="") +@tenant_authentication async def get_rev_reg_issued_count(request: web.BaseRequest): """Request handler to get number of credentials issued against revocation registry. @@ -909,6 +918,7 @@ async def get_rev_reg_issued_count(request: web.BaseRequest): ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(CredRevRecordDetailsResultSchema(), 200, description="") +@tenant_authentication async def get_rev_reg_issued(request: web.BaseRequest): """Request handler to get credentials issued against revocation registry. @@ -946,6 +956,7 @@ async def get_rev_reg_issued(request: web.BaseRequest): ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(CredRevIndyRecordsResultSchema(), 200, description="") +@tenant_authentication async def get_rev_reg_indy_recs(request: web.BaseRequest): """Request handler to get details of revoked credentials from ledger. @@ -980,6 +991,7 @@ async def get_rev_reg_indy_recs(request: web.BaseRequest): @match_info_schema(RevRegIdMatchInfoSchema()) @querystring_schema(RevRegUpdateRequestMatchInfoSchema()) @response_schema(RevRegWalletUpdatedResultSchema(), 200, description="") +@tenant_authentication async def update_rev_reg_revoked_state(request: web.BaseRequest): """Request handler to fix ledger entry of credentials revoked against registry. @@ -1071,6 +1083,7 @@ async def update_rev_reg_revoked_state(request: web.BaseRequest): ) @querystring_schema(CredRevRecordQueryStringSchema()) @response_schema(CredRevRecordResultSchema(), 200, description="") +@tenant_authentication async def get_cred_rev_record(request: web.BaseRequest): """Request handler to get credential revocation record. @@ -1112,6 +1125,7 @@ async def get_cred_rev_record(request: web.BaseRequest): ) @match_info_schema(RevocationCredDefIdMatchInfoSchema()) @response_schema(RevRegResultSchema(), 200, description="") +@tenant_authentication async def get_active_rev_reg(request: web.BaseRequest): """Request handler to get current active revocation registry by cred def id. @@ -1145,6 +1159,7 @@ async def get_active_rev_reg(request: web.BaseRequest): ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(RevocationModuleResponseSchema, description="tails file") +@tenant_authentication async def get_tails_file(request: web.BaseRequest) -> web.FileResponse: """Request handler to download tails file for revocation registry. @@ -1177,6 +1192,7 @@ async def get_tails_file(request: web.BaseRequest) -> web.FileResponse: ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(RevocationModuleResponseSchema(), description="") +@tenant_authentication async def upload_tails_file(request: web.BaseRequest): """Request handler to upload local tails file for revocation registry. @@ -1215,6 +1231,7 @@ async def upload_tails_file(request: web.BaseRequest): @querystring_schema(CreateRevRegTxnForEndorserOptionSchema()) @querystring_schema(RevRegConnIdMatchInfoSchema()) @response_schema(TxnOrRevRegResultSchema(), 200, description="") +@tenant_authentication async def send_rev_reg_def(request: web.BaseRequest): """Request handler to send revocation registry definition by rev reg id to ledger. @@ -1335,6 +1352,7 @@ async def send_rev_reg_def(request: web.BaseRequest): @querystring_schema(CreateRevRegTxnForEndorserOptionSchema()) @querystring_schema(RevRegConnIdMatchInfoSchema()) @response_schema(RevRegResultSchema(), 200, description="") +@tenant_authentication async def send_rev_reg_entry(request: web.BaseRequest): """Request handler to send rev reg entry by registry id to ledger. @@ -1454,6 +1472,7 @@ async def send_rev_reg_entry(request: web.BaseRequest): @match_info_schema(RevRegIdMatchInfoSchema()) @request_schema(RevRegUpdateTailsFileUriSchema()) @response_schema(RevRegResultSchema(), 200, description="") +@tenant_authentication async def update_rev_reg(request: web.BaseRequest): """Request handler to update a rev reg's public tails URI by registry id. @@ -1491,6 +1510,7 @@ async def update_rev_reg(request: web.BaseRequest): @match_info_schema(RevRegIdMatchInfoSchema()) @querystring_schema(SetRevRegStateQueryStringSchema()) @response_schema(RevRegResultSchema(), 200, description="") +@tenant_authentication async def set_rev_reg_state(request: web.BaseRequest): """Request handler to set a revocation registry state manually. @@ -1744,6 +1764,7 @@ class TailsDeleteResponseSchema(OpenAPISchema): @querystring_schema(RevRegId()) @response_schema(TailsDeleteResponseSchema()) @docs(tags=["revocation"], summary="Delete the tail files") +@tenant_authentication async def delete_tails(request: web.BaseRequest) -> json: """Delete Tails Files.""" context: AdminRequestContext = request["context"] diff --git a/aries_cloudagent/revocation/tests/test_routes.py b/aries_cloudagent/revocation/tests/test_routes.py index 71a9841d16..95e4eab0b5 100644 --- a/aries_cloudagent/revocation/tests/test_routes.py +++ b/aries_cloudagent/revocation/tests/test_routes.py @@ -17,7 +17,11 @@ class TestRevocationRoutes(IsolatedAsyncioTestCase): def setUp(self): - self.profile = InMemoryProfile.test_profile() + self.profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) self.context = self.profile.context setattr(self.context, "profile", self.profile) self.request_dict = { @@ -29,11 +33,16 @@ def setUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) self.test_did = "sample-did" - self.author_profile = InMemoryProfile.test_profile() + self.author_profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "author-key", + } + ) self.author_profile.settings.set_value("endorser.author", True) self.author_context = self.author_profile.context setattr(self.author_context, "profile", self.author_profile) @@ -46,6 +55,7 @@ def setUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.author_request_dict[k], + headers={"x-api-key": "author-key"}, ) async def test_validate_cred_rev_rec_qs_and_revoke_req(self): @@ -1054,7 +1064,7 @@ async def test_set_rev_reg_state_not_found(self): async def test_wrong_profile_403(self): self.profile = InMemoryProfile.test_profile( - settings={"wallet.type": "askar"}, + settings={"wallet.type": "askar", "admin.admin_api_key": "secret-key"}, profile_class=AskarAnoncredsProfile, ) self.context = AdminRequestContext.test_context({}, self.profile) @@ -1067,6 +1077,7 @@ async def test_wrong_profile_403(self): query={}, __getitem__=lambda _, k: self.request_dict[k], context=self.context, + headers={"x-api-key": "secret-key"}, ) self.request.json = mock.CoroutineMock( diff --git a/aries_cloudagent/revocation_anoncreds/routes.py b/aries_cloudagent/revocation_anoncreds/routes.py index 99b66f1bd2..1f1b034c1f 100644 --- a/aries_cloudagent/revocation_anoncreds/routes.py +++ b/aries_cloudagent/revocation_anoncreds/routes.py @@ -15,6 +15,7 @@ from marshmallow import fields, validate, validates_schema from marshmallow.exceptions import ValidationError +from ..admin.decorators.auth import tenant_authentication from ..admin.request_context import AdminRequestContext from ..anoncreds.base import ( AnonCredsObjectNotFound, @@ -459,6 +460,7 @@ def validate_fields(self, data, **kwargs): ) @request_schema(RevokeRequestSchemaAnoncreds()) @response_schema(RevocationAnoncredsModuleResponseSchema(), description="") +@tenant_authentication async def revoke(request: web.BaseRequest): """Request handler for storing a credential revocation. @@ -512,6 +514,7 @@ async def revoke(request: web.BaseRequest): @docs(tags=[TAG_TITLE], summary="Publish pending revocations to ledger") @request_schema(PublishRevocationsSchemaAnoncreds()) @response_schema(PublishRevocationsResultSchemaAnoncreds(), 200, description="") +@tenant_authentication async def publish_revocations(request: web.BaseRequest): """Request handler for publishing pending revocations to the ledger. @@ -551,6 +554,7 @@ async def publish_revocations(request: web.BaseRequest): ) @querystring_schema(RevRegsCreatedQueryStringSchema()) @response_schema(RevRegsCreatedSchemaAnoncreds(), 200, description="") +@tenant_authentication async def get_rev_regs(request: web.BaseRequest): """Request handler to get revocation registries that current agent created. @@ -589,6 +593,7 @@ async def get_rev_regs(request: web.BaseRequest): ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(RevRegResultSchemaAnoncreds(), 200, description="") +@tenant_authentication async def get_rev_reg(request: web.BaseRequest): """Request handler to get a revocation registry by rev reg id. @@ -663,6 +668,7 @@ async def _get_issuer_rev_reg_record( ) @match_info_schema(RevocationCredDefIdMatchInfoSchema()) @response_schema(RevRegResultSchemaAnoncreds(), 200, description="") +@tenant_authentication async def get_active_rev_reg(request: web.BaseRequest): """Request handler to get current active revocation registry by cred def id. @@ -692,6 +698,7 @@ async def get_active_rev_reg(request: web.BaseRequest): @docs(tags=[TAG_TITLE], summary="Rotate revocation registry") @match_info_schema(RevocationCredDefIdMatchInfoSchema()) @response_schema(RevRegsCreatedSchemaAnoncreds(), 200, description="") +@tenant_authentication async def rotate_rev_reg(request: web.BaseRequest): """Request handler to rotate the active revocation registries for cred. def. @@ -724,6 +731,7 @@ async def rotate_rev_reg(request: web.BaseRequest): ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(RevRegIssuedResultSchemaAnoncreds(), 200, description="") +@tenant_authentication async def get_rev_reg_issued_count(request: web.BaseRequest): """Request handler to get number of credentials issued against revocation registry. @@ -764,6 +772,7 @@ async def get_rev_reg_issued_count(request: web.BaseRequest): ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(CredRevRecordDetailsResultSchemaAnoncreds(), 200, description="") +@tenant_authentication async def get_rev_reg_issued(request: web.BaseRequest): """Request handler to get credentials issued against revocation registry. @@ -805,6 +814,7 @@ async def get_rev_reg_issued(request: web.BaseRequest): ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(CredRevIndyRecordsResultSchemaAnoncreds(), 200, description="") +@tenant_authentication async def get_rev_reg_indy_recs(request: web.BaseRequest): """Request handler to get details of revoked credentials from ledger. @@ -850,6 +860,7 @@ async def get_rev_reg_indy_recs(request: web.BaseRequest): @match_info_schema(RevRegIdMatchInfoSchema()) @querystring_schema(RevRegUpdateRequestMatchInfoSchema()) @response_schema(RevRegWalletUpdatedResultSchemaAnoncreds(), 200, description="") +@tenant_authentication async def update_rev_reg_revoked_state(request: web.BaseRequest): """Request handler to fix ledger entry of credentials revoked against registry. @@ -945,6 +956,7 @@ async def update_rev_reg_revoked_state(request: web.BaseRequest): ) @querystring_schema(CredRevRecordQueryStringSchema()) @response_schema(CredRevRecordResultSchemaAnoncreds(), 200, description="") +@tenant_authentication async def get_cred_rev_record(request: web.BaseRequest): """Request handler to get credential revocation record. @@ -987,6 +999,7 @@ async def get_cred_rev_record(request: web.BaseRequest): ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(RevocationAnoncredsModuleResponseSchema, description="tails file") +@tenant_authentication async def get_tails_file(request: web.BaseRequest) -> web.FileResponse: """Request handler to download tails file for revocation registry. @@ -1025,6 +1038,7 @@ async def get_tails_file(request: web.BaseRequest) -> web.FileResponse: @match_info_schema(RevRegIdMatchInfoSchema()) @querystring_schema(SetRevRegStateQueryStringSchema()) @response_schema(RevRegResultSchemaAnoncreds(), 200, description="") +@tenant_authentication async def set_rev_reg_state(request: web.BaseRequest): """Request handler to set a revocation registry state manually. diff --git a/aries_cloudagent/revocation_anoncreds/tests/test_routes.py b/aries_cloudagent/revocation_anoncreds/tests/test_routes.py index 2198e7668b..c3a102c513 100644 --- a/aries_cloudagent/revocation_anoncreds/tests/test_routes.py +++ b/aries_cloudagent/revocation_anoncreds/tests/test_routes.py @@ -20,7 +20,10 @@ class TestRevocationRoutes(IsolatedAsyncioTestCase): def setUp(self): - self.profile = InMemoryProfile.test_profile(profile_class=AskarAnoncredsProfile) + self.profile = InMemoryProfile.test_profile( + settings={"admin.admin_api_key": "secret-key"}, + profile_class=AskarAnoncredsProfile, + ) self.context = self.profile.context setattr(self.context, "profile", self.profile) self.request_dict = { @@ -32,6 +35,7 @@ def setUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) self.test_did = "sample-did" @@ -524,7 +528,7 @@ async def test_set_rev_reg_state_not_found(self): async def test_wrong_profile_403(self): self.profile = InMemoryProfile.test_profile( - settings={"wallet.type": "askar"}, + settings={"wallet.type": "askar", "admin.admin_api_key": "secret-key"}, profile_class=AskarProfile, ) self.context = AdminRequestContext.test_context({}, self.profile) @@ -537,6 +541,7 @@ async def test_wrong_profile_403(self): query={}, __getitem__=lambda _, k: self.request_dict[k], context=self.context, + headers={"x-api-key": "secret-key"}, ) self.request.json = mock.CoroutineMock( diff --git a/aries_cloudagent/settings/tests/test_routes.py b/aries_cloudagent/settings/tests/test_routes.py index 4c103bf2ba..3b2e3eb76b 100644 --- a/aries_cloudagent/settings/tests/test_routes.py +++ b/aries_cloudagent/settings/tests/test_routes.py @@ -3,13 +3,13 @@ # pylint: disable=redefined-outer-name import pytest + from aries_cloudagent.tests import mock from ...admin.request_context import AdminRequestContext from ...core.in_memory import InMemoryProfile from ...multitenant.base import BaseMultitenantManager from ...multitenant.manager import MultitenantManager - from .. import routes as test_module @@ -24,7 +24,11 @@ def mock_response(): @pytest.mark.asyncio async def test_get_profile_settings(mock_response): - profile = InMemoryProfile.test_profile() + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) profile.settings.update( { "admin.admin_client_max_request_size": 1, @@ -45,6 +49,7 @@ async def test_get_profile_settings(mock_response): query={}, json=mock.CoroutineMock(return_value={}), __getitem__=lambda _, k: request_dict[k], + headers={"x-api-key": "secret-key"}, ) await test_module.get_profile_settings(request) assert mock_response.call_args[0][0] == { diff --git a/aries_cloudagent/utils/general.py b/aries_cloudagent/utils/general.py new file mode 100644 index 0000000000..7c01793a07 --- /dev/null +++ b/aries_cloudagent/utils/general.py @@ -0,0 +1,10 @@ +"""Utility functions for the admin server.""" + +from hmac import compare_digest + + +def const_compare(string1, string2): + """Compare two strings in constant time.""" + if string1 is None or string2 is None: + return False + return compare_digest(string1.encode(), string2.encode()) diff --git a/aries_cloudagent/vc/routes.py b/aries_cloudagent/vc/routes.py index 3cafdff542..8a78610dcc 100644 --- a/aries_cloudagent/vc/routes.py +++ b/aries_cloudagent/vc/routes.py @@ -1,32 +1,33 @@ """VC-API Routes.""" +import uuid + from aiohttp import web from aiohttp_apispec import docs, request_schema, response_schema from marshmallow.exceptions import ValidationError -import uuid + +from ..admin.decorators.auth import tenant_authentication from ..admin.request_context import AdminRequestContext -from ..storage.error import StorageError, StorageNotFoundError, StorageDuplicateError -from ..wallet.error import WalletError -from ..wallet.base import BaseWallet from ..config.base import InjectionError from ..resolver.base import ResolverError +from ..storage.error import StorageDuplicateError, StorageError, StorageNotFoundError from ..storage.vc_holder.base import VCHolder +from ..wallet.base import BaseWallet +from ..wallet.error import WalletError +from .vc_ld.manager import VcLdpManager, VcLdpManagerError from .vc_ld.models import web_schemas -from .vc_ld.manager import VcLdpManager -from .vc_ld.manager import VcLdpManagerError from .vc_ld.models.credential import ( VerifiableCredential, ) - +from .vc_ld.models.options import LDProofVCOptions from .vc_ld.models.presentation import ( VerifiablePresentation, ) -from .vc_ld.models.options import LDProofVCOptions - @docs(tags=["vc-api"], summary="List credentials") @response_schema(web_schemas.ListCredentialsResponse(), 200, description="") +@tenant_authentication async def list_credentials_route(request: web.BaseRequest): """Request handler for issuing a credential. @@ -46,6 +47,7 @@ async def list_credentials_route(request: web.BaseRequest): @docs(tags=["vc-api"], summary="Fetch credential by ID") @response_schema(web_schemas.FetchCredentialResponse(), 200, description="") +@tenant_authentication async def fetch_credential_route(request: web.BaseRequest): """Request handler for issuing a credential. @@ -66,6 +68,7 @@ async def fetch_credential_route(request: web.BaseRequest): @docs(tags=["vc-api"], summary="Issue a credential") @request_schema(web_schemas.IssueCredentialRequest()) @response_schema(web_schemas.IssueCredentialResponse(), 200, description="") +@tenant_authentication async def issue_credential_route(request: web.BaseRequest): """Request handler for issuing a credential. @@ -107,6 +110,7 @@ async def issue_credential_route(request: web.BaseRequest): @docs(tags=["vc-api"], summary="Verify a credential") @request_schema(web_schemas.VerifyCredentialRequest()) @response_schema(web_schemas.VerifyCredentialResponse(), 200, description="") +@tenant_authentication async def verify_credential_route(request: web.BaseRequest): """Request handler for verifying a credential. @@ -171,6 +175,7 @@ async def store_credential_route(request: web.BaseRequest): @docs(tags=["vc-api"], summary="Prove a presentation") @request_schema(web_schemas.ProvePresentationRequest()) @response_schema(web_schemas.ProvePresentationResponse(), 200, description="") +@tenant_authentication async def prove_presentation_route(request: web.BaseRequest): """Request handler for proving a presentation. @@ -211,6 +216,7 @@ async def prove_presentation_route(request: web.BaseRequest): @docs(tags=["vc-api"], summary="Verify a Presentation") @request_schema(web_schemas.VerifyPresentationRequest()) @response_schema(web_schemas.VerifyPresentationResponse(), 200, description="") +@tenant_authentication async def verify_presentation_route(request: web.BaseRequest): """Request handler for verifying a presentation. diff --git a/aries_cloudagent/wallet/routes.py b/aries_cloudagent/wallet/routes.py index 23afe6fb73..7fb9c75ea5 100644 --- a/aries_cloudagent/wallet/routes.py +++ b/aries_cloudagent/wallet/routes.py @@ -11,6 +11,7 @@ from aries_cloudagent.connections.base_manager import BaseConnectionManager +from ..admin.decorators.auth import tenant_authentication from ..admin.request_context import AdminRequestContext from ..config.injection_context import InjectionContext from ..connections.models.conn_record import ConnRecord @@ -434,6 +435,7 @@ def format_did_info(info: DIDInfo): @docs(tags=["wallet"], summary="List wallet DIDs") @querystring_schema(DIDListQueryStringSchema()) @response_schema(DIDListSchema, 200, description="") +@tenant_authentication async def wallet_did_list(request: web.BaseRequest): """Request handler for searching wallet DIDs. @@ -541,6 +543,7 @@ async def wallet_did_list(request: web.BaseRequest): @docs(tags=["wallet"], summary="Create a local DID") @request_schema(DIDCreateSchema()) @response_schema(DIDResultSchema, 200, description="") +@tenant_authentication async def wallet_create_did(request: web.BaseRequest): """Request handler for creating a new local DID in the wallet. @@ -662,6 +665,7 @@ async def wallet_create_did(request: web.BaseRequest): @docs(tags=["wallet"], summary="Fetch the current public DID") @response_schema(DIDResultSchema, 200, description="") +@tenant_authentication async def wallet_get_public_did(request: web.BaseRequest): """Request handler for fetching the current public DID. @@ -692,6 +696,7 @@ async def wallet_get_public_did(request: web.BaseRequest): @querystring_schema(AttribConnIdMatchInfoSchema()) @querystring_schema(MediationIDSchema()) @response_schema(DIDResultSchema, 200, description="") +@tenant_authentication async def wallet_set_public_did(request: web.BaseRequest): """Request handler for setting the current public DID. @@ -937,6 +942,7 @@ async def promote_wallet_public_did( @querystring_schema(CreateAttribTxnForEndorserOptionSchema()) @querystring_schema(AttribConnIdMatchInfoSchema()) @response_schema(WalletModuleResponseSchema(), description="") +@tenant_authentication async def wallet_set_did_endpoint(request: web.BaseRequest): """Request handler for setting an endpoint for a DID. @@ -1055,6 +1061,7 @@ async def wallet_set_did_endpoint(request: web.BaseRequest): @docs(tags=["wallet"], summary="Create a EdDSA jws using did keys with a given payload") @request_schema(JWSCreateSchema) @response_schema(WalletModuleResponseSchema(), description="") +@tenant_authentication async def wallet_jwt_sign(request: web.BaseRequest): """Request handler for jws creation using did. @@ -1091,6 +1098,7 @@ async def wallet_jwt_sign(request: web.BaseRequest): ) @request_schema(SDJWSCreateSchema) @response_schema(WalletModuleResponseSchema(), description="") +@tenant_authentication async def wallet_sd_jwt_sign(request: web.BaseRequest): """Request handler for sd-jws creation using did. @@ -1127,6 +1135,7 @@ async def wallet_sd_jwt_sign(request: web.BaseRequest): @docs(tags=["wallet"], summary="Verify a EdDSA jws using did keys with a given JWS") @request_schema(JWSVerifySchema()) @response_schema(JWSVerifyResponseSchema(), 200, description="") +@tenant_authentication async def wallet_jwt_verify(request: web.BaseRequest): """Request handler for jws validation using did. @@ -1160,6 +1169,7 @@ async def wallet_jwt_verify(request: web.BaseRequest): ) @request_schema(SDJWSVerifySchema()) @response_schema(SDJWSVerifyResponseSchema(), 200, description="") +@tenant_authentication async def wallet_sd_jwt_verify(request: web.BaseRequest): """Request handler for sd-jws validation using did. @@ -1182,6 +1192,7 @@ async def wallet_sd_jwt_verify(request: web.BaseRequest): @docs(tags=["wallet"], summary="Query DID endpoint in wallet") @querystring_schema(DIDQueryStringSchema()) @response_schema(DIDEndpointSchema, 200, description="") +@tenant_authentication async def wallet_get_did_endpoint(request: web.BaseRequest): """Request handler for getting the current DID endpoint from the wallet. @@ -1215,6 +1226,7 @@ async def wallet_get_did_endpoint(request: web.BaseRequest): @docs(tags=["wallet"], summary="Rotate keypair for a DID not posted to the ledger") @querystring_schema(DIDQueryStringSchema()) @response_schema(WalletModuleResponseSchema(), description="") +@tenant_authentication async def wallet_rotate_did_keypair(request: web.BaseRequest): """Request handler for rotating local DID keypair. @@ -1275,6 +1287,7 @@ class UpgradeResultSchema(OpenAPISchema): ) @querystring_schema(UpgradeVerificationSchema()) @response_schema(UpgradeResultSchema(), description="") +@tenant_authentication async def upgrade_anoncreds(request: web.BaseRequest): """Request handler for triggering an upgrade to anoncreds. diff --git a/aries_cloudagent/wallet/tests/test_routes.py b/aries_cloudagent/wallet/tests/test_routes.py index f99fbc1679..ba8fc4db4d 100644 --- a/aries_cloudagent/wallet/tests/test_routes.py +++ b/aries_cloudagent/wallet/tests/test_routes.py @@ -29,7 +29,9 @@ class TestWalletRoutes(IsolatedAsyncioTestCase): def setUp(self): self.wallet = mock.create_autospec(BaseWallet) self.session_inject = {BaseWallet: self.wallet} - self.profile = InMemoryProfile.test_profile() + self.profile = InMemoryProfile.test_profile( + settings={"admin.admin_api_key": "secret-key"} + ) self.context = AdminRequestContext.test_context( self.session_inject, self.profile ) @@ -43,6 +45,7 @@ def setUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) self.test_did = "did" diff --git a/poetry.lock b/poetry.lock index 89568220cb..77f9e3acec 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "aiohttp" @@ -259,33 +259,33 @@ tests = ["PyHamcrest (>=2.0.2)", "mypy", "pytest (>=4.6)", "pytest-benchmark", " [[package]] name = "black" -version = "24.3.0" +version = "24.4.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"}, - {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"}, - {file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"}, - {file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"}, - {file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"}, - {file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"}, - {file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"}, - {file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"}, - {file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"}, - {file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"}, - {file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"}, - {file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"}, - {file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"}, - {file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"}, - {file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"}, - {file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"}, - {file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"}, - {file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"}, - {file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"}, - {file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"}, - {file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"}, - {file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"}, + {file = "black-24.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6ad001a9ddd9b8dfd1b434d566be39b1cd502802c8d38bbb1ba612afda2ef436"}, + {file = "black-24.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3a3a092b8b756c643fe45f4624dbd5a389f770a4ac294cf4d0fce6af86addaf"}, + {file = "black-24.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dae79397f367ac8d7adb6c779813328f6d690943f64b32983e896bcccd18cbad"}, + {file = "black-24.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:71d998b73c957444fb7c52096c3843875f4b6b47a54972598741fe9a7f737fcb"}, + {file = "black-24.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8e5537f456a22cf5cfcb2707803431d2feeb82ab3748ade280d6ccd0b40ed2e8"}, + {file = "black-24.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64e60a7edd71fd542a10a9643bf369bfd2644de95ec71e86790b063aa02ff745"}, + {file = "black-24.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cd5b4f76056cecce3e69b0d4c228326d2595f506797f40b9233424e2524c070"}, + {file = "black-24.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:64578cf99b6b46a6301bc28bdb89f9d6f9b592b1c5837818a177c98525dbe397"}, + {file = "black-24.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f95cece33329dc4aa3b0e1a771c41075812e46cf3d6e3f1dfe3d91ff09826ed2"}, + {file = "black-24.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4396ca365a4310beef84d446ca5016f671b10f07abdba3e4e4304218d2c71d33"}, + {file = "black-24.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d99dfdf37a2a00a6f7a8dcbd19edf361d056ee51093b2445de7ca09adac965"}, + {file = "black-24.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:21f9407063ec71c5580b8ad975653c66508d6a9f57bd008bb8691d273705adcd"}, + {file = "black-24.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:652e55bb722ca026299eb74e53880ee2315b181dfdd44dca98e43448620ddec1"}, + {file = "black-24.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7f2966b9b2b3b7104fca9d75b2ee856fe3fdd7ed9e47c753a4bb1a675f2caab8"}, + {file = "black-24.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bb9ca06e556a09f7f7177bc7cb604e5ed2d2df1e9119e4f7d2f1f7071c32e5d"}, + {file = "black-24.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4e71cdebdc8efeb6deaf5f2deb28325f8614d48426bed118ecc2dcaefb9ebf3"}, + {file = "black-24.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6644f97a7ef6f401a150cca551a1ff97e03c25d8519ee0bbc9b0058772882665"}, + {file = "black-24.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:75a2d0b4f5eb81f7eebc31f788f9830a6ce10a68c91fbe0fade34fff7a2836e6"}, + {file = "black-24.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb949f56a63c5e134dfdca12091e98ffb5fd446293ebae123d10fc1abad00b9e"}, + {file = "black-24.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:7852b05d02b5b9a8c893ab95863ef8986e4dda29af80bbbda94d7aee1abf8702"}, + {file = "black-24.4.0-py3-none-any.whl", hash = "sha256:74eb9b5420e26b42c00a3ff470dc0cd144b80a766128b1771d07643165e08d0e"}, + {file = "black-24.4.0.tar.gz", hash = "sha256:f07b69fda20578367eaebbd670ff8fc653ab181e1ff95d84497f9fa20e7d0641"}, ] [package.dependencies] @@ -2810,4 +2810,4 @@ indy = ["python3-indy"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "04f0ce35bbf5c63de96de234ac4d67dcd58267c04fa7979cf798a165cdc7b839" +content-hash = "044e3e5eec7c5a47bb91f3f1b8423ad7cdfd7699405bd46f4dd32f24fde7f7db" diff --git a/pyproject.toml b/pyproject.toml index fdb9ab1696..0014cc639a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,7 @@ pre-commit="~3.3.3" ruff = "0.1.2" # Sync with version in .github/workflows/blackformat.yml # Sync with version in .pre-commit-config.yaml -black="24.3.0" +black="24.4.0" sphinx="1.8.4" sphinx-rtd-theme=">=0.4.3"