From 714d4c9ab49c4534f169241ec7b5deca3648a472 Mon Sep 17 00:00:00 2001 From: Jakob Langdal Date: Tue, 14 Mar 2023 09:02:21 +0000 Subject: [PATCH 1/5] Add keycloak library --- requirements-freeze.txt | 10 ++++++---- requirements.txt | 3 ++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/requirements-freeze.txt b/requirements-freeze.txt index bd764af..cd55cfd 100644 --- a/requirements-freeze.txt +++ b/requirements-freeze.txt @@ -6,7 +6,7 @@ chardet==4.0.0 click==7.1.2 clickclick==20.10.2 colorama==0.4.4 -connexion==2.7.0 +connexion==2.14.2 cryptography==3.4.7 cycler==0.10.0 deap==1.3.3 @@ -31,7 +31,6 @@ openapi-spec-validator==0.2.9 packaging==20.9 Pillow==9.0.1 pluggy==0.13.1 -ProcessOptimizer[browniebee]==0.7.7 py==1.10.0 pycparser==2.20 pyparsing==2.4.7 @@ -39,10 +38,13 @@ pyrsistent==0.17.3 pytest==6.2.2 pytest-watch==4.2.0 python-dateutil==2.8.1 +python-keycloak==2.13.2 PyYAML==6.0 redis==4.0.2 -requests==2.25.1 +requests==2.28.2 +requests-toolbelt==0.10.1 rq==1.10.0 +rsa==4.9 scikit-learn==1.1.2 scipy==1.9.1 six==1.16.0 @@ -51,7 +53,7 @@ threadpoolctl==2.1.0 toml==0.10.2 tornado==6.2 typing_extensions==4.4.0 -urllib3==1.26.5 +urllib3==1.26.15 waitress==2.1.2 watchdog==2.0.2 Werkzeug==1.0.1 diff --git a/requirements.txt b/requirements.txt index aad987f..321a386 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ -connexion==2.7.0 +connexion==2.14.2 connexion[swagger-ui] +python-keycloak==2.13.2 Flask==1.1.2 Flask-Cors==3.0.10 ProcessOptimizer[browniebee]==0.7.7 From 42d1c0935f47a699c7ca879eb22847635fe2ab33 Mon Sep 17 00:00:00 2001 From: Jakob Langdal Date: Tue, 14 Mar 2023 12:01:37 +0000 Subject: [PATCH 2/5] Add authentication support --- README.md | 15 +++++++ optimizerapi/auth.py | 54 ++++++++++++++++++++++++++ optimizerapi/openapi/specification.yml | 16 ++++++++ optimizerapi/server.py | 5 +-- 4 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 optimizerapi/auth.py diff --git a/README.md b/README.md index 6bbb293..682e535 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,21 @@ Two specific origins: CORS_ORIGIN="(https://prod.brownie.projects.alexandra.dk|https://prod.cake.projects.alexandra.dk)" python -m optimizerapi.server +# Using authentication + +API endpoints can be protected by either a static API key or using a Keycloak OIDC server. +The static API key is configured by the environment variable `AUTH_API_KEY` + +Keycloak is configured using the following environement variables + + +|Name |Description | +|-------------------|-----------------------------------| +|AUTH_SERVER |Base url of your Keycloak server | +|AUTH_REALM_NAME |OAuth realm name | +|AUTH_CLIENT_ID |Client ID | +|AUTH_CLIENT_SECRET |Client secret | + # Adding or updating dependencies When adding a new dependency, you should manually add it to `requirements.txt` and then run the following commands: diff --git a/optimizerapi/auth.py b/optimizerapi/auth.py new file mode 100644 index 0000000..e5e3a65 --- /dev/null +++ b/optimizerapi/auth.py @@ -0,0 +1,54 @@ +"""Authenitcation module + +This module will verify tokens provided bt a Keycloak OpenID server +""" +import os +from keycloak import KeycloakOpenID + +AUTH_API_KEY = os.getenv("AUTH_API_KEY", '') +AUTH_SERVER = os.getenv("AUTH_SERVER", None) +AUTH_CLIENT_ID = os.getenv("AUTH_CLIENT_ID", None) +AUTH_CLIENT_SECRET = os.getenv("AUTH_CLIENT_SECRET", None) +AUTH_REALM_NAME = os.getenv("AUTH_REALM_NAME", None) + +keycloak_openid = KeycloakOpenID(server_url=AUTH_SERVER, + realm_name=AUTH_REALM_NAME, + client_id=AUTH_CLIENT_ID, + client_secret_key=AUTH_CLIENT_SECRET + ) + + +def token_info(access_token) -> dict: + """Verify token with authentication server + + Returns + ------- + dict + a dictionary containing sub and scope + None in case of invalid token + """ + print(access_token) + if not AUTH_SERVER: + return {'scope': []} + token = access_token + token_data = keycloak_openid.introspect(token) + if 'active' in token_data and token_data['active']: + print('OK') + return token_data + print('NOT OK') + print(token_data) + return None + + +def apikey_handler(access_token) -> dict: + """Verify API key based on environment variable + + Returns + ------- + dict + a dictionary containing sub and scope + None in case of invalid token + """ + if not AUTH_SERVER and AUTH_API_KEY == access_token: + return {'scope': []} + return None diff --git a/optimizerapi/openapi/specification.yml b/optimizerapi/openapi/specification.yml index d784d23..50e20f0 100644 --- a/optimizerapi/openapi/specification.yml +++ b/optimizerapi/openapi/specification.yml @@ -9,6 +9,9 @@ servers: paths: /optimizer: post: + security: + - oauth2: [] + - apikey: [] description: Run optimizer with the specified parameters operationId: optimizerapi.optimizer.run responses: @@ -113,6 +116,19 @@ paths: } components: + securitySchemes: + apikey: + x-apikeyInfoFunc: optimizerapi.auth.apikey_handler + type: apiKey + name: apikey + in: query + oauth2: + type: oauth2 + x-tokenInfoFunc: optimizerapi.auth.token_info + flows: + implicit: + authorizationUrl: https://keycloak.browniebee.projects.alexandra.dk/realms/brownie-bee-dev/protocol/openid-connect/auth + scopes: {} schemas: experiment: title: An experiment definition diff --git a/optimizerapi/server.py b/optimizerapi/server.py index 6e6f407..93f3174 100644 --- a/optimizerapi/server.py +++ b/optimizerapi/server.py @@ -5,16 +5,15 @@ import re import connexion from waitress import serve -from .securepickle import get_crypto from flask_cors import CORS +from .securepickle import get_crypto if __name__ == '__main__': # Initialize crypto get_crypto() app = connexion.FlaskApp( __name__, port=9090, specification_dir='./openapi/') - app.add_api('specification.yml', arguments={ - 'title': 'Hello World Example'}) + app.add_api('specification.yml', strict_validation=True) DEVELOPMENT = "development" flask_env = os.getenv("FLASK_ENV", DEVELOPMENT) From 02bf185715c4d12af12f8a1ab7c1ec7cdb0fb2fe Mon Sep 17 00:00:00 2001 From: Jakob Langdal Date: Tue, 14 Mar 2023 12:15:08 +0000 Subject: [PATCH 3/5] Set default api key --- optimizerapi/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optimizerapi/auth.py b/optimizerapi/auth.py index e5e3a65..e261478 100644 --- a/optimizerapi/auth.py +++ b/optimizerapi/auth.py @@ -5,7 +5,7 @@ import os from keycloak import KeycloakOpenID -AUTH_API_KEY = os.getenv("AUTH_API_KEY", '') +AUTH_API_KEY = os.getenv("AUTH_API_KEY", 'none') AUTH_SERVER = os.getenv("AUTH_SERVER", None) AUTH_CLIENT_ID = os.getenv("AUTH_CLIENT_ID", None) AUTH_CLIENT_SECRET = os.getenv("AUTH_CLIENT_SECRET", None) From 5e1371cb90de3fb3a685b1482fdaeff3264944d4 Mon Sep 17 00:00:00 2001 From: Jakob Langdal Date: Tue, 14 Mar 2023 13:04:55 +0000 Subject: [PATCH 4/5] Activate response validation Also fix related formatting of expected minimum --- optimizerapi/optimizer.py | 11 +++++++++-- optimizerapi/server.py | 3 ++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/optimizerapi/optimizer.py b/optimizerapi/optimizer.py index fdd115c..f705d3e 100644 --- a/optimizerapi/optimizer.py +++ b/optimizerapi/optimizer.py @@ -194,7 +194,8 @@ def process_result(result, optimizer, dimensions, cfg, extras, data, space): experiment_suggestion_count = extras["experimentSuggestionCount"] next_exp = optimizer.ask(n_points=experiment_suggestion_count) - if len(next_exp) > 0 and not any(isinstance(x, list) for x in next_exp): next_exp = [next_exp] + if len(next_exp) > 0 and not any(isinstance(x, list) for x in next_exp): + next_exp = [next_exp] result_details["next"] = round_to_length_scales(next_exp, optimizer.space) if len(data) >= cfg["initialPoints"]: @@ -209,7 +210,7 @@ def process_result(result, optimizer, dimensions, cfg, extras, data, space): plot_objective(model, dimensions=dimensions, usepartialdependence=False, - show_confidence=True, + show_confidence=True, pars=objective_pars) add_plot(plots, f"objective_{idx}") @@ -228,6 +229,12 @@ def process_result(result, optimizer, dimensions, cfg, extras, data, space): add_version_info(result_details["extras"]) # print(str(response)) + org_models = response["result"]['models'] + for model in org_models: + # Flatten expected minimum entries + model['expected_minimum'] = [[ + item for sublist in [x if isinstance( + x, list) else [x] for x in model['expected_minimum']] for item in sublist]] return response diff --git a/optimizerapi/server.py b/optimizerapi/server.py index 93f3174..92b8cba 100644 --- a/optimizerapi/server.py +++ b/optimizerapi/server.py @@ -13,7 +13,8 @@ get_crypto() app = connexion.FlaskApp( __name__, port=9090, specification_dir='./openapi/') - app.add_api('specification.yml', strict_validation=True) + app.add_api('specification.yml', strict_validation=True, + validate_responses=True) DEVELOPMENT = "development" flask_env = os.getenv("FLASK_ENV", DEVELOPMENT) From 30d4792994601bb18e567110846223e2f16f0cd8 Mon Sep 17 00:00:00 2001 From: Jakob Langdal Date: Tue, 14 Mar 2023 16:04:16 +0000 Subject: [PATCH 5/5] Reintroduce ProcessOptimizer dependency --- requirements-freeze.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-freeze.txt b/requirements-freeze.txt index cd55cfd..19701a7 100644 --- a/requirements-freeze.txt +++ b/requirements-freeze.txt @@ -27,6 +27,7 @@ kiwisolver==1.3.1 MarkupSafe==1.1.1 matplotlib==3.5.3 numpy==1.23.3 +ProcessOptimizer[browniebee]==0.7.7 openapi-spec-validator==0.2.9 packaging==20.9 Pillow==9.0.1