Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow access to swagger UI when protected by API key #570

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 53 additions & 14 deletions aries_cloudagent/admin/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ def __init__(
task_queue: An optional task queue for handlers
"""
self.app = None
self.admin_api_key = context.settings.get("admin.admin_api_key")
self.admin_insecure_mode = bool(
context.settings.get("admin.admin_insecure_mode")
)
self.host = host
self.port = port
self.conductor_stats = conductor_stats
Expand All @@ -156,26 +160,29 @@ async def make_application(self) -> web.Application:

middlewares = [validation_middleware]

admin_api_key = self.context.settings.get("admin.admin_api_key")
admin_insecure_mode = self.context.settings.get("admin.admin_insecure_mode")

# admin-token and admin-token are mutually exclusive and required.
# This should be enforced during parameter parsing but to be sure,
# we check here.
assert admin_insecure_mode or admin_api_key
assert not (admin_insecure_mode and admin_api_key)
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
] 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 admin_api_key:
if self.admin_api_key:

@web.middleware
async def check_token(request, handler):
header_admin_api_key = request.headers.get("x-api-key")
if not header_admin_api_key:
raise web.HTTPUnauthorized()
valid_key = self.admin_api_key == header_admin_api_key

if admin_api_key == header_admin_api_key:
if valid_key or is_unprotected_path(request.path):
return await handler(request)
else:
raise web.HTTPUnauthorized()
Expand Down Expand Up @@ -283,6 +290,12 @@ async def stop(self) -> None:

async def on_startup(self, app: web.Application):
"""Perform webserver startup actions."""
if self.admin_api_key:
swagger = app["swagger_dict"]
swagger["securityDefinitions"] = {
"ApiKeyHeader": {"type": "apiKey", "in": "header", "name": "X-API-KEY"}
}
swagger["security"] = [{"ApiKeyHeader": []}]

@docs(tags=["server"], summary="Fetch the list of loaded plugins")
@response_schema(AdminModulesSchema(), 200)
Expand Down Expand Up @@ -355,12 +368,21 @@ async def websocket_handler(self, request):
queue = BasicMessageQueue()
loop = asyncio.get_event_loop()

if self.admin_insecure_mode:
# open to send websocket messages without api key auth
queue.authenticated = True
else:
header_admin_api_key = request.headers.get("x-api-key")
# authenticated via http header?
queue.authenticated = header_admin_api_key == self.admin_api_key

try:
self.websocket_queues[socket_id] = queue
await queue.enqueue(
{
"topic": "settings",
"payload": {
"authenticated": queue.authenticated,
"label": self.context.settings.get("default_label"),
"endpoint": self.context.settings.get("default_endpoint"),
"no_receive_invites": self.context.settings.get(
Expand All @@ -372,7 +394,7 @@ async def websocket_handler(self, request):
)

closed = False
receive = loop.create_task(ws.receive())
receive = loop.create_task(ws.receive_json())
send = loop.create_task(queue.dequeue(timeout=5.0))

while not closed:
Expand All @@ -384,9 +406,22 @@ async def websocket_handler(self, request):
closed = True

if receive.done():
# ignored
if not closed:
receive = loop.create_task(ws.receive())
msg_received = None
msg_api_key = None
try:
# this call can re-raise exeptions from inside the task
msg_received = receive.result()
msg_api_key = msg_received.get("x-api-key")
except Exception:
LOGGER.exception(
"Exception in websocket receiving task:"
)
if self.admin_api_key and self.admin_api_key == msg_api_key:
# authenticated via websocket message
queue.authenticated = True

receive = loop.create_task(ws.receive_json())

if send.done():
try:
Expand All @@ -397,7 +432,10 @@ async def websocket_handler(self, request):
if msg is None:
# we send fake pings because the JS client
# can't detect real ones
msg = {"topic": "ping"}
msg = {
"topic": "ping",
"authenticated": queue.authenticated,
}
if not closed:
if msg:
await ws.send_json(msg)
Expand Down Expand Up @@ -441,4 +479,5 @@ async def send_webhook(self, topic: str, payload: dict):
)

for queue in self.websocket_queues.values():
await queue.enqueue({"topic": topic, "payload": payload})
if queue.authenticated or topic in ("ping", "settings"):
await queue.enqueue({"topic": topic, "payload": payload})
29 changes: 29 additions & 0 deletions aries_cloudagent/admin/tests/test_admin_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,35 @@ async def test_status_secure(self):
result = await resp.json()
assert isinstance(result, dict)

@unittest_run_loop
async def test_websocket_with_api_key_message(self):
async with self.client.ws_connect("/ws") as ws:
result = await ws.receive_json()
assert result["topic"] == "settings"

ping1 = await ws.receive_json()
assert ping1["topic"] == "ping"
assert ping1["authenticated"] == False

await ws.send_json({"dummy": ""})
ping2 = await ws.receive_json()
assert ping2["authenticated"] == False

await ws.send_json({"x-api-key": self.TEST_API_KEY})
ping3 = await ws.receive_json()
assert ping3["authenticated"] == True

@unittest_run_loop
async def test_websocket_with_api_key_header(self):
async with self.client.ws_connect(
"/ws", headers={"x-api-key": self.TEST_API_KEY}
) as ws:
result = await ws.receive_json()
assert result["topic"] == "settings"

ping1 = await ws.receive_json()
assert ping1["authenticated"] == True


class TestAdminServerWebhook(AioHTTPTestCase):
async def setUpAsync(self):
Expand Down
1 change: 1 addition & 0 deletions aries_cloudagent/transport/queue/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def __init__(self):
self.queue = self.make_queue()
self.logger = logging.getLogger(__name__)
self.stop_event = asyncio.Event()
self.authenticated = False

def make_queue(self):
"""Create the queue instance."""
Expand Down