From 158a33137f087321db07f668e8fe3ee857609ffc Mon Sep 17 00:00:00 2001 From: Adeel Ahmad <adeel.ahmad@geant.org> Date: Wed, 12 Mar 2025 16:51:04 +0000 Subject: [PATCH 1/5] Add optional token based authentication --- .gitignore | 1 + docs/source/configuration.rst | 2 +- inventory_provider/__init__.py | 8 ++++++++ inventory_provider/auth.py | 17 +++++++++++++++++ inventory_provider/config.py | 19 +++++++++++++++++++ requirements.txt | 1 + 6 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 inventory_provider/auth.py diff --git a/.gitignore b/.gitignore index e8bf92ea..33b68146 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,6 @@ coverage.xml htmlcov dist venv +.venv .vscode docs/build diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index 3502d074..28b30adf 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -54,7 +54,7 @@ This module has been tested in the following execution environments: .. code-block:: bash $ export FLASK_APP=app.py - $ export SETTINGS_FILENAME=settings.cfg + $ export FLASK_SETTINGS_FILENAME=settings.cfg $ flask run * As an Apache/`mod_wsgi` service. diff --git a/inventory_provider/__init__.py b/inventory_provider/__init__.py index 09574fb6..590610d4 100644 --- a/inventory_provider/__init__.py +++ b/inventory_provider/__init__.py @@ -7,6 +7,7 @@ from flask import Flask from flask_cors import CORS from inventory_provider import environment +from inventory_provider.auth import auth def create_app(setup_logging=True): @@ -48,6 +49,13 @@ def create_app(setup_logging=True): app.config['INVENTORY_PROVIDER_CONFIG'] = inventory_provider_config + # Apply authentication globally to all routes + @app.before_request + @auth.login_required + def secure_before_request(): + """Enforces authentication for all routes""" + pass + # IMS based routes from inventory_provider.routes import lg diff --git a/inventory_provider/auth.py b/inventory_provider/auth.py new file mode 100644 index 00000000..1f995033 --- /dev/null +++ b/inventory_provider/auth.py @@ -0,0 +1,17 @@ +from flask import Blueprint, current_app +from flask_httpauth import HTTPTokenAuth + +auth = HTTPTokenAuth(scheme="ApiKey") + +@auth.verify_token +def verify_api_key(api_key): + config = current_app.config["INVENTORY_PROVIDER_CONFIG"] + # This is to enable anonymous access for testing. + if not api_key: + return "test" + + for service, details in config['api-keys'].items(): + if details.get('api-key') == api_key: + return service + return None + diff --git a/inventory_provider/config.py b/inventory_provider/config.py index 3fa03b9d..0df04129 100644 --- a/inventory_provider/config.py +++ b/inventory_provider/config.py @@ -10,6 +10,24 @@ CONFIG_SCHEMA = { 'maximum': 60, # sanity 'exclusiveMinimum': 0 }, + "api-keys-credentials": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9-_]+$": { + "type": "object", + "properties": { + "api-key": { + "type": "string", + # "minLength": 32, + # "description": "API key (Base64, UUID, or Hexadecimal format)" + } + }, + "required": ["api-key"], + "additionalProperties": False + } + }, + "additionalProperties": False + }, 'ssh-credentials': { 'type': 'object', 'properties': { @@ -235,6 +253,7 @@ CONFIG_SCHEMA = { 'type': 'object', 'properties': { + 'api-keys': {'$ref': '#/definitions/api-keys-credentials'}, 'ssh': {'$ref': '#/definitions/ssh-credentials'}, 'nokia-ssh': {'$ref': '#/definitions/nokia-ssh-credentials'}, 'redis': {'$ref': '#/definitions/redis-credentials'}, diff --git a/requirements.txt b/requirements.txt index 0d226651..9095eb74 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,6 +16,7 @@ lxml==4.9.4 requests netifaces tree-format +Flask-HTTPAuth pytest pytest-mock -- GitLab From 322fa06ff0931a769efbc090127ef4817866b26f Mon Sep 17 00:00:00 2001 From: Adeel Ahmad <adeel.ahmad@geant.org> Date: Tue, 18 Mar 2025 16:27:15 +0000 Subject: [PATCH 2/5] Add route based authorisation for services --- inventory_provider/__init__.py | 21 +++++++++++++++++++-- inventory_provider/auth.py | 3 ++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/inventory_provider/__init__.py b/inventory_provider/__init__.py index 590610d4..41575bae 100644 --- a/inventory_provider/__init__.py +++ b/inventory_provider/__init__.py @@ -3,7 +3,7 @@ automatically invoked app factory """ import logging import os -from flask import Flask +from flask import g, Flask, request, jsonify from flask_cors import CORS from inventory_provider import environment @@ -54,7 +54,24 @@ def create_app(setup_logging=True): @auth.login_required def secure_before_request(): """Enforces authentication for all routes""" - pass + client = g.get("auth_service") + + if not client: + # This allows clients to access any resource without providing an API key + # TODO: Only for testing, should be removed in Production + return + # return jsonify({"error": "Unauthorized"}), 403 + + CLIENT_PERMISSIONS = { + "serviceA": ["msr"], + "serviceB": ["testing"], + } + + allowed_routes = CLIENT_PERMISSIONS.get(client, []) + route = request.path.strip("/").split("/")[0] + + if route not in allowed_routes: + return jsonify({"error": "Forbidden"}), 403 # IMS based routes diff --git a/inventory_provider/auth.py b/inventory_provider/auth.py index 1f995033..466073e3 100644 --- a/inventory_provider/auth.py +++ b/inventory_provider/auth.py @@ -1,4 +1,4 @@ -from flask import Blueprint, current_app +from flask import Blueprint, current_app, g from flask_httpauth import HTTPTokenAuth auth = HTTPTokenAuth(scheme="ApiKey") @@ -12,6 +12,7 @@ def verify_api_key(api_key): for service, details in config['api-keys'].items(): if details.get('api-key') == api_key: + g.auth_service = service return service return None -- GitLab From 560a88b67c5b08864e1b9aca2d3bb00cddf80603 Mon Sep 17 00:00:00 2001 From: Adeel Ahmad <adeel.ahmad@geant.org> Date: Wed, 19 Mar 2025 11:52:38 +0000 Subject: [PATCH 3/5] Create decorator to allow per route authorisation --- inventory_provider/__init__.py | 22 ++---------------- inventory_provider/auth.py | 34 ++++++++++++++++++++++++---- inventory_provider/routes/msr.py | 2 ++ inventory_provider/routes/testing.py | 2 ++ 4 files changed, 35 insertions(+), 25 deletions(-) diff --git a/inventory_provider/__init__.py b/inventory_provider/__init__.py index 41575bae..915b79a2 100644 --- a/inventory_provider/__init__.py +++ b/inventory_provider/__init__.py @@ -3,7 +3,7 @@ automatically invoked app factory """ import logging import os -from flask import g, Flask, request, jsonify +from flask import Flask from flask_cors import CORS from inventory_provider import environment @@ -53,25 +53,7 @@ def create_app(setup_logging=True): @app.before_request @auth.login_required def secure_before_request(): - """Enforces authentication for all routes""" - client = g.get("auth_service") - - if not client: - # This allows clients to access any resource without providing an API key - # TODO: Only for testing, should be removed in Production - return - # return jsonify({"error": "Unauthorized"}), 403 - - CLIENT_PERMISSIONS = { - "serviceA": ["msr"], - "serviceB": ["testing"], - } - - allowed_routes = CLIENT_PERMISSIONS.get(client, []) - route = request.path.strip("/").split("/")[0] - - if route not in allowed_routes: - return jsonify({"error": "Forbidden"}), 403 + pass # IMS based routes diff --git a/inventory_provider/auth.py b/inventory_provider/auth.py index 466073e3..7ee9651e 100644 --- a/inventory_provider/auth.py +++ b/inventory_provider/auth.py @@ -1,5 +1,6 @@ -from flask import Blueprint, current_app, g +from flask import current_app, g, jsonify from flask_httpauth import HTTPTokenAuth +from functools import wraps auth = HTTPTokenAuth(scheme="ApiKey") @@ -8,11 +9,34 @@ def verify_api_key(api_key): config = current_app.config["INVENTORY_PROVIDER_CONFIG"] # This is to enable anonymous access for testing. if not api_key: - return "test" + g.auth_client = "anonymous" + return "anonymous" - for service, details in config['api-keys'].items(): + for client, details in config['api-keys'].items(): if details.get('api-key') == api_key: - g.auth_service = service - return service + g.auth_client = client + return client return None +def authorize(*, allowed_clients): + """Decorator to restrict route access to specific clients.""" + if not isinstance(allowed_clients, (list, tuple)): + allowed_clients = [allowed_clients] # Convert single client to list + def decorator(f): + @wraps(f) + def wrapped(*args, **kwargs): + client = g.get("auth_client") + + if not client: + return jsonify({"error": "Unauthorized"}), 403 + + if client not in allowed_clients: + # Anonymous clients are allowed to access any resource without providing an API key + # TODO: Only for testing, should be removed in Production + if client != "anonymous": + return jsonify({"error": "Forbidden"}), 403 + + return f(*args, **kwargs) + + return wrapped + return decorator \ No newline at end of file diff --git a/inventory_provider/routes/msr.py b/inventory_provider/routes/msr.py index ac2aa9b9..fe40bca8 100644 --- a/inventory_provider/routes/msr.py +++ b/inventory_provider/routes/msr.py @@ -118,6 +118,7 @@ from inventory_provider.routes.common import _ignore_cache_or_retrieve, \ ims_equipment_to_hostname from inventory_provider.routes.poller import get_services from inventory_provider.tasks import common as tasks_common +from inventory_provider.auth import authorize routes = Blueprint('msr-query-routes', __name__) logger = logging.getLogger(__name__) @@ -1447,6 +1448,7 @@ def _asn_peers(asn, group, instance): @routes.route('/asn-peers', methods=['GET'], defaults={'asn': None}) @routes.route('/asn-peers/<int:asn>', methods=['GET']) @common.require_accepts_json +@authorize(allowed_clients="reporting") def asn_peers_get(asn): """ cf. doc for _asn_peers diff --git a/inventory_provider/routes/testing.py b/inventory_provider/routes/testing.py index 5fcb7cce..c43dccb4 100644 --- a/inventory_provider/routes/testing.py +++ b/inventory_provider/routes/testing.py @@ -12,6 +12,7 @@ from inventory_provider import juniper from inventory_provider.routes import common from inventory_provider.tasks import worker from inventory_provider.tasks import common as worker_common +from inventory_provider.auth import authorize routes = Blueprint("inventory-data-testing-support-routes", __name__) @@ -110,6 +111,7 @@ def routers_from_config_dir(): @routes.route("latchdb", methods=['GET']) +@authorize(allowed_clients=("brian", "dashboard")) def latch_db(): config = current_app.config["INVENTORY_PROVIDER_CONFIG"] worker_common.latch_db(config) -- GitLab From 0f66457b8f4fc970e131992863064f5be194a76f Mon Sep 17 00:00:00 2001 From: Adeel Ahmad <adeel.ahmad@geant.org> Date: Thu, 20 Mar 2025 10:11:24 +0000 Subject: [PATCH 4/5] Update config schema for API keys and restrict decorator input to be a list --- inventory_provider/__init__.py | 1 + inventory_provider/auth.py | 12 +++++----- inventory_provider/config.py | 30 ++++++++++++++----------- inventory_provider/routes/classifier.py | 13 +++++++++++ inventory_provider/routes/msr.py | 2 -- inventory_provider/routes/testing.py | 2 -- 6 files changed, 38 insertions(+), 22 deletions(-) diff --git a/inventory_provider/__init__.py b/inventory_provider/__init__.py index 915b79a2..3155d6cc 100644 --- a/inventory_provider/__init__.py +++ b/inventory_provider/__init__.py @@ -53,6 +53,7 @@ def create_app(setup_logging=True): @app.before_request @auth.login_required def secure_before_request(): + # This method is a boilerplate required by the library to enable authentication pass # IMS based routes diff --git a/inventory_provider/auth.py b/inventory_provider/auth.py index 7ee9651e..ba5ef9b6 100644 --- a/inventory_provider/auth.py +++ b/inventory_provider/auth.py @@ -1,6 +1,7 @@ from flask import current_app, g, jsonify from flask_httpauth import HTTPTokenAuth from functools import wraps +from config import ANONYMOUS_SERVICE_NAME auth = HTTPTokenAuth(scheme="ApiKey") @@ -9,8 +10,8 @@ def verify_api_key(api_key): config = current_app.config["INVENTORY_PROVIDER_CONFIG"] # This is to enable anonymous access for testing. if not api_key: - g.auth_client = "anonymous" - return "anonymous" + g.auth_client = ANONYMOUS_SERVICE_NAME + return ANONYMOUS_SERVICE_NAME for client, details in config['api-keys'].items(): if details.get('api-key') == api_key: @@ -20,8 +21,9 @@ def verify_api_key(api_key): def authorize(*, allowed_clients): """Decorator to restrict route access to specific clients.""" - if not isinstance(allowed_clients, (list, tuple)): - allowed_clients = [allowed_clients] # Convert single client to list + if not isinstance(allowed_clients, list): + raise TypeError("allowed_clients must be a list of allowed service names") + def decorator(f): @wraps(f) def wrapped(*args, **kwargs): @@ -33,7 +35,7 @@ def authorize(*, allowed_clients): if client not in allowed_clients: # Anonymous clients are allowed to access any resource without providing an API key # TODO: Only for testing, should be removed in Production - if client != "anonymous": + if client != ANONYMOUS_SERVICE_NAME: return jsonify({"error": "Forbidden"}), 403 return f(*args, **kwargs) diff --git a/inventory_provider/config.py b/inventory_provider/config.py index 0df04129..c8497a8b 100644 --- a/inventory_provider/config.py +++ b/inventory_provider/config.py @@ -1,6 +1,11 @@ import json import jsonschema +DASHBOARD_SERVICE_NAME = 'dashboard' +BRIAN_SERVICE_NAME = 'brian' +REPORTING_SERVICE_NAME = 'reporting' +ANONYMOUS_SERVICE_NAME = 'anonymous' + CONFIG_SCHEMA = { '$schema': 'https://json-schema.org/draft-07/schema#', @@ -10,21 +15,20 @@ CONFIG_SCHEMA = { 'maximum': 60, # sanity 'exclusiveMinimum': 0 }, + 'api-key': { + "type": "object", + "properties": { + "api-key": {"type": "string"} + }, + "required": ["api-key"], + "additionalProperties": False + }, "api-keys-credentials": { "type": "object", - "patternProperties": { - "^[a-zA-Z0-9-_]+$": { - "type": "object", - "properties": { - "api-key": { - "type": "string", - # "minLength": 32, - # "description": "API key (Base64, UUID, or Hexadecimal format)" - } - }, - "required": ["api-key"], - "additionalProperties": False - } + 'properties': { + DASHBOARD_SERVICE_NAME: {'$ref': '#/definitions/api-key'}, + BRIAN_SERVICE_NAME: {'$ref': '#/definitions/api-key'}, + REPORTING_SERVICE_NAME: {'$ref': '#/definitions/api-key'} }, "additionalProperties": False }, diff --git a/inventory_provider/routes/classifier.py b/inventory_provider/routes/classifier.py index 47c4e888..8b65079b 100644 --- a/inventory_provider/routes/classifier.py +++ b/inventory_provider/routes/classifier.py @@ -67,6 +67,8 @@ from redis import Redis from inventory_provider.routes import common from inventory_provider.routes.common import _ignore_cache_or_retrieve, cache_result +from inventory_provider.config import authorize, DASHBOARD_SERVICE_NAME + routes = Blueprint("inventory-data-classifier-support-routes", __name__) logger = logging.getLogger(__name__) @@ -331,6 +333,7 @@ def get_link_info_response_body( @routes.route("/juniper-link-info/<source_equipment>/<path:interface>", methods=['GET']) @common.require_accepts_json +@authorize(allowed_clients=[DASHBOARD_SERVICE_NAME]) def handle_link_info_request(source_equipment: str, interface: str) -> Response: """ Handler for /classifier/juniper-link-info that @@ -372,6 +375,7 @@ def handle_link_info_request(source_equipment: str, interface: str) -> Response: @routes.route("/epipe-sap-info/<source_equipment>/<service_id>/<vpn_id>", methods=['GET']) @common.require_accepts_json +@authorize(allowed_clients=[DASHBOARD_SERVICE_NAME]) def handle_epipe_sap_info_request(source_equipment: str, service_id: str, vpn_id: str) -> Response: r = common.get_current_redis() @@ -600,6 +604,7 @@ def find_interfaces(address): @routes.route("/peer-info/<address_str>", methods=['GET']) @common.require_accepts_json +@authorize(allowed_clients=[DASHBOARD_SERVICE_NAME]) def peer_info(address_str: str) -> Response: """ Handler for /classifier/peer-info that returns bgp peering metadata. @@ -695,6 +700,7 @@ def peer_info(address_str: str) -> Response: @routes.route( "/mtc-interface-info/<node>/<interface>", methods=['GET']) @common.require_accepts_json +@authorize(allowed_clients=[DASHBOARD_SERVICE_NAME]) def get_mtc_interface_info(node, interface): """ Handler for /classifier/mtc-interface-info that @@ -736,6 +742,7 @@ def get_mtc_interface_info(node, interface): "<source_equipment>/<interface>/<circuit_id>", methods=['GET']) @common.require_accepts_json +@authorize(allowed_clients=[DASHBOARD_SERVICE_NAME]) def get_trap_metadata(source_equipment: str, interface: str, circuit_id: str) \ -> Response: """ @@ -829,6 +836,7 @@ def get_trap_metadata(source_equipment: str, interface: str, circuit_id: str) \ @routes.route("/infinera-fiberlink-info/<ne_name_str>/<object_name_str>", methods=['GET']) @common.require_accepts_json +@authorize(allowed_clients=[DASHBOARD_SERVICE_NAME]) def get_fiberlink_trap_metadata(ne_name_str: str, object_name_str: str) \ -> Response: """ @@ -965,6 +973,7 @@ def get_fiberlink_trap_metadata(ne_name_str: str, object_name_str: str) \ @routes.route("/tnms-fibre-info/<path:enms_pc_name>", methods=['GET']) @common.require_accepts_json +@authorize(allowed_clients=[DASHBOARD_SERVICE_NAME]) def get_tnms_fibre_trap_metadata(enms_pc_name: str) -> Response: """ Handler for /classifier/infinera-fiberlink-info that @@ -1067,6 +1076,7 @@ def get_tnms_fibre_trap_metadata(enms_pc_name: str) -> Response: @routes.route('/coriant-port-info/<equipment_name>/<path:entity_string>', methods=['GET']) @common.require_accepts_json +@authorize(allowed_clients=[DASHBOARD_SERVICE_NAME]) def get_coriant_port_info(equipment_name: str, entity_string: str) -> Response: """ Handler for /classifier/coriant-info that @@ -1088,6 +1098,7 @@ def get_coriant_port_info(equipment_name: str, entity_string: str) -> Response: @routes.route('/coriant-tp-info/<equipment_name>/<path:entity_string>', methods=['GET']) @common.require_accepts_json +@authorize(allowed_clients=[DASHBOARD_SERVICE_NAME]) def get_coriant_tp_info(equipment_name: str, entity_string: str) -> Response: """ Handler for /classifier/coriant-info that @@ -1198,6 +1209,7 @@ def _get_coriant_info( @routes.route("/router-info", methods=["GET"]) @common.require_accepts_json +@authorize(allowed_clients=[DASHBOARD_SERVICE_NAME]) def get_all_routers() -> Response: redis = common.get_current_redis() result = cache_result( @@ -1220,6 +1232,7 @@ def _get_router_list(redis): @routes.route("/router-info/<equipment_name>", methods=["GET"]) @common.require_accepts_json +@authorize(allowed_clients=[DASHBOARD_SERVICE_NAME]) def get_router_info(equipment_name: str) -> Response: redis = common.get_current_redis() ims_equipment_name = get_ims_equipment_name_or_none(equipment_name, redis) diff --git a/inventory_provider/routes/msr.py b/inventory_provider/routes/msr.py index fe40bca8..ac2aa9b9 100644 --- a/inventory_provider/routes/msr.py +++ b/inventory_provider/routes/msr.py @@ -118,7 +118,6 @@ from inventory_provider.routes.common import _ignore_cache_or_retrieve, \ ims_equipment_to_hostname from inventory_provider.routes.poller import get_services from inventory_provider.tasks import common as tasks_common -from inventory_provider.auth import authorize routes = Blueprint('msr-query-routes', __name__) logger = logging.getLogger(__name__) @@ -1448,7 +1447,6 @@ def _asn_peers(asn, group, instance): @routes.route('/asn-peers', methods=['GET'], defaults={'asn': None}) @routes.route('/asn-peers/<int:asn>', methods=['GET']) @common.require_accepts_json -@authorize(allowed_clients="reporting") def asn_peers_get(asn): """ cf. doc for _asn_peers diff --git a/inventory_provider/routes/testing.py b/inventory_provider/routes/testing.py index c43dccb4..5fcb7cce 100644 --- a/inventory_provider/routes/testing.py +++ b/inventory_provider/routes/testing.py @@ -12,7 +12,6 @@ from inventory_provider import juniper from inventory_provider.routes import common from inventory_provider.tasks import worker from inventory_provider.tasks import common as worker_common -from inventory_provider.auth import authorize routes = Blueprint("inventory-data-testing-support-routes", __name__) @@ -111,7 +110,6 @@ def routers_from_config_dir(): @routes.route("latchdb", methods=['GET']) -@authorize(allowed_clients=("brian", "dashboard")) def latch_db(): config = current_app.config["INVENTORY_PROVIDER_CONFIG"] worker_common.latch_db(config) -- GitLab From 25c0d990a5f80fc209fd0eddcfd96100c821870f Mon Sep 17 00:00:00 2001 From: Adeel Ahmad <adeel.ahmad@geant.org> Date: Fri, 21 Mar 2025 10:18:13 +0000 Subject: [PATCH 5/5] Add unit tests for API authentication flow --- inventory_provider/auth.py | 13 ++--- inventory_provider/routes/classifier.py | 3 +- test/conftest.py | 11 ++++ test/test_auth.py | 75 +++++++++++++++++++++++++ test/test_flask_config.py | 11 ++++ 5 files changed, 103 insertions(+), 10 deletions(-) create mode 100644 test/test_auth.py diff --git a/inventory_provider/auth.py b/inventory_provider/auth.py index ba5ef9b6..1dd8ff81 100644 --- a/inventory_provider/auth.py +++ b/inventory_provider/auth.py @@ -1,7 +1,8 @@ +from functools import wraps from flask import current_app, g, jsonify from flask_httpauth import HTTPTokenAuth -from functools import wraps -from config import ANONYMOUS_SERVICE_NAME + +from inventory_provider.config import ANONYMOUS_SERVICE_NAME auth = HTTPTokenAuth(scheme="ApiKey") @@ -28,17 +29,11 @@ def authorize(*, allowed_clients): @wraps(f) def wrapped(*args, **kwargs): client = g.get("auth_client") - - if not client: - return jsonify({"error": "Unauthorized"}), 403 - if client not in allowed_clients: # Anonymous clients are allowed to access any resource without providing an API key # TODO: Only for testing, should be removed in Production if client != ANONYMOUS_SERVICE_NAME: return jsonify({"error": "Forbidden"}), 403 - return f(*args, **kwargs) - return wrapped - return decorator \ No newline at end of file + return decorator diff --git a/inventory_provider/routes/classifier.py b/inventory_provider/routes/classifier.py index 8b65079b..018df7f5 100644 --- a/inventory_provider/routes/classifier.py +++ b/inventory_provider/routes/classifier.py @@ -67,7 +67,8 @@ from redis import Redis from inventory_provider.routes import common from inventory_provider.routes.common import _ignore_cache_or_retrieve, cache_result -from inventory_provider.config import authorize, DASHBOARD_SERVICE_NAME +from inventory_provider.auth import authorize +from inventory_provider.config import DASHBOARD_SERVICE_NAME routes = Blueprint("inventory-data-classifier-support-routes", __name__) diff --git a/test/conftest.py b/test/conftest.py index 1c276b4c..35412682 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -70,6 +70,17 @@ def data_config_filename(): with tempfile.NamedTemporaryFile() as f: config = { + "api-keys": { + "brian": { + "api-key": "brian_key" + }, + "dashboard": { + "api-key": "dashboard_key" + }, + "reporting": { + "api-key": "reporting_key" + }, + }, "ssh": { "username": "uSeR-NaMe", "private-key": "private-key-filename", diff --git a/test/test_auth.py b/test/test_auth.py new file mode 100644 index 00000000..226ab7a5 --- /dev/null +++ b/test/test_auth.py @@ -0,0 +1,75 @@ +import jsonschema + +from inventory_provider.routes.classifier_schema import ( + ROUTER_INFO_ALL_ROUTERS_RESPONSE_SCHEMA, +) + +DEFAULT_REQUEST_HEADERS_NO_KEY = { + "Content-type": "application/json", + "Accept": ["application/json"], +} + +DEFAULT_REQUEST_HEADERS_BRIAN_KEY = { + "Content-type": "application/json", + "Accept": ["application/json"], + "Authorization": "ApiKey brian_key", +} + +DEFAULT_REQUEST_HEADERS_REPORTING_KEY = { + "Content-type": "application/json", + "Accept": ["application/json"], + "Authorization": "ApiKey reporting_key", +} + +DEFAULT_REQUEST_HEADERS_DASHBOARD_KEY = { + "Content-type": "application/json", + "Accept": ["application/json"], + "Authorization": "ApiKey dashboard_key", +} + +DEFAULT_REQUEST_HEADERS_BAD_KEY = { + "Content-type": "application/json", + "Accept": ["application/json"], + "Authorization": "ApiKey badapikey", +} + +def test_classifier_router_no_key(client): + rv = client.get("/classifier/router-info", headers=DEFAULT_REQUEST_HEADERS_NO_KEY) + assert rv.status_code == 200 + assert rv.is_json + result = rv.json + + jsonschema.validate(result, ROUTER_INFO_ALL_ROUTERS_RESPONSE_SCHEMA) + assert len(result) > 0 + +def test_classifier_router_dashboard_key(client): + rv = client.get("/classifier/router-info", headers=DEFAULT_REQUEST_HEADERS_DASHBOARD_KEY) + assert rv.status_code == 200 + assert rv.is_json + result = rv.json + + jsonschema.validate(result, ROUTER_INFO_ALL_ROUTERS_RESPONSE_SCHEMA) + assert len(result) > 0 + +def test_classifier_router_brian_key(client): + rv = client.get("/classifier/router-info", headers=DEFAULT_REQUEST_HEADERS_BRIAN_KEY) + assert rv.status_code == 403 + assert rv.is_json + result = rv.json + + assert result["error"] == "Forbidden" + +def test_classifier_router_reporting_key(client): + rv = client.get("/classifier/router-info", headers=DEFAULT_REQUEST_HEADERS_REPORTING_KEY) + assert rv.status_code == 403 + assert rv.is_json + result = rv.json + + assert result["error"] == "Forbidden" + +def test_classifier_router_bad_key(client): + rv = client.get("/classifier/router-info", headers=DEFAULT_REQUEST_HEADERS_BAD_KEY) + assert rv.status_code == 401 + result = rv.text + + assert result == "Unauthorized Access" diff --git a/test/test_flask_config.py b/test/test_flask_config.py index 7ba83ec5..48f7e29c 100644 --- a/test/test_flask_config.py +++ b/test/test_flask_config.py @@ -7,6 +7,17 @@ from inventory_provider.config import CONFIG_SCHEMA @pytest.fixture def config(): return { + "api-keys": { + "brian": { + "api-key": "brian_key" + }, + "dashboard": { + "api-key": "dashboard_key" + }, + "reporting": { + "api-key": "reporting_key" + }, + }, 'redis': { 'hostname': 'localhost', 'port': 6379, -- GitLab