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

Api security #67

Merged
merged 9 commits into from
Jul 16, 2019
2 changes: 2 additions & 0 deletions DevReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ Most configuration parameters are provided to the the agent at startup. Refer to
| `--genesis-transactions` | `--genesis-transactions {"reqSignature":{},"txn":{"data":{"d... <snip>` | Specifies the genesis transactions to use to connect to an Hyperledger Indy ledger. | `false` |
| `--genesis-url` | `--genesis-url https://example.com/genesis` | Specifies the url from which to download the genesis transaction data. For example, the [Sovrin Network genesis transactions](https://raw.githubusercontent.com/sovrin-foundation/sovrin/master/sovrin/pool_transactions_live_genesis). | `false` |
| `--admin` | `--admin 0.0.0.0 5050` | Specifies the host and port on which to run the administrative server. If not provided, no admin server is made available. | `false` |
| `--admin-insecure-mode` | `--admin-insecure-mode` | Instructs the agent to run the admin web server in insecure mode. The admin server will be publicly available to anyone who has access to the interface. | `false` |
| `--admin-api-key` | `--admin-api-key abc123` | Instructs the agent to protect all admin endpoints with the provided API key. The API must be pass in the header `X-API-Key: <api key>`. | `false` |
| `--debug` | `--debug` | Enables a remote debugging service that can be accessed using [ptvsd](https://github.com/Microsoft/ptvsd). The framework will wait for the debugger to connect at start-up. | `false` |
| `--debug-connections` | `--debug-connections` | Enables additional logging of connection information. | `false` |
| `--accept-invites` | `--accept-invites` | Instructs the agent to automatically accept invites. | `false` |
Expand Down
33 changes: 29 additions & 4 deletions aries_cloudagent/admin/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,35 @@ def __init__(

async def make_application(self) -> web.Application:
"""Get the aiohttp application instance."""

middlewares = []

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)

# 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:

@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()

if admin_api_key == header_admin_api_key:
return await handler(request)
else:
raise web.HTTPUnauthorized()

middlewares.append(check_token)

stats: Collector = await self.context.inject(Collector, required=False)
if stats:

Expand Down Expand Up @@ -190,10 +218,7 @@ async def collect_stats(request, handler):
cors.add(route)

setup_aiohttp_apispec(
app=app,
title="Aries Cloud Agent",
version="v1",
swagger_path="/api/doc",
app=app, title="Aries Cloud Agent", version="v1", swagger_path="/api/doc"
)
app.on_startup.append(self.on_startup)
return app
Expand Down
13 changes: 13 additions & 0 deletions aries_cloudagent/admin/tests/test_admin_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,24 @@ def get_admin_server(self) -> AdminServer:
context.injector.bind_provider(
BaseOutboundMessageQueue, ClassProvider(BasicOutboundMessageQueue)
)
context.settings["admin.admin_insecure_mode"] = True
server = AdminServer(
"0.0.0.0", unused_port(), context, self.outbound_message_router
)
return server

@unittest_run_loop
async def test_start_bad_settings(self):
server = self.get_admin_server()
server.context.settings["admin.admin_insecure_mode"] = None

try:
await server.start()
except AssertionError:
return True

raise Exception

@unittest_run_loop
async def test_start_stop(self):
server = self.get_admin_server()
Expand Down
29 changes: 29 additions & 0 deletions aries_cloudagent/config/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import argparse
from typing import Sequence

from .error import ArgsParseError

PARSER = argparse.ArgumentParser(description="Runs an Aries Cloud Agent.")

Expand Down Expand Up @@ -153,6 +154,19 @@
help="Enable the administration API on a given host and port",
)

PARSER.add_argument(
"--admin-api-key",
type=str,
metavar="<api-key>",
help="Set the api key for the admin API.",
)

PARSER.add_argument(
"--admin-insecure-mode",
action="store_true",
help="Do not protect the admin API with token authentication.z",
)

PARSER.add_argument("--debug", action="store_true", help="Enable debugging features")

PARSER.add_argument(
Expand Down Expand Up @@ -297,6 +311,21 @@ def get_settings(args):
settings["wallet.storage_creds"] = args.wallet_storage_creds

if args.admin:

admin_api_key = args.admin_api_key
admin_insecure_mode = args.admin_insecure_mode

if (admin_api_key and admin_insecure_mode) or not (
admin_api_key or admin_insecure_mode
):
raise ArgsParseError(
"Either --admin-api-key or --admin-insecure-mode "
+ "must be set but not both."
)

settings["admin.admin_api_key"] = admin_api_key
settings["admin.admin_insecure_mode"] = admin_insecure_mode

settings["admin.enabled"] = True
settings["admin.host"] = args.admin[0]
settings["admin.port"] = args.admin[1]
Expand Down
7 changes: 7 additions & 0 deletions aries_cloudagent/config/error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Errors for config modules."""

from ..error import BaseError


class ArgsParseError(BaseError):
"""Error raised when there is a problem parsing the command-line arguments."""
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,7 @@ def test_type(self):
"""Test type."""
assert self.menu._type == MENU

@mock.patch(
"aries_cloudagent.messaging.actionmenu.messages.menu.MenuSchema.load"
)
@mock.patch("aries_cloudagent.messaging.actionmenu.messages.menu.MenuSchema.load")
def test_deserialize(self, mock_menu_schema_load):
"""
Test deserialization.
Expand All @@ -71,9 +69,7 @@ def test_deserialize(self, mock_menu_schema_load):

assert menu is mock_menu_schema_load.return_value

@mock.patch(
"aries_cloudagent.messaging.actionmenu.messages.menu.MenuSchema.dump"
)
@mock.patch("aries_cloudagent.messaging.actionmenu.messages.menu.MenuSchema.dump")
def test_serialize(self, mock_menu_schema_dump):
"""
Test serialization.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@
from ...agent_message import AgentMessage, AgentMessageSchema
from ..message_types import PROBLEM_REPORT

HANDLER_CLASS = (
"aries_cloudagent.messaging.problem_report.handler.ProblemReportHandler"
)
HANDLER_CLASS = "aries_cloudagent.messaging.problem_report.handler.ProblemReportHandler"


class ProblemReportReason(Enum):
Expand Down
Loading