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