Skip to content
Snippets Groups Projects
Commit 1327f670 authored by Adeel Ahmad's avatar Adeel Ahmad
Browse files

Merge branch 'DBOARD3-1142/token-auth' into 'develop'

Dboard3 1142/token auth

See merge request !50
parents d4b283eb c564be2c
No related branches found
No related tags found
1 merge request!50Dboard3 1142/token auth
......@@ -10,5 +10,6 @@ coverage.xml
htmlcov
dist
venv
.venv
.vscode
docs/build
......@@ -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.
......
......@@ -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():
# This method is a boilerplate required by the library to enable authentication
pass
# IMS based routes
from inventory_provider.routes import lg
......
from functools import wraps
from flask import current_app, g, jsonify
from flask_httpauth import HTTPTokenAuth
from inventory_provider.config import ServiceName
from typing import Callable, List, Optional
auth = HTTPTokenAuth(scheme="ApiKey")
@auth.verify_token
def verify_api_key(api_key: Optional[str]) -> Optional[ServiceName]:
config = current_app.config["INVENTORY_PROVIDER_CONFIG"]
# This is to enable anonymous access for testing.
if not api_key:
g.auth_client = ServiceName.ANONYMOUS
return ServiceName.ANONYMOUS
for client, details in config['api-keys'].items():
if details.get('api-key') == api_key:
g.auth_client = client
return client
return None
def authorize(*, allowed_clients: List[ServiceName]) -> Callable:
"""Decorator to restrict route access to specific clients."""
if not isinstance(allowed_clients, list):
raise TypeError("allowed_clients must be a list of allowed service names")
def decorator(f: Callable) -> Callable:
@wraps(f)
def wrapped(*args, **kwargs):
client = g.get("auth_client")
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 != ServiceName.ANONYMOUS:
return jsonify({"error": "Forbidden"}), 403
return f(*args, **kwargs)
return wrapped
return decorator
import json
import jsonschema
from enum import StrEnum
class ServiceName(StrEnum):
DASHBOARD = 'dashboard'
BRIAN = 'brian'
REPORTING = 'reporting'
ANONYMOUS = 'anonymous'
CONFIG_SCHEMA = {
'$schema': 'https://json-schema.org/draft-07/schema#',
......@@ -10,6 +19,23 @@ 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",
'properties': {
ServiceName.DASHBOARD: {'$ref': '#/definitions/api-key'},
ServiceName.BRIAN: {'$ref': '#/definitions/api-key'},
ServiceName.REPORTING: {'$ref': '#/definitions/api-key'}
},
"additionalProperties": False
},
'ssh-credentials': {
'type': 'object',
'properties': {
......@@ -235,6 +261,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'},
......
......@@ -67,6 +67,9 @@ 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.auth import authorize
from inventory_provider.config import ServiceName
routes = Blueprint("inventory-data-classifier-support-routes", __name__)
logger = logging.getLogger(__name__)
......@@ -333,6 +336,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=[ServiceName.DASHBOARD])
def handle_link_info_request(source_equipment: str, interface: str) -> Response:
"""
Handler for /classifier/juniper-link-info that
......@@ -374,6 +378,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=[ServiceName.DASHBOARD])
def handle_epipe_sap_info_request(source_equipment: str, service_id: str, vpn_id: str) -> Response:
r = common.get_current_redis()
......@@ -602,6 +607,7 @@ def find_interfaces(address):
@routes.route("/peer-info/<address_str>", methods=['GET'])
@common.require_accepts_json
@authorize(allowed_clients=[ServiceName.DASHBOARD])
def peer_info(address_str: str) -> Response:
"""
Handler for /classifier/peer-info that returns bgp peering metadata.
......@@ -697,6 +703,7 @@ def peer_info(address_str: str) -> Response:
@routes.route(
"/mtc-interface-info/<node>/<interface>", methods=['GET'])
@common.require_accepts_json
@authorize(allowed_clients=[ServiceName.DASHBOARD])
def get_mtc_interface_info(node, interface):
"""
Handler for /classifier/mtc-interface-info that
......@@ -738,6 +745,7 @@ def get_mtc_interface_info(node, interface):
"<source_equipment>/<interface>/<circuit_id>",
methods=['GET'])
@common.require_accepts_json
@authorize(allowed_clients=[ServiceName.DASHBOARD])
def get_trap_metadata(source_equipment: str, interface: str, circuit_id: str) \
-> Response:
"""
......@@ -831,6 +839,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=[ServiceName.DASHBOARD])
def get_fiberlink_trap_metadata(ne_name_str: str, object_name_str: str) \
-> Response:
"""
......@@ -967,6 +976,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=[ServiceName.DASHBOARD])
def get_tnms_fibre_trap_metadata(enms_pc_name: str) -> Response:
"""
Handler for /classifier/infinera-fiberlink-info that
......@@ -1069,6 +1079,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=[ServiceName.DASHBOARD])
def get_coriant_port_info(equipment_name: str, entity_string: str) -> Response:
"""
Handler for /classifier/coriant-info that
......@@ -1090,6 +1101,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=[ServiceName.DASHBOARD])
def get_coriant_tp_info(equipment_name: str, entity_string: str) -> Response:
"""
Handler for /classifier/coriant-info that
......@@ -1200,6 +1212,7 @@ def _get_coriant_info(
@routes.route("/router-info", methods=["GET"])
@common.require_accepts_json
@authorize(allowed_clients=[ServiceName.DASHBOARD])
def get_all_routers() -> Response:
redis = common.get_current_redis()
result = cache_result(
......@@ -1222,6 +1235,7 @@ def _get_router_list(redis):
@routes.route("/router-info/<equipment_name>", methods=["GET"])
@common.require_accepts_json
@authorize(allowed_clients=[ServiceName.DASHBOARD])
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)
......
......@@ -16,6 +16,7 @@ lxml==4.9.4
requests
netifaces
tree-format
Flask-HTTPAuth
pytest
pytest-mock
......
......@@ -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",
......
import jsonschema
from inventory_provider.routes.classifier_schema import (
ROUTER_INFO_ALL_ROUTERS_RESPONSE_SCHEMA,
)
def test_classifier_router_no_key(client):
DEFAULT_REQUEST_HEADERS_NO_KEY = {
"Content-type": "application/json",
"Accept": ["application/json"],
}
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):
DEFAULT_REQUEST_HEADERS_DASHBOARD_KEY = {
"Content-type": "application/json",
"Accept": ["application/json"],
"Authorization": "ApiKey dashboard_key",
}
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):
DEFAULT_REQUEST_HEADERS_BRIAN_KEY = {
"Content-type": "application/json",
"Accept": ["application/json"],
"Authorization": "ApiKey brian_key",
}
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):
DEFAULT_REQUEST_HEADERS_REPORTING_KEY = {
"Content-type": "application/json",
"Accept": ["application/json"],
"Authorization": "ApiKey reporting_key",
}
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):
DEFAULT_REQUEST_HEADERS_BAD_KEY = {
"Content-type": "application/json",
"Accept": ["application/json"],
"Authorization": "ApiKey badapikey",
}
rv = client.get("/classifier/router-info", headers=DEFAULT_REQUEST_HEADERS_BAD_KEY)
assert rv.status_code == 401
result = rv.text
assert result == "Unauthorized Access"
......@@ -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,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment