From 06f9965f165a29a1fa3506366edc9fb89e2cfe52 Mon Sep 17 00:00:00 2001 From: Doug Boulware Date: Fri, 29 Jul 2022 09:28:19 -0600 Subject: [PATCH 01/32] Initial implementation of certificate rest api authentication --- nginx/conf.template | 2 +- scripts/create_superuser.py | 1 + src/authentication/auth.py | 80 +++++++++++-------------------------- src/scheduler/scheduler.py | 9 +++-- src/sensor/settings.py | 4 +- 5 files changed, 33 insertions(+), 63 deletions(-) diff --git a/nginx/conf.template b/nginx/conf.template index 30a4750a..ed438618 100644 --- a/nginx/conf.template +++ b/nginx/conf.template @@ -32,7 +32,7 @@ server { ssl_certificate_key /etc/ssl/private/ssl-cert.key; ssl_protocols TLSv1.2; ssl_client_certificate /etc/ssl/certs/ca.crt; - # ssl_verify_client on; + ssl_verify_client on; # ssl_ocsp on; # Enable OCSP validation ssl_verify_depth 4; # path for static files diff --git a/scripts/create_superuser.py b/scripts/create_superuser.py index 00a4a6a1..145e248f 100755 --- a/scripts/create_superuser.py +++ b/scripts/create_superuser.py @@ -27,6 +27,7 @@ UserModel = get_user_model() try: + username = os.environ["ADMIN_NAME"] admin_user = UserModel._default_manager.get(username="admin") admin_user.email = email admin_user.set_password(password) diff --git a/src/authentication/auth.py b/src/authentication/auth.py index a8a94069..294a5dc5 100644 --- a/src/authentication/auth.py +++ b/src/authentication/auth.py @@ -1,5 +1,5 @@ import logging - +import re import jwt from django.conf import settings from django.contrib.auth import get_user_model @@ -13,74 +13,42 @@ "rest_framework.authentication.TokenAuthentication" in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] ) -oauth_jwt_authentication_enabled = ( - "authentication.auth.OAuthJWTAuthentication" +certificate_authentication_enabled = ( + "authentication.auth.CertificateAuthentication" in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] ) -def jwt_request_has_required_role(request): - if request.auth: - if "authorities" in request.auth: - if request.auth["authorities"]: - authorities = request.auth["authorities"] - return settings.REQUIRED_ROLE.upper() in authorities - return False -class OAuthJWTAuthentication(authentication.BaseAuthentication): + +class CertificateAuthentication(authentication.BaseAuthentication): def authenticate(self, request): auth_header = get_authorization_header(request) if not auth_header: logger.debug("no auth header") return None - auth_header = auth_header.split() - if len(auth_header) != 2: - return None - if auth_header[0].decode().lower() != "bearer": - logger.debug("no JWT bearer token") - return None # attempt other configured authentication methods - token = auth_header[1] - # get JWT public key - public_key = "" - try: - with open(settings.PATH_TO_JWT_PUBLIC_KEY) as public_key_file: - public_key = public_key_file.read() - except Exception as e: - logger.error(e) - if not public_key: - error = exceptions.AuthenticationFailed( - "Unable to get public key to decode jwt" - ) - logger.error(error) - raise error - try: - # decode JWT token - # verifies jwt signature using RS256 algorithm and public key - # requires exp claim to verify token is not expired - # decodes and returns base64 encoded payload - decoded_key = jwt.decode( - token, - public_key, - verify=True, - algorithms="RS256", - options={"require": ["exp"], "verify_exp": True}, - ) - except ExpiredSignatureError as e: - logger.error(e) - raise exceptions.AuthenticationFailed("Token is expired!") - except InvalidSignatureError as e: - logger.error(e) - raise exceptions.AuthenticationFailed("Unable to verify token!") - except Exception as e: - logger.error(e) - raise exceptions.AuthenticationFailed(f"Unable to decode token! {e}") - jwt_username = decoded_key["user_name"] + cert_dn = request.headers.get("X-Ssl-Client-Dn") + logger.info("DN:" + cert_dn) + cn = get_cn_from_dn(cert_dn) + logger.info("Cert cn: " + cn) user_model = get_user_model() user = None try: - user = user_model.objects.get(username=jwt_username) + user = user_model.objects.get(username=cn) except user_model.DoesNotExist: - user = user_model.objects.create_user(username=jwt_username) + user = user_model.objects.create_user(username=cn) user.save() - return (user, decoded_key) + + return user + +def get_cn_from_dn(cert_dn): + p = re.compile("CN=(.*?)(?:,|\+|$)") + match = p.search(cert_dn) + if not match: + raise Exception("No CN found in certificate!") + uid_raw = match.group() + # logger.debug(f"uid_raw = {uid_raw}") + uid = uid_raw.split("=")[1].rstrip(",") + # logger.debug(f"uid = {uid}") + return uid diff --git a/src/scheduler/scheduler.py b/src/scheduler/scheduler.py index 2bfb1b18..63fab400 100644 --- a/src/scheduler/scheduler.py +++ b/src/scheduler/scheduler.py @@ -182,14 +182,15 @@ def _finalize_task_result(self, started, finished, status, detail): if settings.PATH_TO_VERIFY_CERT != "": verify_ssl = settings.PATH_TO_VERIFY_CERT logger.debug(settings.CALLBACK_AUTHENTICATION) - if settings.CALLBACK_AUTHENTICATION == "OAUTH": - client = oauth.get_oauth_client() + if settings.CALLBACK_AUTHENTICATION == "CERT": headers = {"Content-Type": "application/json"} - response = client.post( + + response = requests.post( self.entry.callback_url, data=json.dumps(result_json), headers=headers, - verify=verify_ssl + verify=verify_ssl, + cert=(settings.PATH_TO_CLIENT_CERT, settings.PATH_TO_CLIENT_KEY) ) self._callback_response_handler(response, tr) else: diff --git a/src/sensor/settings.py b/src/sensor/settings.py index 2ead2102..7da628eb 100644 --- a/src/sensor/settings.py +++ b/src/sensor/settings.py @@ -244,9 +244,9 @@ } AUTHENTICATION = env("AUTHENTICATION", default="") -if AUTHENTICATION == "JWT": +if AUTHENTICATION == "CERT": REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] = ( - "authentication.auth.OAuthJWTAuthentication", + "authentication.auth.CertificateAuthentication", "rest_framework.authentication.SessionAuthentication", ) else: From 5664774e5dd21dbf0d0706601a8ec5c3c5694ccd Mon Sep 17 00:00:00 2001 From: Doug Boulware Date: Fri, 29 Jul 2022 10:15:44 -0600 Subject: [PATCH 02/32] Use env variable to set admin user name. --- scripts/create_superuser.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/scripts/create_superuser.py b/scripts/create_superuser.py index 145e248f..dea05b30 100755 --- a/scripts/create_superuser.py +++ b/scripts/create_superuser.py @@ -25,13 +25,12 @@ sys.exit(0) UserModel = get_user_model() - +username = os.environ["ADMIN_NAME"] try: - username = os.environ["ADMIN_NAME"] - admin_user = UserModel._default_manager.get(username="admin") + admin_user = UserModel._default_manager.get(username=username) admin_user.email = email admin_user.set_password(password) print("Reset admin account password and email from environment") except UserModel.DoesNotExist: - UserModel._default_manager.create_superuser("admin", email, password) + UserModel._default_manager.create_superuser(username, email, password) print("Created admin account with password and email from environment") From c0e2574ce286cd476b89144c0fb121427ffe8c47 Mon Sep 17 00:00:00 2001 From: dboulware Date: Fri, 20 Jan 2023 09:59:32 -0700 Subject: [PATCH 03/32] Add ADMIN_NAME setting to be able to pass CN of admin user do db creation. Add setting for NGINX to set DN. --- docker-compose.yml | 1 + env.template | 3 ++- nginx/conf.template | 4 ++- src/authentication/auth.py | 41 ++++++++++++++----------------- src/authentication/permissions.py | 4 --- src/sensor/settings.py | 1 + 6 files changed, 25 insertions(+), 29 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4c1d7487..c885fe86 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,6 +31,7 @@ services: - DEBUG - DOCKER_GIT_CREDENTIALS environment: + - ADMIN_NAME - ADMIN_EMAIL - ADMIN_PASSWORD - AUTHENTICATION diff --git a/env.template b/env.template index 7ecd10fc..716fc42d 100644 --- a/env.template +++ b/env.template @@ -72,4 +72,5 @@ PATH_TO_VERIFY_CERT=scos_test_ca.crt # Path relative to configs/certs PATH_TO_JWT_PUBLIC_KEY=jwt_pubkey.pem # set to JWT to enable JWT authentication -AUTHENTICATION=TOKEN +AUTHENTICATION=CERT +ADMIN_NAME=Admin diff --git a/nginx/conf.template b/nginx/conf.template index ed438618..7b79bcac 100644 --- a/nginx/conf.template +++ b/nginx/conf.template @@ -32,7 +32,7 @@ server { ssl_certificate_key /etc/ssl/private/ssl-cert.key; ssl_protocols TLSv1.2; ssl_client_certificate /etc/ssl/certs/ca.crt; - ssl_verify_client on; + ssl_verify_client optional; # ssl_ocsp on; # Enable OCSP validation ssl_verify_depth 4; # path for static files @@ -50,8 +50,10 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; proxy_set_header Host $http_host; + proxy_set_header X-SSL-CLIENT-DN $ssl_client_s_dn; proxy_redirect off; proxy_pass http://wsgi-server; + } } diff --git a/src/authentication/auth.py b/src/authentication/auth.py index 294a5dc5..0bf2ebf6 100644 --- a/src/authentication/auth.py +++ b/src/authentication/auth.py @@ -3,44 +3,39 @@ import jwt from django.conf import settings from django.contrib.auth import get_user_model -from jwt import ExpiredSignatureError, InvalidSignatureError from rest_framework import authentication, exceptions from rest_framework.authentication import get_authorization_header logger = logging.getLogger(__name__) token_auth_enabled = ( - "rest_framework.authentication.TokenAuthentication" - in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] + "rest_framework.authentication.TokenAuthentication" + in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] ) certificate_authentication_enabled = ( - "authentication.auth.CertificateAuthentication" - in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] + "authentication.auth.CertificateAuthentication" + in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] ) - - - class CertificateAuthentication(authentication.BaseAuthentication): def authenticate(self, request): - auth_header = get_authorization_header(request) - if not auth_header: - logger.debug("no auth header") - return None + logger.debug("Authenticating certificate.") cert_dn = request.headers.get("X-Ssl-Client-Dn") - logger.info("DN:" + cert_dn) - cn = get_cn_from_dn(cert_dn) - logger.info("Cert cn: " + cn) - user_model = get_user_model() - user = None - try: - user = user_model.objects.get(username=cn) - except user_model.DoesNotExist: - user = user_model.objects.create_user(username=cn) - user.save() + if cert_dn: + logger.info("DN:" + cert_dn) + cn = get_cn_from_dn(cert_dn) + logger.info("Cert cn: " + cn) + user_model = get_user_model() + user = None + try: + user = user_model.objects.get(username=cn) + except user_model.DoesNotExist: + user = user_model.objects.create_user(username=cn) + user.save() + return user, None + return None, None - return user def get_cn_from_dn(cert_dn): p = re.compile("CN=(.*?)(?:,|\+|$)") diff --git a/src/authentication/permissions.py b/src/authentication/permissions.py index 40c55ec6..4a2beafa 100644 --- a/src/authentication/permissions.py +++ b/src/authentication/permissions.py @@ -1,14 +1,10 @@ from rest_framework import permissions -from .auth import jwt_request_has_required_role, oauth_jwt_authentication_enabled - class RequiredJWTRolePermissionOrIsSuperuser(permissions.BasePermission): message = "User missing required role" def has_permission(self, request, view): - if oauth_jwt_authentication_enabled and jwt_request_has_required_role(request): - return True if request.user.is_superuser: return True return False diff --git a/src/sensor/settings.py b/src/sensor/settings.py index 7da628eb..e3dfd1b5 100644 --- a/src/sensor/settings.py +++ b/src/sensor/settings.py @@ -246,6 +246,7 @@ AUTHENTICATION = env("AUTHENTICATION", default="") if AUTHENTICATION == "CERT": REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] = ( + "rest_framework.authentication.TokenAuthentication", "authentication.auth.CertificateAuthentication", "rest_framework.authentication.SessionAuthentication", ) From 04c1f55a09176798f21568e2dcbec008fa857d5a Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Mon, 23 Jan 2023 11:21:59 -0700 Subject: [PATCH 04/32] raise exception if user not found, remove token auth and session auth --- src/authentication/auth.py | 3 +-- src/sensor/settings.py | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/authentication/auth.py b/src/authentication/auth.py index 0bf2ebf6..e51c88ad 100644 --- a/src/authentication/auth.py +++ b/src/authentication/auth.py @@ -31,8 +31,7 @@ def authenticate(self, request): try: user = user_model.objects.get(username=cn) except user_model.DoesNotExist: - user = user_model.objects.create_user(username=cn) - user.save() + raise exceptions.AuthenticationFailed("No matching username found!") return user, None return None, None diff --git a/src/sensor/settings.py b/src/sensor/settings.py index e3dfd1b5..a1d16628 100644 --- a/src/sensor/settings.py +++ b/src/sensor/settings.py @@ -246,9 +246,7 @@ AUTHENTICATION = env("AUTHENTICATION", default="") if AUTHENTICATION == "CERT": REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] = ( - "rest_framework.authentication.TokenAuthentication", "authentication.auth.CertificateAuthentication", - "rest_framework.authentication.SessionAuthentication", ) else: REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] = ( From 16b3f05376fa3e4a7b3873a95f5109a857ed5555 Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Mon, 23 Jan 2023 12:15:41 -0700 Subject: [PATCH 05/32] cert auth in migration and runtime settings --- src/sensor/migration_settings.py | 5 ++--- src/sensor/runtime_settings.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/sensor/migration_settings.py b/src/sensor/migration_settings.py index 614f058f..954b044f 100644 --- a/src/sensor/migration_settings.py +++ b/src/sensor/migration_settings.py @@ -239,10 +239,9 @@ } AUTHENTICATION = env("AUTHENTICATION", default="") -if AUTHENTICATION == "JWT": +if AUTHENTICATION == "CERT": REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] = ( - "authentication.auth.OAuthJWTAuthentication", - "rest_framework.authentication.SessionAuthentication", + "authentication.auth.CertificateAuthentication", ) else: REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] = ( diff --git a/src/sensor/runtime_settings.py b/src/sensor/runtime_settings.py index 8fd66452..09df46df 100644 --- a/src/sensor/runtime_settings.py +++ b/src/sensor/runtime_settings.py @@ -239,10 +239,9 @@ } AUTHENTICATION = env("AUTHENTICATION", default="") -if AUTHENTICATION == "JWT": +if AUTHENTICATION == "CERT": REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] = ( - "authentication.auth.OAuthJWTAuthentication", - "rest_framework.authentication.SessionAuthentication", + "authentication.auth.CertificateAuthentication", ) else: REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] = ( From 9cb4c80d9e863b0f317ae0ed261f9f5c88176195 Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Mon, 23 Jan 2023 12:35:36 -0700 Subject: [PATCH 06/32] update requirements --- src/requirements-dev.in | 2 +- src/requirements-dev.txt | 54 ++++++++++++++++------------------------ src/requirements.in | 2 -- src/requirements.txt | 39 +++++++++++------------------ 4 files changed, 38 insertions(+), 59 deletions(-) diff --git a/src/requirements-dev.in b/src/requirements-dev.in index 8a629ed0..82b5b2c5 100644 --- a/src/requirements-dev.in +++ b/src/requirements-dev.in @@ -1,6 +1,6 @@ -rrequirements.txt -cryptography>=36.0, <39.0 +cryptography>=38.0.3 numpy>=1.0, <2.0 pre-commit>=2.0, <3.0 pytest-cov>=3.0, <4.0 diff --git a/src/requirements-dev.txt b/src/requirements-dev.txt index fb7dbf2b..e11303cb 100644 --- a/src/requirements-dev.txt +++ b/src/requirements-dev.txt @@ -33,7 +33,7 @@ cffi==1.15.1 # pynacl cfgv==3.3.1 # via pre-commit -charset-normalizer==2.1.1 +charset-normalizer==3.0.1 # via # -r requirements.txt # requests @@ -50,9 +50,9 @@ coreschema==0.0.4 # -r requirements.txt # coreapi # drf-yasg -coverage[toml]==7.0.0 +coverage[toml]==7.0.5 # via pytest-cov -cryptography==38.0.1 +cryptography==39.0.0 # via # -r requirements-dev.in # -r requirements.txt @@ -104,9 +104,9 @@ environs==9.5.0 # -r requirements.txt # scos-actions # scos-tekrsa -exceptiongroup==1.0.4 +exceptiongroup==1.1.0 # via pytest -filelock==3.8.2 +filelock==3.9.0 # via # -r requirements.txt # ray @@ -123,7 +123,7 @@ grpcio==1.51.1 # ray gunicorn==20.1.0 # via -r requirements.txt -identify==2.5.11 +identify==2.5.15 # via pre-commit idna==3.4 # via @@ -133,7 +133,7 @@ inflection==0.5.1 # via # -r requirements.txt # drf-yasg -iniconfig==1.1.1 +iniconfig==2.0.0 # via pytest its-preselector @ git+https://github.com/NTIA/Preselector@3.0.0 # via @@ -154,7 +154,7 @@ jsonschema==3.2.0 # -r requirements.txt # docker-compose # ray -markupsafe==2.1.1 +markupsafe==2.1.2 # via # -r requirements.txt # jinja2 @@ -172,7 +172,7 @@ numexpr==2.8.4 # via # -r requirements.txt # scos-actions -numpy==1.24.0 +numpy==1.24.1 # via # -r requirements-dev.in # -r requirements.txt @@ -182,11 +182,7 @@ numpy==1.24.0 # scos-actions # sigmf # tekrsa-api-wrap -oauthlib==3.2.2 - # via - # -r requirements.txt - # requests-oauthlib -packaging==22.0 +packaging==23.0 # via # -r requirements.txt # docker @@ -194,11 +190,11 @@ packaging==22.0 # marshmallow # pytest # tox -paramiko==2.12.0 +paramiko==3.0.0 # via # -r requirements.txt # docker -platformdirs==2.6.0 +platformdirs==2.6.2 # via # -r requirements.txt # virtualenv @@ -206,7 +202,7 @@ pluggy==1.0.0 # via # pytest # tox -pre-commit==2.20.0 +pre-commit==2.21.0 # via -r requirements-dev.in protobuf==4.21.12 # via @@ -230,11 +226,11 @@ pynacl==1.5.0 # via # -r requirements.txt # paramiko -pyrsistent==0.19.2 +pyrsistent==0.19.3 # via # -r requirements.txt # jsonschema -pytest==7.2.0 +pytest==7.2.1 # via # pytest-cov # pytest-django @@ -246,12 +242,12 @@ python-dateutil==2.8.2 # via # -r requirements.txt # scos-actions -python-dotenv==0.21.0 +python-dotenv==0.21.1 # via # -r requirements.txt # docker-compose # environs -pytz==2022.7 +pytz==2022.7.1 # via # -r requirements.txt # django @@ -267,7 +263,7 @@ ray==2.2.0 # via # -r requirements.txt # scos-actions -requests==2.28.1 +requests==2.28.2 # via # -r requirements.txt # coreapi @@ -276,11 +272,8 @@ requests==2.28.1 # its-preselector # ray # requests-mock - # requests-oauthlib requests-mock==1.10.0 # via -r requirements.txt -requests-oauthlib==1.3.1 - # via -r requirements.txt ruamel-yaml==0.17.21 # via # -r requirements.txt @@ -288,9 +281,9 @@ ruamel-yaml==0.17.21 # scos-actions ruamel-yaml-clib==0.2.7 # via - # -r requirements.txt - # ruamel-yaml -scipy==1.9.3 + # -r requirements.txt + # ruamel-yaml +scipy==1.10.0 # via # -r requirements.txt # scos-actions @@ -310,7 +303,6 @@ six==1.16.0 # django-session-timeout # dockerpty # jsonschema - # paramiko # python-dateutil # requests-mock # sigmf @@ -328,8 +320,6 @@ texttable==1.6.7 # via # -r requirements.txt # docker-compose -toml==0.10.2 - # via pre-commit tomli==2.0.1 # via # coverage @@ -342,7 +332,7 @@ uritemplate==4.1.1 # -r requirements.txt # coreapi # drf-yasg -urllib3==1.26.13 +urllib3==1.26.14 # via # -r requirements.txt # docker diff --git a/src/requirements.in b/src/requirements.in index ca5e874a..c03ebab4 100644 --- a/src/requirements.in +++ b/src/requirements.in @@ -6,11 +6,9 @@ drf-yasg>=1.0, <2.0 environs>=9.0, <10.0 gunicorn>=20.0, <21.0 jsonfield>=3.0, <4.0 -oauthlib>=3.2.1, <4.0 psycopg2-binary>=2.0, <3.0 pyjwt>=2.4.0, <3.0 requests-mock>=1.0, <2.0 -requests_oauthlib>=1.0, <2.0 scos_actions @ git+https://github.com/NTIA/scos-actions@6.0.1 #scos_usrp @ git+https://github.com/NTIA/scos-usrp@3.0.0 scos_tekrsa @ git+https://github.com/NTIA/scos-tekrsa@3.0.0 diff --git a/src/requirements.txt b/src/requirements.txt index c6bef082..812e4421 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -20,7 +20,7 @@ cffi==1.15.1 # via # cryptography # pynacl -charset-normalizer==2.1.1 +charset-normalizer==3.0.1 # via requests click==8.1.3 # via ray @@ -30,7 +30,7 @@ coreschema==0.0.4 # via # coreapi # drf-yasg -cryptography==38.0.1 +cryptography==39.0.0 # via paramiko defusedxml==0.7.1 # via its-preselector @@ -67,7 +67,7 @@ environs==9.5.0 # -r requirements.in # scos-actions # scos-tekrsa -filelock==3.8.2 +filelock==3.9.0 # via # ray # virtualenv @@ -95,7 +95,7 @@ jsonschema==3.2.0 # via # docker-compose # ray -markupsafe==2.1.1 +markupsafe==2.1.2 # via jinja2 marshmallow==3.19.0 # via environs @@ -103,7 +103,7 @@ msgpack==1.0.4 # via ray numexpr==2.8.4 # via scos-actions -numpy==1.24.0 +numpy==1.24.1 # via # numexpr # ray @@ -111,18 +111,14 @@ numpy==1.24.0 # scos-actions # sigmf # tekrsa-api-wrap -oauthlib==3.2.2 - # via - # -r requirements.in - # requests-oauthlib -packaging==22.0 +packaging==23.0 # via # docker # drf-yasg # marshmallow -paramiko==2.12.0 +paramiko==3.0.0 # via docker -platformdirs==2.6.0 +platformdirs==2.6.2 # via virtualenv protobuf==4.21.12 # via ray @@ -136,15 +132,15 @@ pyjwt==2.6.0 # via -r requirements.in pynacl==1.5.0 # via paramiko -pyrsistent==0.19.2 +pyrsistent==0.19.3 # via jsonschema python-dateutil==2.8.2 # via scos-actions -python-dotenv==0.21.0 +python-dotenv==0.21.1 # via # docker-compose # environs -pytz==2022.7 +pytz==2022.7.1 # via # django # djangorestframework @@ -155,7 +151,7 @@ pyyaml==5.4.1 # ray ray==2.2.0 # via scos-actions -requests==2.28.1 +requests==2.28.2 # via # coreapi # docker @@ -163,19 +159,15 @@ requests==2.28.1 # its-preselector # ray # requests-mock - # requests-oauthlib requests-mock==1.10.0 # via -r requirements.in -requests-oauthlib==1.3.1 - # via -r requirements.in ruamel-yaml==0.17.21 # via # drf-yasg # scos-actions ruamel-yaml-clib==0.2.7 - # via - # ruamel-yaml -scipy==1.9.3 + # via ruamel-yaml +scipy==1.10.0 # via scos-actions scos_actions @ git+https://github.com/NTIA/scos-actions@6.0.1 # via @@ -190,7 +182,6 @@ six==1.16.0 # django-session-timeout # dockerpty # jsonschema - # paramiko # python-dateutil # requests-mock # sigmf @@ -205,7 +196,7 @@ uritemplate==4.1.1 # via # coreapi # drf-yasg -urllib3==1.26.13 +urllib3==1.26.14 # via # docker # requests From ba037ab28b1e784b4ada8ca28c0b0c453ef7b5de Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Mon, 23 Jan 2023 13:13:14 -0700 Subject: [PATCH 07/32] remove pyjwt from requirements --- src/requirements-dev.txt | 2 -- src/requirements.in | 1 - src/requirements.txt | 2 -- 3 files changed, 5 deletions(-) diff --git a/src/requirements-dev.txt b/src/requirements-dev.txt index e11303cb..18724358 100644 --- a/src/requirements-dev.txt +++ b/src/requirements-dev.txt @@ -220,8 +220,6 @@ pycparser==2.21 # via # -r requirements.txt # cffi -pyjwt==2.6.0 - # via -r requirements.txt pynacl==1.5.0 # via # -r requirements.txt diff --git a/src/requirements.in b/src/requirements.in index c03ebab4..46d75fba 100644 --- a/src/requirements.in +++ b/src/requirements.in @@ -7,7 +7,6 @@ environs>=9.0, <10.0 gunicorn>=20.0, <21.0 jsonfield>=3.0, <4.0 psycopg2-binary>=2.0, <3.0 -pyjwt>=2.4.0, <3.0 requests-mock>=1.0, <2.0 scos_actions @ git+https://github.com/NTIA/scos-actions@6.0.1 #scos_usrp @ git+https://github.com/NTIA/scos-usrp@3.0.0 diff --git a/src/requirements.txt b/src/requirements.txt index 812e4421..8e3f6b58 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -128,8 +128,6 @@ psycopg2-binary==2.9.5 # via -r requirements.in pycparser==2.21 # via cffi -pyjwt==2.6.0 - # via -r requirements.in pynacl==1.5.0 # via paramiko pyrsistent==0.19.3 From a7d529155941fc36183c1565e495a35706578be3 Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Mon, 23 Jan 2023 13:28:57 -0700 Subject: [PATCH 08/32] update readme --- README.md | 102 ++++++++++------------------------------ configs/certs/README.md | 2 +- 2 files changed, 27 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index a8550edc..217e3d7f 100644 --- a/README.md +++ b/README.md @@ -371,38 +371,31 @@ authenticating when using a callback URL. ### Sensor Authentication And Permissions -The sensor can be configured to authenticate using OAuth JWT access tokens from an -external authorization server or using Django Rest Framework Token Authentication. +The sensor can be configured to authenticate using mutual TLS with client certificates +or using Django Rest Framework Token Authentication. #### Django Rest Framework Token Authentication This is the default authentication method. To enable Django Rest Framework Authentication, make sure `AUTHENTICATION` is set to `TOKEN` in the environment file (this will be enabled if `AUTHENTICATION` set to anything other -than `JWT`). +than `CERT`). A token is automatically created for each user. Django Rest Framework Token Authentication will check that the token in the Authorization header ("Token " + token) matches a user's token. -#### OAuth2 JWT Authentication +#### Certificate Authentication -To enable OAuth 2 JWT Authentication, set `AUTHENTICATION` to `JWT` in the environment -file. To authenticate, the client will need to send a JWT access token in the -authorization header (using "Bearer " + access token). The token signature will be -verified using the public key from the `PATH_TO_JWT_PUBLIC_KEY` setting. The expiration -time will be checked. Only users who have an authority matching the `REQUIRED_ROLE` -setting will be authorized. - -The token is expected to come from an OAuth2 authorization server. For more -information, see . +To enable Certificate Authentication, set `AUTHENTICATION` to `CERT` in the environment +file. To authenticate, the client will need to send a trusted client certificate. The +Common Name must match the username of a user in the database. #### Certificates Use this section to create self-signed certificates with customized organizational and host information. This section includes instructions for creating a self-signed -root CA, SSL server certificates for the sensor, optional client certificates, and test -JWT public/private key pair. +root CA, SSL server certificates for the sensor, and optional client certificates. As described below, a self-signed CA can be created for testing. **For production, make sure to use certificates from a trusted CA.** @@ -501,36 +494,11 @@ openssl pkcs12 -export -out client.pfx -inkey client.key -in client.pem -certfil Import client.pfx into web browser for use with the browsable API or use the client.pem or client.pfx when communicating with the API programmatically. -##### Generating JWT Public/Private Key - -The JWT public key must correspond to the private key of the JWT issuer (OAuth -authorization server). For manual testing, the instructions below could be used to -create a public/private key pair for creating JWTs without an authorization -server. - -###### Step 1: Create public/private key pair - -```bash -openssl genrsa -out jwt.pem 4096 -``` - -###### Step 2: Extract Public Key - -```bash -openssl rsa -in jwt.pem -outform PEM -pubout -out jwt_public_key.pem -``` - -###### Step 3: Extract Private Key - -```bash -openssl pkey -inform PEM -outform PEM -in jwt.pem -out jwt_private_key.pem -``` - ###### Configure scos-sensor The Nginx web server can be set to require client certificates (mutual TLS). This can -optionally be enabled. To require client certificates, uncomment -`ssl_verify_client on;` in the [Nginx configuration file](nginx/conf.template). If you +optionally be enabled. To require client certificates, make sure `ssl_verify_client` is +set to `on` in the [Nginx configuration file](nginx/conf.template). If you use OCSP, also uncomment `ssl_ocsp on;`. Additional configuration may be needed for Nginx to check certificate revocation lists (CRL). @@ -542,16 +510,10 @@ environment file) to the path of the sensor01_combined.pem relative to configs/c mutual TLS, also copy the CA certificate to the same directory. Then, set `SSL_CA_PATH` to the path of the CA certificate relative to `configs/certs`. -If you are using JWT authentication, set `PATH_TO_JWT_PUBLIC_KEY` to the path of the -JWT public key relative to configs/certs. This public key file should correspond to the -private key used to sign the JWT. Alternatively, the JWT private key -created above could be used to manually sign a JWT token for testing if -`PATH_TO_JWT_PUBLIC_KEY` is set to the JWT public key created above. - If you are using client certificates, use client.pfx to connect to the browsable API by importing this certificate into your browser. -For callback functionality with an OAuth authorized callback URL, set +For callback functionality with a server that uses certificate authentication, set `PATH_TO_CLIENT_CERT` and `PATH_TO_VERIFY_CERT`, both relative to configs/certs. Depending on the configuration of the callback URL server and the authorization server, the sensor server certificate could be used as a client certificate by setting @@ -562,18 +524,15 @@ as used for `SSL_CA_PATH` (scostestca.pem). #### Permissions and Users -The API requires the user to either have an authority in the JWT token matching the the -`REQUIRED_ROLE` setting or that the user be a superuser. New users created using the +The API requires the user to be a superuser. New users created using the API initially do not have superuser access. However, an admin can mark a user as a -superuser in the Sensor Configuration Portal. When using JWT tokens, the user does not -have to be pre-created using the sensor's API. The API will accept any user using a -JWT token if they have an authority matching the required role setting. +superuser in the Sensor Configuration Portal. ### Callback URL Authentication -OAuth and Token authentication are supported for authenticating against the server -pointed to by the callback URL. Callback SSL verification can be enabled -or disabled using `CALLBACK_SSL_VERIFICATION` in the environment file. +Certificate and token authentication are supported for authenticating against the +server pointed to by the callback URL. Callback SSL verification can be enabled or +disabled using `CALLBACK_SSL_VERIFICATION` in the environment file. #### Token @@ -590,29 +549,20 @@ verify the callback URL server SSL certificate. If this is unset and https://requests.readthedocs.io/en/master/user/advanced/#ca-certificates) will be used. -#### OAuth +#### Certificate -The OAuth 2 password flow is supported for callback URL authentication. The following -settings in the environment file are used to configure the OAuth 2 password flow -authentication. +Certificate authetnication (mutual TLS) is supported for callback URL authentication. +The following settings in the environment file are used to configure certificate +authentication for the callback URL. -- `CALLBACK_AUTHENTICATION` - set to `OAUTH`. -- `CLIENT_ID` - client ID used to authorize the client (the sensor) against the - authorization server. -- `CLIENT_SECRET` - client secret used to authorize the client (the sensor) against the - authorization server. -- `OAUTH_TOKEN_URL` - URL to get the access token. +- `CALLBACK_AUTHENTICATION` - set to `CERT`. - `PATH_TO_CLIENT_CERT` - client certificate used to authenticate against the - authorization server. -- `PATH_TO_VERIFY_CERT` - CA certificate to verify the authorization server and - callback URL server SSL certificate. If this is unset and `CALLBACK_SSL_VERIFICATION` - is set to true, [standard trusted CAs]( + callback URL server. +- `PATH_TO_VERIFY_CERT` - CA certificate to verify the callback URL server SSL + certificate. If this is unset and `CALLBACK_SSL_VERIFICATION` + is set to true, [standard trusted CAs]( https://requests.readthedocs.io/en/master/user/advanced/#ca-certificates) will be - used. - -In src/sensor/settings.py, the OAuth `USER_NAME` and `PASSWORD` are set to be the same -as `CLIENT_ID` and `CLIENT_SECRET`. This may need to change depending on your -authorization server. + used. ## Actions and Hardware Support diff --git a/configs/certs/README.md b/configs/certs/README.md index 2b9c6951..85b06b8f 100644 --- a/configs/certs/README.md +++ b/configs/certs/README.md @@ -1,3 +1,3 @@ # Certs -Add SSL certs and JWT public key here. +Add SSL certs here. From 8b9d2532564e0a00455fbc70b68b79e9194d86ae Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Tue, 24 Jan 2023 15:28:24 -0700 Subject: [PATCH 09/32] remove jwt/oauth, change permission to IsSuperuser --- docker-compose.yml | 2 -- env.template | 6 ++-- src/authentication/oauth.py | 54 ------------------------------- src/authentication/permissions.py | 4 +-- src/scheduler/scheduler.py | 1 - src/sensor/migration_settings.py | 9 ++---- src/sensor/runtime_settings.py | 8 +---- src/sensor/settings.py | 8 +---- 8 files changed, 8 insertions(+), 84 deletions(-) delete mode 100644 src/authentication/oauth.py diff --git a/docker-compose.yml b/docker-compose.yml index 0f0ff682..7b061cc3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -51,9 +51,7 @@ services: - MAX_DISK_USAGE - MOCK_SIGAN - MOCK_SIGAN_RANDOM - - OAUTH_TOKEN_URL - PATH_TO_CLIENT_CERT - - PATH_TO_JWT_PUBLIC_KEY - PATH_TO_VERIFY_CERT - POSTGRES_PASSWORD - SECRET_KEY diff --git a/env.template b/env.template index 3a34ca90..7550cc82 100644 --- a/env.template +++ b/env.template @@ -58,19 +58,17 @@ MANAGER_IP="$(hostname -I | cut -d' ' -f1)" BASE_IMAGE=ghcr.io/ntia/scos-tekrsa/tekrsa_usb:0.2.1 # Default callback api/results -# Set to OAUTH if using OAuth Password Flow Authentication, callback url needs to be api/v2/results +# Set to CERT for certificate authentication CALLBACK_AUTHENTICATION=TOKEN CLIENT_ID=sensor01.sms.internal CLIENT_SECRET=sensor-secret -OAUTH_TOKEN_URL=https://scosmgrqa01.sms.internal:443/authserver/oauth/token # Sensor certificate with private key used as client cert PATH_TO_CLIENT_CERT=sensor01.pem # Trusted Certificate Authority certificate to verify authserver and callback URL server certificate PATH_TO_VERIFY_CERT=scos_test_ca.crt # Path relative to configs/certs -PATH_TO_JWT_PUBLIC_KEY=jwt_pubkey.pem -# set to JWT to enable JWT authentication +# set to CERT to enable certificate authentication AUTHENTICATION=CERT ADMIN_NAME=Admin diff --git a/src/authentication/oauth.py b/src/authentication/oauth.py deleted file mode 100644 index 23f47f8f..00000000 --- a/src/authentication/oauth.py +++ /dev/null @@ -1,54 +0,0 @@ -import logging - -from django.conf import settings -from oauthlib.oauth2 import LegacyApplicationClient -from requests_oauthlib import OAuth2Session - -logger = logging.getLogger(__name__) - - -def get_oauth_token(): - """Returns OAuth access token.""" - try: - logger.debug(settings.CLIENT_ID) - logger.debug(settings.CLIENT_SECRET) - logger.debug(settings.USER_NAME) - logger.debug(settings.PASSWORD) - - logger.debug(settings.OAUTH_TOKEN_URL) - logger.debug(settings.PATH_TO_CLIENT_CERT) - logger.debug(settings.PATH_TO_VERIFY_CERT) - verify_ssl = settings.CALLBACK_SSL_VERIFICATION - if settings.CALLBACK_SSL_VERIFICATION: - if settings.PATH_TO_VERIFY_CERT != "": - verify_ssl = settings.PATH_TO_VERIFY_CERT - - logger.debug(verify_ssl) - oauth = OAuth2Session( - client=LegacyApplicationClient(client_id=settings.CLIENT_ID) - ) - oauth.cert = settings.PATH_TO_CLIENT_CERT - token = oauth.fetch_token( - token_url=settings.OAUTH_TOKEN_URL, - username=settings.USER_NAME, - password=settings.PASSWORD, - client_id=settings.CLIENT_ID, - client_secret=settings.CLIENT_SECRET, - verify=verify_ssl, - ) - oauth.close() - logger.debug("Response from oauth.fetch_token: " + str(token)) - return token - except Exception: - raise - - -def get_oauth_client(): - """Returns Authorized OAuth Client (with authentication header token).""" - try: - token = get_oauth_token() - client = OAuth2Session(settings.CLIENT_ID, token=token) - client.cert = settings.PATH_TO_CLIENT_CERT - return client - except Exception: - raise diff --git a/src/authentication/permissions.py b/src/authentication/permissions.py index 4a2beafa..42a3f14c 100644 --- a/src/authentication/permissions.py +++ b/src/authentication/permissions.py @@ -1,8 +1,8 @@ from rest_framework import permissions -class RequiredJWTRolePermissionOrIsSuperuser(permissions.BasePermission): - message = "User missing required role" +class IsSuperuser(permissions.BasePermission): + message = "User is not superuser" def has_permission(self, request, view): if request.user.is_superuser: diff --git a/src/scheduler/scheduler.py b/src/scheduler/scheduler.py index 6a72f935..346845f7 100644 --- a/src/scheduler/scheduler.py +++ b/src/scheduler/scheduler.py @@ -9,7 +9,6 @@ import requests from django.utils import timezone -from authentication import oauth from schedule.models import ScheduleEntry from sensor import settings from tasks.consts import MAX_DETAIL_LEN diff --git a/src/sensor/migration_settings.py b/src/sensor/migration_settings.py index 954b044f..d967b3aa 100644 --- a/src/sensor/migration_settings.py +++ b/src/sensor/migration_settings.py @@ -221,7 +221,7 @@ ), "DEFAULT_PERMISSION_CLASSES": ( "rest_framework.permissions.IsAuthenticated", - "authentication.permissions.RequiredJWTRolePermissionOrIsSuperuser", + "authentication.permissions.IsSuperuser", ), "DEFAULT_RENDERER_CLASSES": ( "rest_framework.renderers.JSONRenderer", @@ -389,12 +389,7 @@ PATH_TO_VERIFY_CERT = env("PATH_TO_VERIFY_CERT", default="") if PATH_TO_VERIFY_CERT != "": PATH_TO_VERIFY_CERT = path.join(CERTS_DIR, PATH_TO_VERIFY_CERT) -# Public key to verify JWT token -PATH_TO_JWT_PUBLIC_KEY = env.str("PATH_TO_JWT_PUBLIC_KEY", default="") -if PATH_TO_JWT_PUBLIC_KEY != "": - PATH_TO_JWT_PUBLIC_KEY = path.join(CERTS_DIR, PATH_TO_JWT_PUBLIC_KEY) -# Required role from JWT token to access API -REQUIRED_ROLE = "ROLE_MANAGER" + PRESELECTOR_CONFIG = env.str( "PRESELECTOR_CONFIG", default=path.join(CONFIG_DIR, "preselector_config.json") diff --git a/src/sensor/runtime_settings.py b/src/sensor/runtime_settings.py index 09df46df..dd5fe7a5 100644 --- a/src/sensor/runtime_settings.py +++ b/src/sensor/runtime_settings.py @@ -221,7 +221,7 @@ ), "DEFAULT_PERMISSION_CLASSES": ( "rest_framework.permissions.IsAuthenticated", - "authentication.permissions.RequiredJWTRolePermissionOrIsSuperuser", + "authentication.permissions.IsSuperuser", ), "DEFAULT_RENDERER_CLASSES": ( "rest_framework.renderers.JSONRenderer", @@ -389,12 +389,6 @@ PATH_TO_VERIFY_CERT = env("PATH_TO_VERIFY_CERT", default="") if PATH_TO_VERIFY_CERT != "": PATH_TO_VERIFY_CERT = path.join(CERTS_DIR, PATH_TO_VERIFY_CERT) -# Public key to verify JWT token -PATH_TO_JWT_PUBLIC_KEY = env.str("PATH_TO_JWT_PUBLIC_KEY", default="") -if PATH_TO_JWT_PUBLIC_KEY != "": - PATH_TO_JWT_PUBLIC_KEY = path.join(CERTS_DIR, PATH_TO_JWT_PUBLIC_KEY) -# Required role from JWT token to access API -REQUIRED_ROLE = "ROLE_MANAGER" PRESELECTOR_CONFIG = env.str( "PRESELECTOR_CONFIG", default=path.join(CONFIG_DIR, "preselector_config.json") diff --git a/src/sensor/settings.py b/src/sensor/settings.py index ee080e25..f0969beb 100644 --- a/src/sensor/settings.py +++ b/src/sensor/settings.py @@ -222,7 +222,7 @@ ), "DEFAULT_PERMISSION_CLASSES": ( "rest_framework.permissions.IsAuthenticated", - "authentication.permissions.RequiredJWTRolePermissionOrIsSuperuser", + "authentication.permissions.IsSuperuser", ), "DEFAULT_RENDERER_CLASSES": ( "rest_framework.renderers.JSONRenderer", @@ -390,12 +390,6 @@ PATH_TO_VERIFY_CERT = env("PATH_TO_VERIFY_CERT", default="") if PATH_TO_VERIFY_CERT != "": PATH_TO_VERIFY_CERT = path.join(CERTS_DIR, PATH_TO_VERIFY_CERT) -# Public key to verify JWT token -PATH_TO_JWT_PUBLIC_KEY = env.str("PATH_TO_JWT_PUBLIC_KEY", default="") -if PATH_TO_JWT_PUBLIC_KEY != "": - PATH_TO_JWT_PUBLIC_KEY = path.join(CERTS_DIR, PATH_TO_JWT_PUBLIC_KEY) -# Required role from JWT token to access API -REQUIRED_ROLE = "ROLE_MANAGER" PRESELECTOR_CONFIG = env.str( "PRESELECTOR_CONFIG", default=path.join(CONFIG_DIR, "preselector_config.json") From dfc673d315b38c2f1020ea4d3658f033c3c5033e Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Tue, 24 Jan 2023 15:59:24 -0700 Subject: [PATCH 10/32] fix tests for cert auth, remove oauth variables from settings, fix scheduler cert auth callback --- src/authentication/auth.py | 2 - .../tests/jwt_content_example.json | 35 -- src/authentication/tests/test_cert_auth.py | 342 ++++++++++++++ src/authentication/tests/test_jwt_auth.py | 432 ------------------ src/authentication/tests/utils.py | 21 - src/conftest.py | 30 +- src/scheduler/scheduler.py | 2 +- src/scheduler/tests/test_scheduler.py | 41 +- src/sensor/migration_settings.py | 5 - src/sensor/runtime_settings.py | 5 - src/sensor/settings.py | 5 - src/sensor/tests/certificate_auth_client.py | 52 +++ src/sensor/tests/utils.py | 10 + src/tasks/tests/test_list_view.py | 2 +- src/tox.ini | 9 +- 15 files changed, 434 insertions(+), 559 deletions(-) delete mode 100644 src/authentication/tests/jwt_content_example.json create mode 100644 src/authentication/tests/test_cert_auth.py delete mode 100644 src/authentication/tests/test_jwt_auth.py delete mode 100644 src/authentication/tests/utils.py create mode 100644 src/sensor/tests/certificate_auth_client.py diff --git a/src/authentication/auth.py b/src/authentication/auth.py index e51c88ad..1cdcd0b7 100644 --- a/src/authentication/auth.py +++ b/src/authentication/auth.py @@ -1,10 +1,8 @@ import logging import re -import jwt from django.conf import settings from django.contrib.auth import get_user_model from rest_framework import authentication, exceptions -from rest_framework.authentication import get_authorization_header logger = logging.getLogger(__name__) diff --git a/src/authentication/tests/jwt_content_example.json b/src/authentication/tests/jwt_content_example.json deleted file mode 100644 index 5e5f2b4f..00000000 --- a/src/authentication/tests/jwt_content_example.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "user_name": "sensor01", - "scope": [ - "read", - "write" - ], - "exp": 1601494344, - "userDetails": { - "id": null, - "uid": "", - "altSecurityIdenties": null, - "email": "sensor01", - "firstname": "sensor01", - "lastname": "", - "cn": "sensor01", - "lastlogin": 1583360759668, - "enabled": true, - "authorities": [ - { - "authority": "ROLE_MANAGER" - } - ], - "password": null, - "username": "sensor01", - "dn": "cn=sensor01,ou=OU1,ou=OU2,ou=OU3,dc=DC1,dc=DC2", - "accountNonLocked": true, - "accountnullxpired": true, - "credentialsnullxpired": true - }, - "authorities": [ - "ROLE_MANAGER" - ], - "jti": "e4271916-bfe0-4028-b372-dc05c4882c88", - "client_id": "sensor01" -} diff --git a/src/authentication/tests/test_cert_auth.py b/src/authentication/tests/test_cert_auth.py new file mode 100644 index 00000000..e8b489ec --- /dev/null +++ b/src/authentication/tests/test_cert_auth.py @@ -0,0 +1,342 @@ +import base64 +import json +import os +import secrets +from datetime import datetime, timedelta +from tempfile import NamedTemporaryFile + +import pytest +from rest_framework.reverse import reverse +from rest_framework.test import RequestsClient + +from authentication.auth import certificate_authentication_enabled +from authentication.models import User +from sensor import V1 +from sensor.tests.utils import get_requests_ssl_dn_header + +pytestmark = pytest.mark.skipif( + not certificate_authentication_enabled, + reason="Certificate authentication is not enabled!", +) + + + + +one_min = timedelta(minutes=1) +one_day = timedelta(days=1) + + + + + +@pytest.mark.django_db +def test_no_client_cert_unauthorized(live_server): + client = RequestsClient() + response = client.get(f"{live_server.url}") + assert response.status_code == 403 + + + +@pytest.mark.django_db +def test_client_cert_accepted(live_server, admin_user): + client = RequestsClient() + response = client.get(f"{live_server.url}", headers=get_requests_ssl_dn_header("admin")) + assert response.status_code == 200 + + +def test_bad_client_cert_forbidden(live_server, admin_user): + client = RequestsClient() + response = client.get(f"{live_server.url}", headers=get_requests_ssl_dn_header("user")) + assert response.status_code == 403 + assert "No matching username found!" in response.json()["detail"] + + +# @pytest.mark.django_db +# def test_certificate_expired_1_day_forbidden(live_server): +# current_datetime = datetime.now() +# token_payload = get_token_payload(exp=(current_datetime - one_day).timestamp()) +# encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") +# client = RequestsClient() +# response = client.get(f"{live_server.url}", headers=get_headers(encoded)) +# assert response.status_code == 403 +# assert response.json()["detail"] == "Token is expired!" + + +# @pytest.mark.django_db +# def test_bad_private_key_forbidden(live_server): +# token_payload = get_token_payload() +# encoded = jwt.encode( +# token_payload, str(BAD_PRIVATE_KEY.decode("utf-8")), algorithm="RS256" +# ) +# client = RequestsClient() +# response = client.get(f"{live_server.url}", headers=get_headers(encoded)) +# assert response.status_code == 403 +# assert response.json()["detail"] == "Unable to verify token!" + + +# @pytest.mark.django_db +# def test_bad_public_key_forbidden(settings, live_server): +# with NamedTemporaryFile() as jwt_public_key_file: +# jwt_public_key_file.write(BAD_PUBLIC_KEY) +# jwt_public_key_file.flush() +# settings.PATH_TO_JWT_PUBLIC_KEY = jwt_public_key_file.name +# token_payload = get_token_payload() +# encoded = jwt.encode( +# token_payload, str(jwt_keys.private_key), algorithm="RS256" +# ) +# client = RequestsClient() +# response = client.get(f"{live_server.url}", headers=get_headers(encoded)) +# assert response.status_code == 403 +# assert response.json()["detail"] == "Unable to verify token!" + + +# @pytest.mark.django_db +# def test_certificate_expired_1_min_forbidden(live_server): +# current_datetime = datetime.now() +# token_payload = get_token_payload(exp=(current_datetime - one_min).timestamp()) +# encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") +# client = RequestsClient() +# response = client.get(f"{live_server.url}", headers=get_headers(encoded)) +# assert response.status_code == 403 +# assert response.json()["detail"] == "Token is expired!" + + +# @pytest.mark.django_db +# def test_certificate_expires_in_1_min_accepted(live_server): +# current_datetime = datetime.now() +# token_payload = get_token_payload(exp=(current_datetime + one_min).timestamp()) +# encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") +# client = RequestsClient() +# response = client.get(f"{live_server.url}", headers=get_headers(encoded)) +# assert response.status_code == 200 + + +# @pytest.mark.django_db +# def test_urls_unauthorized(live_server): +# token_payload = get_token_payload(authorities=["ROLE_USER"]) +# encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") +# client = RequestsClient() +# headers = get_headers(encoded) + +# capabilities = reverse("capabilities", kwargs=V1) +# response = client.get(f"{live_server.url}{capabilities}", headers=headers) +# assert response.status_code == 403 + +# schedule_list = reverse("schedule-list", kwargs=V1) +# response = client.get(f"{live_server.url}{schedule_list}", headers=headers) +# assert response.status_code == 403 + +# status = reverse("status", kwargs=V1) +# response = client.get(f"{live_server.url}{status}", headers=headers) +# assert response.status_code == 403 + +# task_root = reverse("task-root", kwargs=V1) +# response = client.get(f"{live_server.url}{task_root}", headers=headers) +# assert response.status_code == 403 + +# task_results_overview = reverse("task-results-overview", kwargs=V1) +# response = client.get(f"{live_server.url}{task_results_overview}", headers=headers) +# assert response.status_code == 403 + +# upcoming_tasks = reverse("upcoming-tasks", kwargs=V1) +# response = client.get(f"{live_server.url}{upcoming_tasks}", headers=headers) +# assert response.status_code == 403 + +# user_list = reverse("user-list", kwargs=V1) +# response = client.get(f"{live_server.url}{user_list}", headers=headers) +# assert response.status_code == 403 + + +# @pytest.mark.django_db +# def test_urls_authorized(live_server): +# token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) +# encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") +# client = RequestsClient() +# headers = get_headers(encoded) + +# capabilities = reverse("capabilities", kwargs=V1) +# response = client.get(f"{live_server.url}{capabilities}", headers=headers) +# assert response.status_code == 200 + +# schedule_list = reverse("schedule-list", kwargs=V1) +# response = client.get(f"{live_server.url}{schedule_list}", headers=headers) +# assert response.status_code == 200 + +# status = reverse("status", kwargs=V1) +# response = client.get(f"{live_server.url}{status}", headers=headers) +# assert response.status_code == 200 + +# task_root = reverse("task-root", kwargs=V1) +# response = client.get(f"{live_server.url}{task_root}", headers=headers) +# assert response.status_code == 200 + +# task_results_overview = reverse("task-results-overview", kwargs=V1) +# response = client.get(f"{live_server.url}{task_results_overview}", headers=headers) +# assert response.status_code == 200 + +# upcoming_tasks = reverse("upcoming-tasks", kwargs=V1) +# response = client.get(f"{live_server.url}{upcoming_tasks}", headers=headers) +# assert response.status_code == 200 + +# user_list = reverse("user-list", kwargs=V1) +# response = client.get(f"{live_server.url}{user_list}", headers=headers) +# assert response.status_code == 200 + + +# @pytest.mark.django_db +# def test_user_cannot_view_user_detail(live_server): +# sensor01_token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) +# encoded = jwt.encode( +# sensor01_token_payload, str(jwt_keys.private_key), algorithm="RS256" +# ) +# client = RequestsClient() +# response = client.get(f"{live_server.url}", headers=get_headers(encoded)) +# assert response.status_code == 200 + +# sensor02_token_payload = get_token_payload(authorities=["ROLE_USER"]) +# sensor02_token_payload["user_name"] = "sensor02" +# encoded = jwt.encode( +# sensor02_token_payload, str(jwt_keys.private_key), algorithm="RS256" +# ) +# client = RequestsClient() + +# sensor01_user = User.objects.get(username=sensor01_token_payload["user_name"]) +# kws = {"pk": sensor01_user.pk} +# kws.update(V1) +# user_detail = reverse("user-detail", kwargs=kws) +# response = client.get( +# f"{live_server.url}{user_detail}", headers=get_headers(encoded) +# ) +# assert response.status_code == 403 + + +# @pytest.mark.django_db +# def test_user_cannot_view_user_detail_role_change(live_server): +# sensor01_token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) +# encoded = jwt.encode( +# sensor01_token_payload, str(jwt_keys.private_key), algorithm="RS256" +# ) +# client = RequestsClient() +# response = client.get(f"{live_server.url}", headers=get_headers(encoded)) +# assert response.status_code == 200 + +# token_payload = get_token_payload(authorities=["ROLE_USER"]) +# encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") +# client = RequestsClient() + +# sensor01_user = User.objects.get(username=sensor01_token_payload["user_name"]) +# kws = {"pk": sensor01_user.pk} +# kws.update(V1) +# user_detail = reverse("user-detail", kwargs=kws) +# response = client.get( +# f"{live_server.url}{user_detail}", headers=get_headers(encoded) +# ) +# assert response.status_code == 403 + + +# @pytest.mark.django_db +# def test_admin_can_view_user_detail(live_server): +# token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) +# encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") +# client = RequestsClient() +# headers = get_headers(encoded) +# response = client.get(f"{live_server.url}", headers=headers) +# assert response.status_code == 200 + +# sensor01_user = User.objects.get(username=token_payload["user_name"]) +# kws = {"pk": sensor01_user.pk} +# kws.update(V1) +# user_detail = reverse("user-detail", kwargs=kws) +# response = client.get(f"{live_server.url}{user_detail}", headers=headers) +# assert response.status_code == 200 + + +# @pytest.mark.django_db +# def test_admin_can_view_other_user_detail(live_server): +# sensor01_token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) +# encoded = jwt.encode( +# sensor01_token_payload, str(jwt_keys.private_key), algorithm="RS256" +# ) +# client = RequestsClient() +# response = client.get(f"{live_server.url}", headers=get_headers(encoded)) +# assert response.status_code == 200 + +# sensor02_token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) +# sensor02_token_payload["user_name"] = "sensor02" +# encoded = jwt.encode( +# sensor02_token_payload, str(jwt_keys.private_key), algorithm="RS256" +# ) +# client = RequestsClient() + +# sensor01_user = User.objects.get(username=sensor01_token_payload["user_name"]) +# kws = {"pk": sensor01_user.pk} +# kws.update(V1) +# user_detail = reverse("user-detail", kwargs=kws) +# response = client.get( +# f"{live_server.url}{user_detail}", headers=get_headers(encoded) +# ) +# assert response.status_code == 200 + + +# @pytest.mark.django_db +# def test_token_hidden(live_server): +# token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) +# encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") +# client = RequestsClient() +# headers = get_headers(encoded) +# response = client.get(f"{live_server.url}", headers=headers) +# assert response.status_code == 200 + +# sensor01_user = User.objects.get(username=token_payload["user_name"]) +# kws = {"pk": sensor01_user.pk} +# kws.update(V1) +# user_detail = reverse("user-detail", kwargs=kws) +# client = RequestsClient() +# response = client.get(f"{live_server.url}{user_detail}", headers=headers) +# assert response.status_code == 200 +# assert ( +# response.json()["auth_token"] +# == "rest_framework.authentication.TokenAuthentication is not enabled" +# ) + + +# @pytest.mark.django_db +# def test_change_token_role_bad_signature(live_server): +# """Make sure token modified after it was signed is rejected""" +# token_payload = get_token_payload(authorities=["ROLE_USER"]) +# encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") +# first_period = encoded.find(".") +# second_period = encoded.find(".", first_period + 1) +# payload = encoded[first_period + 1 : second_period] +# payload_bytes = payload.encode("utf-8") +# # must be multiple of 4 for b64decode +# for i in range(len(payload_bytes) % 4): +# payload_bytes = payload_bytes + b"=" +# decoded = base64.b64decode(payload_bytes) +# payload_str = decoded.decode("utf-8") +# payload_data = json.loads(payload_str) +# payload_data["user_name"] = "sensor013" +# payload_data["authorities"] = ["ROLE_MANAGER"] +# payload_data["userDetails"]["authorities"] = [{"authority": "ROLE_MANAGER"}] +# payload_str = json.dumps(payload_data) +# modified_payload = base64.b64encode(payload_str.encode("utf-8")) +# modified_payload = modified_payload.decode("utf-8") +# # remove padding +# if modified_payload.endswith("="): +# last_padded_index = len(modified_payload) - 1 +# for i in range(len(modified_payload) - 1, -1, -1): +# if modified_payload[i] != "=": +# last_padded_index = i +# break +# modified_payload = modified_payload[: last_padded_index + 1] +# modified_token = ( +# encoded[:first_period] +# + "." +# + modified_payload +# + "." +# + encoded[second_period + 1 :] +# ) +# client = RequestsClient() +# response = client.get(f"{live_server.url}", headers=get_headers(modified_token)) +# assert response.status_code == 403 +# assert response.json()["detail"] == "Unable to verify token!" diff --git a/src/authentication/tests/test_jwt_auth.py b/src/authentication/tests/test_jwt_auth.py deleted file mode 100644 index 313e6f1d..00000000 --- a/src/authentication/tests/test_jwt_auth.py +++ /dev/null @@ -1,432 +0,0 @@ -import base64 -import json -import os -import secrets -from datetime import datetime, timedelta -from tempfile import NamedTemporaryFile - -import jwt -import pytest -from rest_framework.reverse import reverse -from rest_framework.test import RequestsClient - -from authentication.auth import oauth_jwt_authentication_enabled -from authentication.models import User -from authentication.tests.utils import get_test_public_private_key -from sensor import V1 - -pytestmark = pytest.mark.skipif( - not oauth_jwt_authentication_enabled, - reason="OAuth JWT authentication is not enabled!", -) - - -jwt_content_file = os.path.join( - os.path.dirname(os.path.abspath(__file__)), "jwt_content_example.json" -) -with open(jwt_content_file) as token_file: - TOKEN_CONTENT = json.load(token_file) - -BAD_PRIVATE_KEY, BAD_PUBLIC_KEY = get_test_public_private_key() - -one_min = timedelta(minutes=1) -one_day = timedelta(days=1) - - -def get_token_payload(authorities=["ROLE_MANAGER"], exp=None, client_id=None): - token_payload = TOKEN_CONTENT.copy() - current_datetime = datetime.now() - if not exp: - token_payload["exp"] = (current_datetime + one_day).timestamp() - else: - token_payload["exp"] = exp - token_payload["userDetails"]["lastlogin"] = (current_datetime - one_day).timestamp() - token_payload["userDetails"]["authorities"] = [] - for authority in authorities: - token_payload["userDetails"]["authorities"].append({"authority": authority}) - token_payload["userDetails"]["enabled"] = True - token_payload["authorities"] = authorities - if client_id: - token_payload["client_id"] = client_id - return token_payload - - -def get_headers(token): - return { - "Authorization": f"Bearer {token}", - "X-Ssl-Client-Dn": f"CN=Test,OU=Test,O=Test,L=Test,ST=Test,C=Test", - } - - -@pytest.mark.django_db -def test_no_token_unauthorized(live_server): - client = RequestsClient() - response = client.get(f"{live_server.url}") - assert response.status_code == 403 - - -@pytest.mark.django_db -def test_token_no_roles_unauthorized(live_server, jwt_keys): - token_payload = get_token_payload(authorities=[]) - encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") - client = RequestsClient() - response = client.get(f"{live_server.url}", headers=get_headers(encoded)) - assert response.status_code == 403 - assert response.json()["detail"] == "User missing required role" - - -@pytest.mark.django_db -def test_token_role_manager_accepted(live_server, jwt_keys): - token_payload = get_token_payload() - encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") - client = RequestsClient() - response = client.get(f"{live_server.url}", headers=get_headers(encoded)) - assert response.status_code == 200 - - -def test_bad_token_forbidden(live_server): - client = RequestsClient() - token = ( - secrets.token_urlsafe(28) - + "." - + secrets.token_urlsafe(679) - + "." - + secrets.token_urlsafe(525) - ) - response = client.get(f"{live_server.url}", headers=get_headers(token)) - print(f"headers: {response.request.headers}") - assert response.status_code == 403 - assert "Unable to decode token!" in response.json()["detail"] - - -@pytest.mark.django_db -def test_token_expired_1_day_forbidden(live_server, jwt_keys): - current_datetime = datetime.now() - token_payload = get_token_payload(exp=(current_datetime - one_day).timestamp()) - encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") - client = RequestsClient() - response = client.get(f"{live_server.url}", headers=get_headers(encoded)) - assert response.status_code == 403 - assert response.json()["detail"] == "Token is expired!" - - -@pytest.mark.django_db -def test_bad_private_key_forbidden(live_server): - token_payload = get_token_payload() - encoded = jwt.encode( - token_payload, str(BAD_PRIVATE_KEY.decode("utf-8")), algorithm="RS256" - ) - client = RequestsClient() - response = client.get(f"{live_server.url}", headers=get_headers(encoded)) - assert response.status_code == 403 - assert response.json()["detail"] == "Unable to verify token!" - - -@pytest.mark.django_db -def test_bad_public_key_forbidden(settings, live_server, jwt_keys): - with NamedTemporaryFile() as jwt_public_key_file: - jwt_public_key_file.write(BAD_PUBLIC_KEY) - jwt_public_key_file.flush() - settings.PATH_TO_JWT_PUBLIC_KEY = jwt_public_key_file.name - token_payload = get_token_payload() - encoded = jwt.encode( - token_payload, str(jwt_keys.private_key), algorithm="RS256" - ) - client = RequestsClient() - response = client.get(f"{live_server.url}", headers=get_headers(encoded)) - assert response.status_code == 403 - assert response.json()["detail"] == "Unable to verify token!" - - -@pytest.mark.django_db -def test_token_expired_1_min_forbidden(live_server, jwt_keys): - current_datetime = datetime.now() - token_payload = get_token_payload(exp=(current_datetime - one_min).timestamp()) - encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") - client = RequestsClient() - response = client.get(f"{live_server.url}", headers=get_headers(encoded)) - assert response.status_code == 403 - assert response.json()["detail"] == "Token is expired!" - - -@pytest.mark.django_db -def test_token_expires_in_1_min_accepted(live_server, jwt_keys): - current_datetime = datetime.now() - token_payload = get_token_payload(exp=(current_datetime + one_min).timestamp()) - encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") - client = RequestsClient() - response = client.get(f"{live_server.url}", headers=get_headers(encoded)) - assert response.status_code == 200 - - -@pytest.mark.django_db -def test_token_role_user_forbidden(live_server, jwt_keys): - token_payload = get_token_payload(authorities=["ROLE_USER"]) - encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") - client = RequestsClient() - response = client.get(f"{live_server.url}", headers=get_headers(encoded)) - assert response.status_code == 403 - assert response.json()["detail"] == "User missing required role" - - -@pytest.mark.django_db -def test_token_role_user_required_role_accepted(settings, live_server, jwt_keys): - settings.REQUIRED_ROLE = "ROLE_USER" - token_payload = get_token_payload(authorities=["ROLE_USER"]) - encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") - client = RequestsClient() - response = client.get(f"{live_server.url}", headers=get_headers(encoded)) - assert response.status_code == 200 - - -@pytest.mark.django_db -def test_token_multiple_roles_accepted(live_server, jwt_keys): - token_payload = get_token_payload( - authorities=["ROLE_MANAGER", "ROLE_USER", "ROLE_ITS"] - ) - encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") - client = RequestsClient() - response = client.get(f"{live_server.url}", headers=get_headers(encoded)) - assert response.status_code == 200 - - -@pytest.mark.django_db -def test_token_mulitple_roles_forbidden(live_server, jwt_keys): - token_payload = get_token_payload( - authorities=["ROLE_SENSOR", "ROLE_USER", "ROLE_ITS"] - ) - encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") - client = RequestsClient() - response = client.get(f"{live_server.url}", headers=get_headers(encoded)) - assert response.status_code == 403 - - -@pytest.mark.django_db -def test_urls_unauthorized(live_server, jwt_keys): - token_payload = get_token_payload(authorities=["ROLE_USER"]) - encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") - client = RequestsClient() - headers = get_headers(encoded) - - capabilities = reverse("capabilities", kwargs=V1) - response = client.get(f"{live_server.url}{capabilities}", headers=headers) - assert response.status_code == 403 - - schedule_list = reverse("schedule-list", kwargs=V1) - response = client.get(f"{live_server.url}{schedule_list}", headers=headers) - assert response.status_code == 403 - - status = reverse("status", kwargs=V1) - response = client.get(f"{live_server.url}{status}", headers=headers) - assert response.status_code == 403 - - task_root = reverse("task-root", kwargs=V1) - response = client.get(f"{live_server.url}{task_root}", headers=headers) - assert response.status_code == 403 - - task_results_overview = reverse("task-results-overview", kwargs=V1) - response = client.get(f"{live_server.url}{task_results_overview}", headers=headers) - assert response.status_code == 403 - - upcoming_tasks = reverse("upcoming-tasks", kwargs=V1) - response = client.get(f"{live_server.url}{upcoming_tasks}", headers=headers) - assert response.status_code == 403 - - user_list = reverse("user-list", kwargs=V1) - response = client.get(f"{live_server.url}{user_list}", headers=headers) - assert response.status_code == 403 - - -@pytest.mark.django_db -def test_urls_authorized(live_server, jwt_keys): - token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) - encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") - client = RequestsClient() - headers = get_headers(encoded) - - capabilities = reverse("capabilities", kwargs=V1) - response = client.get(f"{live_server.url}{capabilities}", headers=headers) - assert response.status_code == 200 - - schedule_list = reverse("schedule-list", kwargs=V1) - response = client.get(f"{live_server.url}{schedule_list}", headers=headers) - assert response.status_code == 200 - - status = reverse("status", kwargs=V1) - response = client.get(f"{live_server.url}{status}", headers=headers) - assert response.status_code == 200 - - task_root = reverse("task-root", kwargs=V1) - response = client.get(f"{live_server.url}{task_root}", headers=headers) - assert response.status_code == 200 - - task_results_overview = reverse("task-results-overview", kwargs=V1) - response = client.get(f"{live_server.url}{task_results_overview}", headers=headers) - assert response.status_code == 200 - - upcoming_tasks = reverse("upcoming-tasks", kwargs=V1) - response = client.get(f"{live_server.url}{upcoming_tasks}", headers=headers) - assert response.status_code == 200 - - user_list = reverse("user-list", kwargs=V1) - response = client.get(f"{live_server.url}{user_list}", headers=headers) - assert response.status_code == 200 - - -@pytest.mark.django_db -def test_user_cannot_view_user_detail(live_server, jwt_keys): - sensor01_token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) - encoded = jwt.encode( - sensor01_token_payload, str(jwt_keys.private_key), algorithm="RS256" - ) - client = RequestsClient() - response = client.get(f"{live_server.url}", headers=get_headers(encoded)) - assert response.status_code == 200 - - sensor02_token_payload = get_token_payload(authorities=["ROLE_USER"]) - sensor02_token_payload["user_name"] = "sensor02" - encoded = jwt.encode( - sensor02_token_payload, str(jwt_keys.private_key), algorithm="RS256" - ) - client = RequestsClient() - - sensor01_user = User.objects.get(username=sensor01_token_payload["user_name"]) - kws = {"pk": sensor01_user.pk} - kws.update(V1) - user_detail = reverse("user-detail", kwargs=kws) - response = client.get( - f"{live_server.url}{user_detail}", headers=get_headers(encoded) - ) - assert response.status_code == 403 - - -@pytest.mark.django_db -def test_user_cannot_view_user_detail_role_change(live_server, jwt_keys): - sensor01_token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) - encoded = jwt.encode( - sensor01_token_payload, str(jwt_keys.private_key), algorithm="RS256" - ) - client = RequestsClient() - response = client.get(f"{live_server.url}", headers=get_headers(encoded)) - assert response.status_code == 200 - - token_payload = get_token_payload(authorities=["ROLE_USER"]) - encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") - client = RequestsClient() - - sensor01_user = User.objects.get(username=sensor01_token_payload["user_name"]) - kws = {"pk": sensor01_user.pk} - kws.update(V1) - user_detail = reverse("user-detail", kwargs=kws) - response = client.get( - f"{live_server.url}{user_detail}", headers=get_headers(encoded) - ) - assert response.status_code == 403 - - -@pytest.mark.django_db -def test_admin_can_view_user_detail(live_server, jwt_keys): - token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) - encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") - client = RequestsClient() - headers = get_headers(encoded) - response = client.get(f"{live_server.url}", headers=headers) - assert response.status_code == 200 - - sensor01_user = User.objects.get(username=token_payload["user_name"]) - kws = {"pk": sensor01_user.pk} - kws.update(V1) - user_detail = reverse("user-detail", kwargs=kws) - response = client.get(f"{live_server.url}{user_detail}", headers=headers) - assert response.status_code == 200 - - -@pytest.mark.django_db -def test_admin_can_view_other_user_detail(live_server, jwt_keys): - sensor01_token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) - encoded = jwt.encode( - sensor01_token_payload, str(jwt_keys.private_key), algorithm="RS256" - ) - client = RequestsClient() - response = client.get(f"{live_server.url}", headers=get_headers(encoded)) - assert response.status_code == 200 - - sensor02_token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) - sensor02_token_payload["user_name"] = "sensor02" - encoded = jwt.encode( - sensor02_token_payload, str(jwt_keys.private_key), algorithm="RS256" - ) - client = RequestsClient() - - sensor01_user = User.objects.get(username=sensor01_token_payload["user_name"]) - kws = {"pk": sensor01_user.pk} - kws.update(V1) - user_detail = reverse("user-detail", kwargs=kws) - response = client.get( - f"{live_server.url}{user_detail}", headers=get_headers(encoded) - ) - assert response.status_code == 200 - - -@pytest.mark.django_db -def test_token_hidden(live_server, jwt_keys): - token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) - encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") - client = RequestsClient() - headers = get_headers(encoded) - response = client.get(f"{live_server.url}", headers=headers) - assert response.status_code == 200 - - sensor01_user = User.objects.get(username=token_payload["user_name"]) - kws = {"pk": sensor01_user.pk} - kws.update(V1) - user_detail = reverse("user-detail", kwargs=kws) - client = RequestsClient() - response = client.get(f"{live_server.url}{user_detail}", headers=headers) - assert response.status_code == 200 - assert ( - response.json()["auth_token"] - == "rest_framework.authentication.TokenAuthentication is not enabled" - ) - - -@pytest.mark.django_db -def test_change_token_role_bad_signature(live_server, jwt_keys): - """Make sure token modified after it was signed is rejected""" - token_payload = get_token_payload(authorities=["ROLE_USER"]) - encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") - first_period = encoded.find(".") - second_period = encoded.find(".", first_period + 1) - payload = encoded[first_period + 1 : second_period] - payload_bytes = payload.encode("utf-8") - # must be multiple of 4 for b64decode - for i in range(len(payload_bytes) % 4): - payload_bytes = payload_bytes + b"=" - decoded = base64.b64decode(payload_bytes) - payload_str = decoded.decode("utf-8") - payload_data = json.loads(payload_str) - payload_data["user_name"] = "sensor013" - payload_data["authorities"] = ["ROLE_MANAGER"] - payload_data["userDetails"]["authorities"] = [{"authority": "ROLE_MANAGER"}] - payload_str = json.dumps(payload_data) - modified_payload = base64.b64encode(payload_str.encode("utf-8")) - modified_payload = modified_payload.decode("utf-8") - # remove padding - if modified_payload.endswith("="): - last_padded_index = len(modified_payload) - 1 - for i in range(len(modified_payload) - 1, -1, -1): - if modified_payload[i] != "=": - last_padded_index = i - break - modified_payload = modified_payload[: last_padded_index + 1] - modified_token = ( - encoded[:first_period] - + "." - + modified_payload - + "." - + encoded[second_period + 1 :] - ) - client = RequestsClient() - response = client.get(f"{live_server.url}", headers=get_headers(modified_token)) - assert response.status_code == 403 - assert response.json()["detail"] == "Unable to verify token!" diff --git a/src/authentication/tests/utils.py b/src/authentication/tests/utils.py deleted file mode 100644 index 3ee92b5d..00000000 --- a/src/authentication/tests/utils.py +++ /dev/null @@ -1,21 +0,0 @@ -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import rsa - - -def get_test_public_private_key(): - """Creates public/private key pair for testing - https://stackoverflow.com/a/39126754 - """ - key = rsa.generate_private_key( - backend=default_backend(), public_exponent=65537, key_size=4096 - ) - private_key = key.private_bytes( - serialization.Encoding.PEM, - serialization.PrivateFormat.PKCS8, - serialization.NoEncryption(), - ) - public_key = key.public_key().public_bytes( - serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo - ) - return private_key, public_key diff --git a/src/conftest.py b/src/conftest.py index ec8667eb..a615cba8 100644 --- a/src/conftest.py +++ b/src/conftest.py @@ -8,13 +8,9 @@ import scheduler from authentication.models import User -from authentication.tests.utils import get_test_public_private_key +from sensor.tests.certificate_auth_client import CertificateAuthClient from tasks.models import TaskResult -PRIVATE_KEY, PUBLIC_KEY = get_test_public_private_key() -Keys = namedtuple("KEYS", ["private_key", "public_key"]) -keys = Keys(PRIVATE_KEY.decode("utf-8"), PUBLIC_KEY.decode("utf-8")) - @pytest.fixture(autouse=True) def cleanup_db(db): @@ -24,7 +20,7 @@ def cleanup_db(db): shutil.rmtree(settings.MEDIA_ROOT, ignore_errors=True) -@pytest.yield_fixture +@pytest.fixture def testclock(): """Replace scheduler's timefn with manually steppable test timefn.""" # Setup test clock @@ -66,11 +62,18 @@ def user(db): @pytest.fixture def user_client(db, user): """A Django test client logged in as a normal user""" - client = Client() + client = CertificateAuthClient() client.login(username=user.username, password=user.password) return client +@pytest.fixture +def admin_client(db, admin_user): + """A Django test client logged in as an admin user""" + client = CertificateAuthClient() + client.login(username=admin_user.username, password=admin_user.password) + + return client @pytest.fixture def alt_user(db): @@ -92,7 +95,7 @@ def alt_user(db): @pytest.fixture def alt_user_client(db, alt_user): """A Django test client logged in as a normal user""" - client = Client() + client = CertificateAuthClient() client.login(username=alt_user.username, password=alt_user.password) return client @@ -129,16 +132,7 @@ def alt_admin_client(db, alt_admin_user): """A Django test client logged in as an admin user.""" from django.test.client import Client - client = Client() + client = CertificateAuthClient() client.login(username=alt_admin_user.username, password="password") return client - - -@pytest.fixture(autouse=True) -def jwt_keys(settings): - with tempfile.NamedTemporaryFile() as jwt_public_key_file: - jwt_public_key_file.write(PUBLIC_KEY) - jwt_public_key_file.flush() - settings.PATH_TO_JWT_PUBLIC_KEY = jwt_public_key_file.name - yield keys diff --git a/src/scheduler/scheduler.py b/src/scheduler/scheduler.py index 346845f7..2b012397 100644 --- a/src/scheduler/scheduler.py +++ b/src/scheduler/scheduler.py @@ -188,7 +188,7 @@ def _finalize_task_result(self, started, finished, status, detail): data=json.dumps(result_json), headers=headers, verify=verify_ssl, - cert=(settings.PATH_TO_CLIENT_CERT, settings.PATH_TO_CLIENT_KEY) + cert=settings.PATH_TO_CLIENT_CERT ) self._callback_response_handler(response, tr) else: diff --git a/src/scheduler/tests/test_scheduler.py b/src/scheduler/tests/test_scheduler.py index 3810a5e6..10350ad4 100644 --- a/src/scheduler/tests/test_scheduler.py +++ b/src/scheduler/tests/test_scheduler.py @@ -317,24 +317,16 @@ def test_minimum_duration_non_blocking(): def verify_request(request_history, status="success", detail=None): request_json = None - if conf.settings.CALLBACK_AUTHENTICATION == "OAUTH": - oauth_history = request_history[0] - assert oauth_history.verify == conf.settings.PATH_TO_VERIFY_CERT - assert ( - oauth_history.text - == f"grant_type=password&username={conf.settings.USER_NAME}&password={conf.settings.PASSWORD}" - ) - assert oauth_history.cert == conf.settings.PATH_TO_CLIENT_CERT - auth_header = oauth_history.headers.get("Authorization") - auth_header = auth_header.replace("Basic ", "") - auth_header_decoded = base64.b64decode(auth_header).decode("utf-8") - assert ( - auth_header_decoded - == f"{conf.settings.CLIENT_ID}:{conf.settings.CLIENT_SECRET}" - ) - request_json = request_history[1].json() - else: - request_json = request_history[0].json() + history = request_history[0] + if conf.settings.CALLBACK_SSL_VERIFICATION: + if conf.settings.PATH_TO_VERIFY_CERT: + assert history.verify == conf.settings.PATH_TO_VERIFY_CERT + else: + assert history.verify == True + if conf.settings.CALLBACK_AUTHENTICATION == "CERT": + assert history.cert == conf.settings.PATH_TO_CLIENT_CERT + + request_json = request_history[0].json() assert request_json["status"] == status assert request_json["task_id"] == 1 assert request_json["self"] @@ -348,9 +340,7 @@ def verify_request(request_history, status="success", detail=None): @pytest.mark.django_db def test_failure_posted_to_callback_url(test_scheduler, settings): """If an entry has callback_url defined, scheduler should POST to it.""" - oauth_token_url = "https://auth/mock" callback_url = "https://results" - settings.OAUTH_TOKEN_URL = oauth_token_url cb_flag = threading.Event() def cb_request_handler(sess, resp): @@ -368,16 +358,14 @@ def cb_request_handler(sess, resp): request_history = None with requests_mock.Mocker() as m: # register mock url for posting - if settings.CALLBACK_AUTHENTICATION == "OAUTH": + if settings.CALLBACK_AUTHENTICATION == "CERT": m.post( callback_url, - request_headers={"Authorization": "Bearer " + "test_access_token"}, ) else: m.post( callback_url, request_headers={"Authorization": "Token " + str(token)} ) - m.post(oauth_token_url, json={"access_token": "test_access_token"}) s.run(blocking=False) time.sleep(0.1) # let requests thread run request_history = m.request_history @@ -389,9 +377,7 @@ def cb_request_handler(sess, resp): @pytest.mark.django_db def test_success_posted_to_callback_url(test_scheduler, settings): """If an entry has callback_url defined, scheduler should POST to it.""" - oauth_token_url = "https://auth/mock" callback_url = "https://results" - settings.OAUTH_TOKEN_URL = oauth_token_url cb_flag = threading.Event() def cb_request_handler(sess, resp): @@ -410,20 +396,17 @@ def cb_request_handler(sess, resp): request_history = None with requests_mock.Mocker() as m: # register mock url for posting - if settings.CALLBACK_AUTHENTICATION == "OAUTH": + if settings.CALLBACK_AUTHENTICATION == "CERT": m.post( callback_url, - request_headers={"Authorization": "Bearer " + "test_access_token"}, ) else: m.post( callback_url, request_headers={"Authorization": "Token " + str(token)} ) - m.post(oauth_token_url, json={"access_token": "test_access_token"}) s.run(blocking=False) time.sleep(0.1) # let requests thread run request_history = m.request_history - # request_json = m.request_history[0].json() assert cb_flag.is_set() assert action_flag.is_set() diff --git a/src/sensor/migration_settings.py b/src/sensor/migration_settings.py index d967b3aa..05dabed1 100644 --- a/src/sensor/migration_settings.py +++ b/src/sensor/migration_settings.py @@ -374,12 +374,7 @@ CALLBACK_SSL_VERIFICATION = env.bool("CALLBACK_SSL_VERIFICATION", default=True) # OAuth Password Flow Authentication CALLBACK_AUTHENTICATION = env("CALLBACK_AUTHENTICATION", default="") -CLIENT_ID = env("CLIENT_ID", default="") -CLIENT_SECRET = env("CLIENT_SECRET", default="") -USER_NAME = CLIENT_ID -PASSWORD = CLIENT_SECRET -OAUTH_TOKEN_URL = env("OAUTH_TOKEN_URL", default="") CERTS_DIR = path.join(CONFIG_DIR, "certs") # Sensor certificate with private key used as client cert PATH_TO_CLIENT_CERT = env("PATH_TO_CLIENT_CERT", default="") diff --git a/src/sensor/runtime_settings.py b/src/sensor/runtime_settings.py index dd5fe7a5..4cf5854e 100644 --- a/src/sensor/runtime_settings.py +++ b/src/sensor/runtime_settings.py @@ -374,12 +374,7 @@ CALLBACK_SSL_VERIFICATION = env.bool("CALLBACK_SSL_VERIFICATION", default=True) # OAuth Password Flow Authentication CALLBACK_AUTHENTICATION = env("CALLBACK_AUTHENTICATION", default="") -CLIENT_ID = env("CLIENT_ID", default="") -CLIENT_SECRET = env("CLIENT_SECRET", default="") -USER_NAME = CLIENT_ID -PASSWORD = CLIENT_SECRET -OAUTH_TOKEN_URL = env("OAUTH_TOKEN_URL", default="") CERTS_DIR = path.join(CONFIG_DIR, "certs") # Sensor certificate with private key used as client cert PATH_TO_CLIENT_CERT = env("PATH_TO_CLIENT_CERT", default="") diff --git a/src/sensor/settings.py b/src/sensor/settings.py index f0969beb..e799bf65 100644 --- a/src/sensor/settings.py +++ b/src/sensor/settings.py @@ -375,12 +375,7 @@ CALLBACK_SSL_VERIFICATION = env.bool("CALLBACK_SSL_VERIFICATION", default=True) # OAuth Password Flow Authentication CALLBACK_AUTHENTICATION = env("CALLBACK_AUTHENTICATION", default="") -CLIENT_ID = env("CLIENT_ID", default="") -CLIENT_SECRET = env("CLIENT_SECRET", default="") -USER_NAME = CLIENT_ID -PASSWORD = CLIENT_SECRET -OAUTH_TOKEN_URL = env("OAUTH_TOKEN_URL", default="") CERTS_DIR = path.join(CONFIG_DIR, "certs") # Sensor certificate with private key used as client cert PATH_TO_CLIENT_CERT = env("PATH_TO_CLIENT_CERT", default="") diff --git a/src/sensor/tests/certificate_auth_client.py b/src/sensor/tests/certificate_auth_client.py new file mode 100644 index 00000000..e8fca0a5 --- /dev/null +++ b/src/sensor/tests/certificate_auth_client.py @@ -0,0 +1,52 @@ +from django.test.client import Client, MULTIPART_CONTENT +from django.conf import settings +from django.core.serializers.json import DjangoJSONEncoder + +from sensor.tests.utils import get_http_request_ssl_dn_header + +cert_auth_enabled = settings.AUTHENTICATION == "CERT" + +class CertificateAuthClient(Client): + """Adds SSL DN header if certificate authentication is being used""" + + def __init__(self, enforce_csrf_checks=False, json_encoder=DjangoJSONEncoder, **defaults) -> None: + super().__init__(enforce_csrf_checks, json_encoder=json_encoder, **defaults) + self.username = "" + + def get_kwargs(self, extra): + kwargs = {} + kwargs.update(extra) + if cert_auth_enabled: + kwargs.update(get_http_request_ssl_dn_header(self.username)) + return kwargs + + def get(self, path, data=None, follow=False, secure=False, **extra): + return super().get(path, data, follow, secure, **self.get_kwargs(extra)) + + def post(self, path, data=None, content_type=MULTIPART_CONTENT, follow=False, secure=False, **extra): + return super().post(path, data, content_type, follow, secure, **self.get_kwargs(extra)) + + def head(self, path, data=None, follow=False, secure=False, **extra): + pass + + def options(self, path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra): + pass + + def put(self, path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra): + return super().put(path, data, content_type, follow, secure, **self.get_kwargs(extra)) + + def patch(self, path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra): + pass + + def delete(self, path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra): + return super().delete(path, data, content_type, follow, secure, **self.get_kwargs(extra)) + + def trace(self, path, follow=False, secure=False, **extra): + pass + + def login(self, **credentials): + if cert_auth_enabled: + assert "username" in credentials + self.username = credentials["username"] + else: + super().login(**credentials) diff --git a/src/sensor/tests/utils.py b/src/sensor/tests/utils.py index cf327955..b2de0c51 100644 --- a/src/sensor/tests/utils.py +++ b/src/sensor/tests/utils.py @@ -14,3 +14,13 @@ def validate_response(response, expected_code=None): if actual_code not in (status.HTTP_204_NO_CONTENT,): rjson = response.json() return rjson + +def get_requests_ssl_dn_header(common_name): + return { + "X-Ssl-Client-Dn": f"C=TC,ST=test_state,L=test_locality,O=test_org,OU=test_ou,CN={common_name}", + } + +def get_http_request_ssl_dn_header(common_name): + return { + "HTTP_X-SSL-CLIENT-DN": f"C=TC,ST=test_state,L=test_locality,O=test_org,OU=test_ou,CN={common_name}", + } diff --git a/src/tasks/tests/test_list_view.py b/src/tasks/tests/test_list_view.py index 163516bd..a86fc5f3 100644 --- a/src/tasks/tests/test_list_view.py +++ b/src/tasks/tests/test_list_view.py @@ -35,7 +35,7 @@ def test_user_cannot_view_result_list(admin_client, user_client): url = reverse_result_list(entry_name) response = user_client.get(url, **HTTPS_KWARG) rjson = validate_response(response, status.HTTP_403_FORBIDDEN) - return "results" not in rjson + assert "results" not in rjson @pytest.mark.django_db diff --git a/src/tox.ini b/src/tox.ini index 8ddd3f88..3ee4d73b 100644 --- a/src/tox.ini +++ b/src/tox.ini @@ -14,16 +14,15 @@ setenv = CALLBACK_AUTHENTICATION=TOKEN SWITCH_CONFIGS_DIR=../configs/switches -[testenv:oauth] +[testenv:cert] envlist = py38,py39,py310 setenv = - AUTHENTICATION=JWT - CALLBACK_AUTHENTICATION=OAUTH - CLIENT_ID=sensor01.sms.internal - CLIENT_SECRET=sensor-secret + AUTHENTICATION=CERT + CALLBACK_AUTHENTICATION=CERT PATH_TO_CLIENT_CERT=test/sensor01.pem PATH_TO_VERIFY_CERT=test/scos_test_ca.crt SWITCH_CONFIGS_DIR=../configs/switches + [testenv:coverage] basepython = python3 deps = From d36ce6bcf408fc74355104e0162f15c19bd6189b Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Fri, 27 Jan 2023 12:51:47 -0700 Subject: [PATCH 11/32] update README, remove unused files, fix tests, cleanup, exception handling --- README.md | 6 +- nginx/conf.template | 4 +- scripts/restore_fixture.sh | 66 --- src/authentication/auth.py | 9 +- src/authentication/tests/test_cert_auth.py | 443 ++++++-------------- src/authentication/tests/test_token_auth.py | 2 + src/conftest.py | 8 +- src/schedule/tests/test_user_views.py | 2 +- src/scheduler/tests/test_scheduler.py | 23 +- src/sensor/migration_settings.py | 1 - src/sensor/runtime_settings.py | 1 - src/sensor/settings.py | 1 - src/sensor/tests/certificate_auth_client.py | 19 +- src/sensor/tests/test_api_docs.py | 3 +- src/sensor/tests/utils.py | 2 +- src/status/fixtures/greyhound.json | 12 - src/tasks/tests/test_overview_view.py | 6 +- src/test_utils/task_test_utils.py | 1 + src/tox.ini | 2 +- 19 files changed, 190 insertions(+), 421 deletions(-) delete mode 100755 scripts/restore_fixture.sh delete mode 100644 src/status/fixtures/greyhound.json diff --git a/README.md b/README.md index 217e3d7f..f3b4b53d 100644 --- a/README.md +++ b/README.md @@ -460,8 +460,8 @@ cat sensor01_decrypted.key sensor01.pem > sensor01_combined.pem ##### Client Certificate -This certificate is required for using the sensor with mutual TLS which is required if -OAuth authentication is enabled. +This certificate is required for using the sensor with mutual TLS certificate +authentication. Replace the brackets with the information specific to your user and organization. @@ -542,7 +542,7 @@ will send the user's (user who created the schedule) token in the authorization the token against what it originally sent to the sensor when creating the schedule. This method of authentication for the callback URL is enabled by default. To verify it is enabled, set `CALLBACK_AUTHENTICATION` to `TOKEN` in the environment file (this will -be enabled if `CALLBACK_AUTHENTICATION` set to anything other than `OAUTH`). +be enabled if `CALLBACK_AUTHENTICATION` set to anything other than `CERT`). `PATH_TO_VERIFY_CERT`, in the environment file, can used to set a CA certificate to verify the callback URL server SSL certificate. If this is unset and `CALLBACK_SSL_VERIFICATION` is set to true, [standard trusted CAs]( diff --git a/nginx/conf.template b/nginx/conf.template index 7b79bcac..f39db8c7 100644 --- a/nginx/conf.template +++ b/nginx/conf.template @@ -30,9 +30,9 @@ server { ssl_session_tickets off; ssl_certificate /etc/ssl/certs/ssl-cert.pem; ssl_certificate_key /etc/ssl/private/ssl-cert.key; - ssl_protocols TLSv1.2; + ssl_protocols TLSv1.2 TLSv1.3; ssl_client_certificate /etc/ssl/certs/ca.crt; - ssl_verify_client optional; + ssl_verify_client on; # ssl_ocsp on; # Enable OCSP validation ssl_verify_depth 4; # path for static files diff --git a/scripts/restore_fixture.sh b/scripts/restore_fixture.sh deleted file mode 100755 index 5224e3e6..00000000 --- a/scripts/restore_fixture.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash - -# Exit on error -set -e - -REPO_ROOT=${REPO_ROOT:=$(git rev-parse --show-toplevel)} -PROGRAM_NAME=${0##*/} -INPUT="$1" - - -echo_usage() { - cat << EOF -Restore a fixture file. - -Usage: $PROGRAM_NAME filename - -Example: - $PROGRAM_NAME ./src/capabilities/fixtures/greyhound-2018-02-22.json - -EOF - - exit 0 -} - - -if [[ ! "$INPUT" || "$INPUT" == "-h" || "$INPUT" == "--help" ]]; then - echo_usage - exit 0 -fi - -HOST_PATH=$(readlink -e "$INPUT") -FILENAME=$(basename "$INPUT") -CONTAINER_PATH="/tmp/$FILENAME" - -if [[ ! -e "$HOST_PATH" ]]; then - echo "Fixture file \"$HOST_PATH\" doesn't exist." - exit 1 -fi - -set +e # this command may "fail" -DB_RUNNING=$(docker-compose -f ${REPO_ROOT}/docker-compose.yml ps db |grep Up) -API_RUNNING=$(docker-compose -f ${REPO_ROOT}/docker-compose.yml ps api |grep Up) -set -e - -# Ensure database container is running -docker-compose -f ${REPO_ROOT}/docker-compose.yml up -d db - -# Load given fixture file into database -if [[ "$API_RUNNING" ]]; then - API_CONTAINER=$(docker-compose -f ${REPO_ROOT}/docker-compose.yml ps -q api) - docker cp "$HOST_PATH" ${API_CONTAINER}:/tmp - docker-compose -f ${REPO_ROOT}/docker-compose.yml exec api \ - /src/manage.py loaddata "$CONTAINER_PATH" -else - docker-compose -f ${REPO_ROOT}/docker-compose.yml run \ - -v "$HOST_PATH":"$CONTAINER_PATH" \ - --rm api /src/manage.py loaddata "$CONTAINER_PATH" -fi - -# If the DB was already running, leave it up -if [[ ! "$DB_RUNNING" ]]; then - # Stop database container - docker-compose -f ${REPO_ROOT}/docker-compose.yml stop db -fi - -echo "Restored fixture from $INPUT." diff --git a/src/authentication/auth.py b/src/authentication/auth.py index 1cdcd0b7..9d13e833 100644 --- a/src/authentication/auth.py +++ b/src/authentication/auth.py @@ -21,15 +21,14 @@ def authenticate(self, request): logger.debug("Authenticating certificate.") cert_dn = request.headers.get("X-Ssl-Client-Dn") if cert_dn: - logger.info("DN:" + cert_dn) - cn = get_cn_from_dn(cert_dn) - logger.info("Cert cn: " + cn) user_model = get_user_model() - user = None try: + cn = get_cn_from_dn(cert_dn) user = user_model.objects.get(username=cn) except user_model.DoesNotExist: raise exceptions.AuthenticationFailed("No matching username found!") + except Exception: + raise exceptions.AuthenticationFailed("Error occurred during certificate authentication!") return user, None return None, None @@ -40,7 +39,5 @@ def get_cn_from_dn(cert_dn): if not match: raise Exception("No CN found in certificate!") uid_raw = match.group() - # logger.debug(f"uid_raw = {uid_raw}") uid = uid_raw.split("=")[1].rstrip(",") - # logger.debug(f"uid = {uid}") return uid diff --git a/src/authentication/tests/test_cert_auth.py b/src/authentication/tests/test_cert_auth.py index e8b489ec..36312fa2 100644 --- a/src/authentication/tests/test_cert_auth.py +++ b/src/authentication/tests/test_cert_auth.py @@ -1,10 +1,3 @@ -import base64 -import json -import os -import secrets -from datetime import datetime, timedelta -from tempfile import NamedTemporaryFile - import pytest from rest_framework.reverse import reverse from rest_framework.test import RequestsClient @@ -20,21 +13,39 @@ ) +@pytest.mark.django_db +def test_no_client_cert_unauthorized_no_dn(live_server): + client = RequestsClient() + response = client.get(f"{live_server.url}") + assert response.status_code == 403 + capabilities = reverse("capabilities", kwargs=V1) + response = client.get(f"{live_server.url}{capabilities}") + assert response.status_code == 403 -one_min = timedelta(minutes=1) -one_day = timedelta(days=1) - + schedule_list = reverse("schedule-list", kwargs=V1) + response = client.get(f"{live_server.url}{schedule_list}") + assert response.status_code == 403 + status = reverse("status", kwargs=V1) + response = client.get(f"{live_server.url}{status}") + assert response.status_code == 403 + task_root = reverse("task-root", kwargs=V1) + response = client.get(f"{live_server.url}{task_root}") + assert response.status_code == 403 + task_results_overview = reverse("task-results-overview", kwargs=V1) + response = client.get(f"{live_server.url}{task_results_overview}") + assert response.status_code == 403 -@pytest.mark.django_db -def test_no_client_cert_unauthorized(live_server): - client = RequestsClient() - response = client.get(f"{live_server.url}") + upcoming_tasks = reverse("upcoming-tasks", kwargs=V1) + response = client.get(f"{live_server.url}{upcoming_tasks}") assert response.status_code == 403 + user_list = reverse("user-list", kwargs=V1) + response = client.get(f"{live_server.url}{user_list}") + assert response.status_code == 403 @pytest.mark.django_db @@ -44,299 +55,123 @@ def test_client_cert_accepted(live_server, admin_user): assert response.status_code == 200 -def test_bad_client_cert_forbidden(live_server, admin_user): +def test_mismatching_user_forbidden(live_server, admin_user): client = RequestsClient() response = client.get(f"{live_server.url}", headers=get_requests_ssl_dn_header("user")) assert response.status_code == 403 assert "No matching username found!" in response.json()["detail"] -# @pytest.mark.django_db -# def test_certificate_expired_1_day_forbidden(live_server): -# current_datetime = datetime.now() -# token_payload = get_token_payload(exp=(current_datetime - one_day).timestamp()) -# encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") -# client = RequestsClient() -# response = client.get(f"{live_server.url}", headers=get_headers(encoded)) -# assert response.status_code == 403 -# assert response.json()["detail"] == "Token is expired!" - - -# @pytest.mark.django_db -# def test_bad_private_key_forbidden(live_server): -# token_payload = get_token_payload() -# encoded = jwt.encode( -# token_payload, str(BAD_PRIVATE_KEY.decode("utf-8")), algorithm="RS256" -# ) -# client = RequestsClient() -# response = client.get(f"{live_server.url}", headers=get_headers(encoded)) -# assert response.status_code == 403 -# assert response.json()["detail"] == "Unable to verify token!" - - -# @pytest.mark.django_db -# def test_bad_public_key_forbidden(settings, live_server): -# with NamedTemporaryFile() as jwt_public_key_file: -# jwt_public_key_file.write(BAD_PUBLIC_KEY) -# jwt_public_key_file.flush() -# settings.PATH_TO_JWT_PUBLIC_KEY = jwt_public_key_file.name -# token_payload = get_token_payload() -# encoded = jwt.encode( -# token_payload, str(jwt_keys.private_key), algorithm="RS256" -# ) -# client = RequestsClient() -# response = client.get(f"{live_server.url}", headers=get_headers(encoded)) -# assert response.status_code == 403 -# assert response.json()["detail"] == "Unable to verify token!" - - -# @pytest.mark.django_db -# def test_certificate_expired_1_min_forbidden(live_server): -# current_datetime = datetime.now() -# token_payload = get_token_payload(exp=(current_datetime - one_min).timestamp()) -# encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") -# client = RequestsClient() -# response = client.get(f"{live_server.url}", headers=get_headers(encoded)) -# assert response.status_code == 403 -# assert response.json()["detail"] == "Token is expired!" - - -# @pytest.mark.django_db -# def test_certificate_expires_in_1_min_accepted(live_server): -# current_datetime = datetime.now() -# token_payload = get_token_payload(exp=(current_datetime + one_min).timestamp()) -# encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") -# client = RequestsClient() -# response = client.get(f"{live_server.url}", headers=get_headers(encoded)) -# assert response.status_code == 200 - - -# @pytest.mark.django_db -# def test_urls_unauthorized(live_server): -# token_payload = get_token_payload(authorities=["ROLE_USER"]) -# encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") -# client = RequestsClient() -# headers = get_headers(encoded) - -# capabilities = reverse("capabilities", kwargs=V1) -# response = client.get(f"{live_server.url}{capabilities}", headers=headers) -# assert response.status_code == 403 - -# schedule_list = reverse("schedule-list", kwargs=V1) -# response = client.get(f"{live_server.url}{schedule_list}", headers=headers) -# assert response.status_code == 403 - -# status = reverse("status", kwargs=V1) -# response = client.get(f"{live_server.url}{status}", headers=headers) -# assert response.status_code == 403 - -# task_root = reverse("task-root", kwargs=V1) -# response = client.get(f"{live_server.url}{task_root}", headers=headers) -# assert response.status_code == 403 - -# task_results_overview = reverse("task-results-overview", kwargs=V1) -# response = client.get(f"{live_server.url}{task_results_overview}", headers=headers) -# assert response.status_code == 403 - -# upcoming_tasks = reverse("upcoming-tasks", kwargs=V1) -# response = client.get(f"{live_server.url}{upcoming_tasks}", headers=headers) -# assert response.status_code == 403 - -# user_list = reverse("user-list", kwargs=V1) -# response = client.get(f"{live_server.url}{user_list}", headers=headers) -# assert response.status_code == 403 - - -# @pytest.mark.django_db -# def test_urls_authorized(live_server): -# token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) -# encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") -# client = RequestsClient() -# headers = get_headers(encoded) - -# capabilities = reverse("capabilities", kwargs=V1) -# response = client.get(f"{live_server.url}{capabilities}", headers=headers) -# assert response.status_code == 200 - -# schedule_list = reverse("schedule-list", kwargs=V1) -# response = client.get(f"{live_server.url}{schedule_list}", headers=headers) -# assert response.status_code == 200 - -# status = reverse("status", kwargs=V1) -# response = client.get(f"{live_server.url}{status}", headers=headers) -# assert response.status_code == 200 - -# task_root = reverse("task-root", kwargs=V1) -# response = client.get(f"{live_server.url}{task_root}", headers=headers) -# assert response.status_code == 200 - -# task_results_overview = reverse("task-results-overview", kwargs=V1) -# response = client.get(f"{live_server.url}{task_results_overview}", headers=headers) -# assert response.status_code == 200 - -# upcoming_tasks = reverse("upcoming-tasks", kwargs=V1) -# response = client.get(f"{live_server.url}{upcoming_tasks}", headers=headers) -# assert response.status_code == 200 - -# user_list = reverse("user-list", kwargs=V1) -# response = client.get(f"{live_server.url}{user_list}", headers=headers) -# assert response.status_code == 200 - - -# @pytest.mark.django_db -# def test_user_cannot_view_user_detail(live_server): -# sensor01_token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) -# encoded = jwt.encode( -# sensor01_token_payload, str(jwt_keys.private_key), algorithm="RS256" -# ) -# client = RequestsClient() -# response = client.get(f"{live_server.url}", headers=get_headers(encoded)) -# assert response.status_code == 200 - -# sensor02_token_payload = get_token_payload(authorities=["ROLE_USER"]) -# sensor02_token_payload["user_name"] = "sensor02" -# encoded = jwt.encode( -# sensor02_token_payload, str(jwt_keys.private_key), algorithm="RS256" -# ) -# client = RequestsClient() - -# sensor01_user = User.objects.get(username=sensor01_token_payload["user_name"]) -# kws = {"pk": sensor01_user.pk} -# kws.update(V1) -# user_detail = reverse("user-detail", kwargs=kws) -# response = client.get( -# f"{live_server.url}{user_detail}", headers=get_headers(encoded) -# ) -# assert response.status_code == 403 - - -# @pytest.mark.django_db -# def test_user_cannot_view_user_detail_role_change(live_server): -# sensor01_token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) -# encoded = jwt.encode( -# sensor01_token_payload, str(jwt_keys.private_key), algorithm="RS256" -# ) -# client = RequestsClient() -# response = client.get(f"{live_server.url}", headers=get_headers(encoded)) -# assert response.status_code == 200 - -# token_payload = get_token_payload(authorities=["ROLE_USER"]) -# encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") -# client = RequestsClient() - -# sensor01_user = User.objects.get(username=sensor01_token_payload["user_name"]) -# kws = {"pk": sensor01_user.pk} -# kws.update(V1) -# user_detail = reverse("user-detail", kwargs=kws) -# response = client.get( -# f"{live_server.url}{user_detail}", headers=get_headers(encoded) -# ) -# assert response.status_code == 403 - - -# @pytest.mark.django_db -# def test_admin_can_view_user_detail(live_server): -# token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) -# encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") -# client = RequestsClient() -# headers = get_headers(encoded) -# response = client.get(f"{live_server.url}", headers=headers) -# assert response.status_code == 200 - -# sensor01_user = User.objects.get(username=token_payload["user_name"]) -# kws = {"pk": sensor01_user.pk} -# kws.update(V1) -# user_detail = reverse("user-detail", kwargs=kws) -# response = client.get(f"{live_server.url}{user_detail}", headers=headers) -# assert response.status_code == 200 - - -# @pytest.mark.django_db -# def test_admin_can_view_other_user_detail(live_server): -# sensor01_token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) -# encoded = jwt.encode( -# sensor01_token_payload, str(jwt_keys.private_key), algorithm="RS256" -# ) -# client = RequestsClient() -# response = client.get(f"{live_server.url}", headers=get_headers(encoded)) -# assert response.status_code == 200 - -# sensor02_token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) -# sensor02_token_payload["user_name"] = "sensor02" -# encoded = jwt.encode( -# sensor02_token_payload, str(jwt_keys.private_key), algorithm="RS256" -# ) -# client = RequestsClient() - -# sensor01_user = User.objects.get(username=sensor01_token_payload["user_name"]) -# kws = {"pk": sensor01_user.pk} -# kws.update(V1) -# user_detail = reverse("user-detail", kwargs=kws) -# response = client.get( -# f"{live_server.url}{user_detail}", headers=get_headers(encoded) -# ) -# assert response.status_code == 200 - - -# @pytest.mark.django_db -# def test_token_hidden(live_server): -# token_payload = get_token_payload(authorities=["ROLE_MANAGER"]) -# encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") -# client = RequestsClient() -# headers = get_headers(encoded) -# response = client.get(f"{live_server.url}", headers=headers) -# assert response.status_code == 200 - -# sensor01_user = User.objects.get(username=token_payload["user_name"]) -# kws = {"pk": sensor01_user.pk} -# kws.update(V1) -# user_detail = reverse("user-detail", kwargs=kws) -# client = RequestsClient() -# response = client.get(f"{live_server.url}{user_detail}", headers=headers) -# assert response.status_code == 200 -# assert ( -# response.json()["auth_token"] -# == "rest_framework.authentication.TokenAuthentication is not enabled" -# ) - - -# @pytest.mark.django_db -# def test_change_token_role_bad_signature(live_server): -# """Make sure token modified after it was signed is rejected""" -# token_payload = get_token_payload(authorities=["ROLE_USER"]) -# encoded = jwt.encode(token_payload, str(jwt_keys.private_key), algorithm="RS256") -# first_period = encoded.find(".") -# second_period = encoded.find(".", first_period + 1) -# payload = encoded[first_period + 1 : second_period] -# payload_bytes = payload.encode("utf-8") -# # must be multiple of 4 for b64decode -# for i in range(len(payload_bytes) % 4): -# payload_bytes = payload_bytes + b"=" -# decoded = base64.b64decode(payload_bytes) -# payload_str = decoded.decode("utf-8") -# payload_data = json.loads(payload_str) -# payload_data["user_name"] = "sensor013" -# payload_data["authorities"] = ["ROLE_MANAGER"] -# payload_data["userDetails"]["authorities"] = [{"authority": "ROLE_MANAGER"}] -# payload_str = json.dumps(payload_data) -# modified_payload = base64.b64encode(payload_str.encode("utf-8")) -# modified_payload = modified_payload.decode("utf-8") -# # remove padding -# if modified_payload.endswith("="): -# last_padded_index = len(modified_payload) - 1 -# for i in range(len(modified_payload) - 1, -1, -1): -# if modified_payload[i] != "=": -# last_padded_index = i -# break -# modified_payload = modified_payload[: last_padded_index + 1] -# modified_token = ( -# encoded[:first_period] -# + "." -# + modified_payload -# + "." -# + encoded[second_period + 1 :] -# ) -# client = RequestsClient() -# response = client.get(f"{live_server.url}", headers=get_headers(modified_token)) -# assert response.status_code == 403 -# assert response.json()["detail"] == "Unable to verify token!" +@pytest.mark.django_db +def test_urls_unauthorized_not_superuser(live_server, user): + client = RequestsClient() + headers = get_requests_ssl_dn_header("user") + + capabilities = reverse("capabilities", kwargs=V1) + response = client.get(f"{live_server.url}{capabilities}", headers=headers) + assert response.status_code == 403 + + schedule_list = reverse("schedule-list", kwargs=V1) + response = client.get(f"{live_server.url}{schedule_list}", headers=headers) + assert response.status_code == 403 + + status = reverse("status", kwargs=V1) + response = client.get(f"{live_server.url}{status}", headers=headers) + assert response.status_code == 403 + + task_root = reverse("task-root", kwargs=V1) + response = client.get(f"{live_server.url}{task_root}", headers=headers) + assert response.status_code == 403 + + task_results_overview = reverse("task-results-overview", kwargs=V1) + response = client.get(f"{live_server.url}{task_results_overview}", headers=headers) + assert response.status_code == 403 + + upcoming_tasks = reverse("upcoming-tasks", kwargs=V1) + response = client.get(f"{live_server.url}{upcoming_tasks}", headers=headers) + assert response.status_code == 403 + + user_list = reverse("user-list", kwargs=V1) + response = client.get(f"{live_server.url}{user_list}", headers=headers) + assert response.status_code == 403 + + +@pytest.mark.django_db +def test_urls_authorized(live_server, admin_user): + client = RequestsClient() + headers = get_requests_ssl_dn_header("admin") + + capabilities = reverse("capabilities", kwargs=V1) + response = client.get(f"{live_server.url}{capabilities}", headers=headers) + assert response.status_code == 200 + + schedule_list = reverse("schedule-list", kwargs=V1) + response = client.get(f"{live_server.url}{schedule_list}", headers=headers) + assert response.status_code == 200 + + status = reverse("status", kwargs=V1) + response = client.get(f"{live_server.url}{status}", headers=headers) + assert response.status_code == 200 + + task_root = reverse("task-root", kwargs=V1) + response = client.get(f"{live_server.url}{task_root}", headers=headers) + assert response.status_code == 200 + + task_results_overview = reverse("task-results-overview", kwargs=V1) + response = client.get(f"{live_server.url}{task_results_overview}", headers=headers) + assert response.status_code == 200 + + upcoming_tasks = reverse("upcoming-tasks", kwargs=V1) + response = client.get(f"{live_server.url}{upcoming_tasks}", headers=headers) + assert response.status_code == 200 + + user_list = reverse("user-list", kwargs=V1) + response = client.get(f"{live_server.url}{user_list}", headers=headers) + assert response.status_code == 200 + + +@pytest.mark.django_db +def test_token_hidden(live_server, admin_user): + client = RequestsClient() + headers = get_requests_ssl_dn_header("admin") + response = client.get(f"{live_server.url}", headers=headers) + assert response.status_code == 200 + + sensor01_user = User.objects.get(username=admin_user.username) + kws = {"pk": sensor01_user.pk} + kws.update(V1) + user_detail = reverse("user-detail", kwargs=kws) + client = RequestsClient() + response = client.get(f"{live_server.url}{user_detail}", headers=headers) + assert response.status_code == 200 + assert ( + response.json()["auth_token"] + == "rest_framework.authentication.TokenAuthentication is not enabled" + ) + + +@pytest.mark.django_db +def test_empty_common_name_unauthorized(live_server, admin_user): + client = RequestsClient() + response = client.get(f"{live_server.url}", headers=get_requests_ssl_dn_header("")) + assert response.status_code == 403 + + +@pytest.mark.django_db +def test_invalid_dn_unauthorized(live_server, admin_user): + client = RequestsClient() + headers = { + "X-Ssl-Client-Dn": f"C=TC,ST=test_state,L=test_locality,O=test_org,OU=test_ou", + } + response = client.get(f"{live_server.url}", headers=headers) + assert response.status_code == 403 + + +@pytest.mark.django_db +def test_empty_dn_unauthorized(live_server, admin_user): + client = RequestsClient() + headers = { + "X-Ssl-Client-Dn": "", + } + response = client.get(f"{live_server.url}", headers=headers) + assert response.status_code == 403 \ No newline at end of file diff --git a/src/authentication/tests/test_token_auth.py b/src/authentication/tests/test_token_auth.py index c0e7aae6..f8f1d48b 100644 --- a/src/authentication/tests/test_token_auth.py +++ b/src/authentication/tests/test_token_auth.py @@ -6,6 +6,7 @@ from authentication.auth import token_auth_enabled from sensor import V1 +from sensor.tests.utils import HTTPS_KWARG pytestmark = pytest.mark.skipif( not token_auth_enabled, reason="Token authentication is not enabled!" @@ -160,6 +161,7 @@ def test_user_cannot_view_user_detail(settings, live_server, user_client, user): response = user_client.get( f"{live_server.url}{user_detail}", headers={"Authorization": f"Token {user.auth_token.key}"}, + **HTTPS_KWARG ) assert response.status_code == 403 diff --git a/src/conftest.py b/src/conftest.py index a615cba8..acdc0b75 100644 --- a/src/conftest.py +++ b/src/conftest.py @@ -63,7 +63,7 @@ def user(db): def user_client(db, user): """A Django test client logged in as a normal user""" client = CertificateAuthClient() - client.login(username=user.username, password=user.password) + assert client.login(username=user.username, password=user.password) return client @@ -71,7 +71,7 @@ def user_client(db, user): def admin_client(db, admin_user): """A Django test client logged in as an admin user""" client = CertificateAuthClient() - client.login(username=admin_user.username, password=admin_user.password) + assert client.login(username=admin_user.username, password="password") return client @@ -96,7 +96,7 @@ def alt_user(db): def alt_user_client(db, alt_user): """A Django test client logged in as a normal user""" client = CertificateAuthClient() - client.login(username=alt_user.username, password=alt_user.password) + assert client.login(username=alt_user.username, password=alt_user.password) return client @@ -133,6 +133,6 @@ def alt_admin_client(db, alt_admin_user): from django.test.client import Client client = CertificateAuthClient() - client.login(username=alt_admin_user.username, password="password") + assert client.login(username=alt_admin_user.username, password="password") return client diff --git a/src/schedule/tests/test_user_views.py b/src/schedule/tests/test_user_views.py index 1d9b3a65..10b8c511 100644 --- a/src/schedule/tests/test_user_views.py +++ b/src/schedule/tests/test_user_views.py @@ -32,6 +32,6 @@ def test_user_cannot_view_schedule_entry_detail(user_client, admin_client): kws = {"pk": admin_entry_name} kws.update(V1) admin_url = reverse("schedule-detail", kwargs=kws) - response = user_client.get(admin_url) + response = user_client.get(admin_url, **HTTPS_KWARG) rjson = validate_response(response, status.HTTP_403_FORBIDDEN) assert rjson != EMPTY_SCHEDULE_RESPONSE diff --git a/src/scheduler/tests/test_scheduler.py b/src/scheduler/tests/test_scheduler.py index 10350ad4..8d73d586 100644 --- a/src/scheduler/tests/test_scheduler.py +++ b/src/scheduler/tests/test_scheduler.py @@ -415,15 +415,20 @@ def cb_request_handler(sess, resp): @pytest.mark.django_db def test_notification_failed_status_unknown_host(test_scheduler): - entry = create_entry("t", 1, 1, 100, 5, "logger", "https://badmgr.its.bldrdoc.gov") - entry.save() - entry.refresh_from_db() - print("entry = " + entry.name) - s = test_scheduler - advance_testclock(s.timefn, 1) - s.run(blocking=False) # queue first 10 tasks - result = TaskResult.objects.first() - assert result.status == "notification_failed" + with requests_mock.Mocker() as m: + callback_url = "https://results" + entry = create_entry("t", 1, 1, 100, 5, "logger", callback_url) + entry.save() + token = entry.owner.auth_token + m.post(callback_url, request_headers={"Authorization": "Token " + str(token)}, text='Not Found', status_code=404) + + entry.refresh_from_db() + print("entry = " + entry.name) + s = test_scheduler + advance_testclock(s.timefn, 1) + s.run(blocking=False) # queue first 10 tasks + result = TaskResult.objects.first() + assert result.status == "notification_failed" @pytest.mark.django_db diff --git a/src/sensor/migration_settings.py b/src/sensor/migration_settings.py index 05dabed1..b63e0a95 100644 --- a/src/sensor/migration_settings.py +++ b/src/sensor/migration_settings.py @@ -372,7 +372,6 @@ CALLBACK_SSL_VERIFICATION = env.bool("CALLBACK_SSL_VERIFICATION", default=True) -# OAuth Password Flow Authentication CALLBACK_AUTHENTICATION = env("CALLBACK_AUTHENTICATION", default="") CERTS_DIR = path.join(CONFIG_DIR, "certs") diff --git a/src/sensor/runtime_settings.py b/src/sensor/runtime_settings.py index 4cf5854e..22dcaf75 100644 --- a/src/sensor/runtime_settings.py +++ b/src/sensor/runtime_settings.py @@ -372,7 +372,6 @@ CALLBACK_SSL_VERIFICATION = env.bool("CALLBACK_SSL_VERIFICATION", default=True) -# OAuth Password Flow Authentication CALLBACK_AUTHENTICATION = env("CALLBACK_AUTHENTICATION", default="") CERTS_DIR = path.join(CONFIG_DIR, "certs") diff --git a/src/sensor/settings.py b/src/sensor/settings.py index e799bf65..c6fbc477 100644 --- a/src/sensor/settings.py +++ b/src/sensor/settings.py @@ -373,7 +373,6 @@ CALLBACK_SSL_VERIFICATION = env.bool("CALLBACK_SSL_VERIFICATION", default=True) -# OAuth Password Flow Authentication CALLBACK_AUTHENTICATION = env("CALLBACK_AUTHENTICATION", default="") CERTS_DIR = path.join(CONFIG_DIR, "certs") diff --git a/src/sensor/tests/certificate_auth_client.py b/src/sensor/tests/certificate_auth_client.py index e8fca0a5..098555b4 100644 --- a/src/sensor/tests/certificate_auth_client.py +++ b/src/sensor/tests/certificate_auth_client.py @@ -21,32 +21,41 @@ def get_kwargs(self, extra): return kwargs def get(self, path, data=None, follow=False, secure=False, **extra): + assert secure return super().get(path, data, follow, secure, **self.get_kwargs(extra)) def post(self, path, data=None, content_type=MULTIPART_CONTENT, follow=False, secure=False, **extra): + assert secure return super().post(path, data, content_type, follow, secure, **self.get_kwargs(extra)) def head(self, path, data=None, follow=False, secure=False, **extra): - pass + assert secure + return super().head(path, data, follow, secure, **self.get_kwargs(extra)) def options(self, path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra): - pass + assert secure + return super().options(self, path, data, content_type, follow, secure, **self.get_kwargs(extra)) def put(self, path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra): + assert secure return super().put(path, data, content_type, follow, secure, **self.get_kwargs(extra)) def patch(self, path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra): - pass + assert secure + return super().patch(path, data, content_type, follow, secure, **self.get_kwargs(extra)) def delete(self, path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra): + assert secure return super().delete(path, data, content_type, follow, secure, **self.get_kwargs(extra)) def trace(self, path, follow=False, secure=False, **extra): - pass + assert secure + return super().trace(path, follow, secure, **self.get_kwargs(extra)) def login(self, **credentials): if cert_auth_enabled: assert "username" in credentials self.username = credentials["username"] + return True else: - super().login(**credentials) + return super().login(**credentials) diff --git a/src/sensor/tests/test_api_docs.py b/src/sensor/tests/test_api_docs.py index 5f73b507..8594bdce 100644 --- a/src/sensor/tests/test_api_docs.py +++ b/src/sensor/tests/test_api_docs.py @@ -4,6 +4,7 @@ from rest_framework.reverse import reverse from sensor import V1, settings +from sensor.tests.utils import HTTPS_KWARG def test_api_docs_up_to_date(admin_client): @@ -16,7 +17,7 @@ def test_api_docs_up_to_date(admin_client): return True schema_url = reverse("api_schema", kwargs=V1) + "?format=openapi" - response = admin_client.get(schema_url) + response = admin_client.get(schema_url, **HTTPS_KWARG) with open(settings.OPENAPI_FILE, "w+") as openapi_file: openapi_json = json.loads(response.content) diff --git a/src/sensor/tests/utils.py b/src/sensor/tests/utils.py index b2de0c51..39f84203 100644 --- a/src/sensor/tests/utils.py +++ b/src/sensor/tests/utils.py @@ -1,6 +1,6 @@ from rest_framework import status -HTTPS_KWARG = {"wsgi.url_scheme": "https"} +HTTPS_KWARG = {"wsgi.url_scheme": "https", "secure": True} def validate_response(response, expected_code=None): diff --git a/src/status/fixtures/greyhound.json b/src/status/fixtures/greyhound.json deleted file mode 100644 index 0ba0d957..00000000 --- a/src/status/fixtures/greyhound.json +++ /dev/null @@ -1,12 +0,0 @@ -[ -{ - "model": "status.location", - "pk": 1, - "fields": { - "active": true, - "description": "DOC Boulder Labs Bldg 1 3420", - "latitude": "39.994793", - "longitude": "-105.262078" - } -} -] diff --git a/src/tasks/tests/test_overview_view.py b/src/tasks/tests/test_overview_view.py index bf15b80e..0bdc99b8 100644 --- a/src/tasks/tests/test_overview_view.py +++ b/src/tasks/tests/test_overview_view.py @@ -31,10 +31,10 @@ def test_admin_get_overview(admin_client): assert overview["schedule_entry"] # is non-empty string -def test_user_delete_overview_not_allowed(admin_client): +def test_user_delete_overview_not_allowed(user_client): url = reverse_results_overview() - response = admin_client.delete(url, **HTTPS_KWARG) - assert validate_response(response, status.HTTP_405_METHOD_NOT_ALLOWED) + response = user_client.delete(url, **HTTPS_KWARG) + assert validate_response(response, status.HTTP_403_FORBIDDEN) def test_admin_delete_overview_not_allowed(admin_client): diff --git a/src/test_utils/task_test_utils.py b/src/test_utils/task_test_utils.py index 2f8534ce..78ce02c2 100644 --- a/src/test_utils/task_test_utils.py +++ b/src/test_utils/task_test_utils.py @@ -187,6 +187,7 @@ def update_result_detail(client, schedule_entry_name, task_id, new_acquisition): "data": json.dumps(new_acquisition), "content_type": "application/json", "wsgi.url_scheme": "https", + "secure": True } return client.put(url, **kwargs) diff --git a/src/tox.ini b/src/tox.ini index 3ee4d73b..8dcf7ff1 100644 --- a/src/tox.ini +++ b/src/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py38,py39,py310 +envlist = py38,py39,py310,cert skip_missing_interpreters = True skipsdist = True From 1846c2ca5889e3d779eaf083ab2f028b990565d3 Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Fri, 27 Jan 2023 14:56:49 -0700 Subject: [PATCH 12/32] update openapi security definitions, autoformatting, settings comment --- docs/openapi.json | 13 +++- src/authentication/auth.py | 15 ++-- src/authentication/tests/test_cert_auth.py | 10 ++- src/authentication/tests/test_token_auth.py | 2 +- src/conftest.py | 2 + src/scheduler/scheduler.py | 2 +- src/scheduler/tests/test_scheduler.py | 9 ++- src/sensor/migration_settings.py | 69 +++++++++--------- src/sensor/runtime_settings.py | 69 +++++++++--------- src/sensor/settings.py | 69 +++++++++--------- src/sensor/tests/certificate_auth_client.py | 77 +++++++++++++++++---- src/sensor/tests/utils.py | 2 + src/test_utils/task_test_utils.py | 2 +- 13 files changed, 207 insertions(+), 134 deletions(-) diff --git a/docs/openapi.json b/docs/openapi.json index 270b63cc..602ce87d 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -14,7 +14,7 @@ }, "host": "testserver", "schemes": [ - "http" + "https" ], "basePath": "/api", "consumes": [ @@ -24,14 +24,21 @@ "application/json" ], "securityDefinitions": { + "cert": { + "type": "cert", + "description": "Certificate based mutual TLS authentication. AUTHENTICATION must be set to 'CERT'. This is done by the client verifying the server certificate and the server verifying the client certificate. The client certificate Common Name (CN) should contain the username of a user that exists in the database. Client certificate verification is handled by NGINX. For more information, see https://www.rfc-editor.org/rfc/rfc5246." + }, "token": { "type": "apiKey", - "description": "Tokens are automatically generated for all users. You can view yours by going to your User Details view in the browsable API at `/api/v1/users/me` and looking for the `auth_token` key. New user accounts do not initially have a password and so can not log in to the browsable API. To set a password for a user (for testing purposes), an admin can do that in the Sensor Configuration Portal, but only the account's token should be stored and used for general purpose API access. Example cURL call: `curl -kLsS -H \"Authorization: Token 529c30e6e04b3b546f2e073e879b75fdfa147c15\" https://localhost/api/v1`", + "description": "Tokens are automatically generated for all users. You can view yours by going to your User Details view in the browsable API at `/api/v1/users/me` and looking for the `auth_token` key. New user accounts do not initially have a password and so can not log in to the browsable API. To set a password for a user (for testing purposes), an admin can do that in the Sensor Configuration Portal, but only the account's token should be stored and used for general purpose API access. Example cURL call: `curl -kLsS -H \"Authorization: Token 529c30e6e04b3b546f2e073e879b75fdfa147c15\" https://localhost/api/v1`. AUTHENTICATION should be set to 'TOKEN'", "name": "Token", "in": "header" } }, "security": [ + { + "cert": [] + }, { "token": [] } @@ -1941,4 +1948,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/authentication/auth.py b/src/authentication/auth.py index 9d13e833..448a9d30 100644 --- a/src/authentication/auth.py +++ b/src/authentication/auth.py @@ -1,5 +1,6 @@ import logging import re + from django.conf import settings from django.contrib.auth import get_user_model from rest_framework import authentication, exceptions @@ -7,12 +8,12 @@ logger = logging.getLogger(__name__) token_auth_enabled = ( - "rest_framework.authentication.TokenAuthentication" - in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] + "rest_framework.authentication.TokenAuthentication" + in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] ) certificate_authentication_enabled = ( - "authentication.auth.CertificateAuthentication" - in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] + "authentication.auth.CertificateAuthentication" + in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] ) @@ -28,13 +29,15 @@ def authenticate(self, request): except user_model.DoesNotExist: raise exceptions.AuthenticationFailed("No matching username found!") except Exception: - raise exceptions.AuthenticationFailed("Error occurred during certificate authentication!") + raise exceptions.AuthenticationFailed( + "Error occurred during certificate authentication!" + ) return user, None return None, None def get_cn_from_dn(cert_dn): - p = re.compile("CN=(.*?)(?:,|\+|$)") + p = re.compile(r"CN=(.*?)(?:,|\+|$)") match = p.search(cert_dn) if not match: raise Exception("No CN found in certificate!") diff --git a/src/authentication/tests/test_cert_auth.py b/src/authentication/tests/test_cert_auth.py index 36312fa2..a041cccc 100644 --- a/src/authentication/tests/test_cert_auth.py +++ b/src/authentication/tests/test_cert_auth.py @@ -51,13 +51,17 @@ def test_no_client_cert_unauthorized_no_dn(live_server): @pytest.mark.django_db def test_client_cert_accepted(live_server, admin_user): client = RequestsClient() - response = client.get(f"{live_server.url}", headers=get_requests_ssl_dn_header("admin")) + response = client.get( + f"{live_server.url}", headers=get_requests_ssl_dn_header("admin") + ) assert response.status_code == 200 def test_mismatching_user_forbidden(live_server, admin_user): client = RequestsClient() - response = client.get(f"{live_server.url}", headers=get_requests_ssl_dn_header("user")) + response = client.get( + f"{live_server.url}", headers=get_requests_ssl_dn_header("user") + ) assert response.status_code == 403 assert "No matching username found!" in response.json()["detail"] @@ -174,4 +178,4 @@ def test_empty_dn_unauthorized(live_server, admin_user): "X-Ssl-Client-Dn": "", } response = client.get(f"{live_server.url}", headers=headers) - assert response.status_code == 403 \ No newline at end of file + assert response.status_code == 403 diff --git a/src/authentication/tests/test_token_auth.py b/src/authentication/tests/test_token_auth.py index f8f1d48b..e0948f74 100644 --- a/src/authentication/tests/test_token_auth.py +++ b/src/authentication/tests/test_token_auth.py @@ -161,7 +161,7 @@ def test_user_cannot_view_user_detail(settings, live_server, user_client, user): response = user_client.get( f"{live_server.url}{user_detail}", headers={"Authorization": f"Token {user.auth_token.key}"}, - **HTTPS_KWARG + **HTTPS_KWARG, ) assert response.status_code == 403 diff --git a/src/conftest.py b/src/conftest.py index acdc0b75..9897bcdc 100644 --- a/src/conftest.py +++ b/src/conftest.py @@ -67,6 +67,7 @@ def user_client(db, user): return client + @pytest.fixture def admin_client(db, admin_user): """A Django test client logged in as an admin user""" @@ -75,6 +76,7 @@ def admin_client(db, admin_user): return client + @pytest.fixture def alt_user(db): """A normal user.""" diff --git a/src/scheduler/scheduler.py b/src/scheduler/scheduler.py index 2b012397..332761f8 100644 --- a/src/scheduler/scheduler.py +++ b/src/scheduler/scheduler.py @@ -188,7 +188,7 @@ def _finalize_task_result(self, started, finished, status, detail): data=json.dumps(result_json), headers=headers, verify=verify_ssl, - cert=settings.PATH_TO_CLIENT_CERT + cert=settings.PATH_TO_CLIENT_CERT, ) self._callback_response_handler(response, tr) else: diff --git a/src/scheduler/tests/test_scheduler.py b/src/scheduler/tests/test_scheduler.py index 8d73d586..195708d1 100644 --- a/src/scheduler/tests/test_scheduler.py +++ b/src/scheduler/tests/test_scheduler.py @@ -420,8 +420,13 @@ def test_notification_failed_status_unknown_host(test_scheduler): entry = create_entry("t", 1, 1, 100, 5, "logger", callback_url) entry.save() token = entry.owner.auth_token - m.post(callback_url, request_headers={"Authorization": "Token " + str(token)}, text='Not Found', status_code=404) - + m.post( + callback_url, + request_headers={"Authorization": "Token " + str(token)}, + text="Not Found", + status_code=404, + ) + entry.refresh_from_db() print("entry = " + entry.name) s = test_scheduler diff --git a/src/sensor/migration_settings.py b/src/sensor/migration_settings.py index b63e0a95..7b658e05 100644 --- a/src/sensor/migration_settings.py +++ b/src/sensor/migration_settings.py @@ -252,45 +252,44 @@ # https://drf-yasg.readthedocs.io/en/stable/settings.html SWAGGER_SETTINGS = { - "SECURITY_DEFINITIONS": {}, + "SECURITY_DEFINITIONS": { + "cert": { + "type": "cert", + "description": ( + "Certificate based mutual TLS authentication. " + "AUTHENTICATION must be set to 'CERT'. " + "This is done by the client verifying the server certificate and the server verifying the client certificate. " + "The client certificate Common Name (CN) should contain the username of a user that exists in the database. " + "Client certificate verification is handled by NGINX. " + "For more information, see https://www.rfc-editor.org/rfc/rfc5246." + ), + }, + "token": { + "type": "apiKey", + "description": ( + "Tokens are automatically generated for all users. You can " + "view yours by going to your User Details view in the " + "browsable API at `/api/v1/users/me` and looking for the " + "`auth_token` key. New user accounts do not initially " + "have a password and so can not log in to the browsable API. " + "To set a password for a user (for testing purposes), an " + "admin can do that in the Sensor Configuration Portal, but " + "only the account's token should be stored and used for " + "general purpose API access. " + 'Example cURL call: `curl -kLsS -H "Authorization: Token' + ' 529c30e6e04b3b546f2e073e879b75fdfa147c15" ' + "https://localhost/api/v1`. " + "AUTHENTICATION should be set to 'TOKEN'" + ), + "name": "Token", + "in": "header", + } + }, "APIS_SORTER": "alpha", "OPERATIONS_SORTER": "method", "VALIDATOR_URL": None, } -if AUTHENTICATION == "JWT": - SWAGGER_SETTINGS["SECURITY_DEFINITIONS"]["oAuth2JWT"] = { - "type": "oauth2", - "description": ( - "OAuth2 authentication using resource owner password flow." - "This is done by verifing JWT bearer tokens signed with RS256 algorithm." - "The JWT_PUBLIC_KEY_FILE setting controls the public key used for signature verification." - "Only authorizes users who have an authority matching the REQUIRED_ROLE setting." - "For more information, see https://tools.ietf.org/html/rfc6749#section-4.3." - ), - "flows": {"password": {"scopes": {}}}, # scopes are not used - } -else: - SWAGGER_SETTINGS["SECURITY_DEFINITIONS"]["token"] = { - "type": "apiKey", - "description": ( - "Tokens are automatically generated for all users. You can " - "view yours by going to your User Details view in the " - "browsable API at `/api/v1/users/me` and looking for the " - "`auth_token` key. New user accounts do not initially " - "have a password and so can not log in to the browsable API. " - "To set a password for a user (for testing purposes), an " - "admin can do that in the Sensor Configuration Portal, but " - "only the account's token should be stored and used for " - "general purpose API access. " - 'Example cURL call: `curl -kLsS -H "Authorization: Token' - ' 529c30e6e04b3b546f2e073e879b75fdfa147c15" ' - "https://localhost/api/v1`" - ), - "name": "Token", - "in": "header", - } - # Database # https://docs.djangoproject.com/en/1.11/ref/settings/#databases @@ -379,7 +378,7 @@ PATH_TO_CLIENT_CERT = env("PATH_TO_CLIENT_CERT", default="") if PATH_TO_CLIENT_CERT != "": PATH_TO_CLIENT_CERT = path.join(CERTS_DIR, PATH_TO_CLIENT_CERT) -# Trusted Certificate Authority certificate to verify authserver and callback URL server certificate +# Trusted Certificate Authority certificate to verify callback URL server certificate PATH_TO_VERIFY_CERT = env("PATH_TO_VERIFY_CERT", default="") if PATH_TO_VERIFY_CERT != "": PATH_TO_VERIFY_CERT = path.join(CERTS_DIR, PATH_TO_VERIFY_CERT) diff --git a/src/sensor/runtime_settings.py b/src/sensor/runtime_settings.py index 22dcaf75..9f9dc89d 100644 --- a/src/sensor/runtime_settings.py +++ b/src/sensor/runtime_settings.py @@ -252,45 +252,44 @@ # https://drf-yasg.readthedocs.io/en/stable/settings.html SWAGGER_SETTINGS = { - "SECURITY_DEFINITIONS": {}, + "SECURITY_DEFINITIONS": { + "cert": { + "type": "cert", + "description": ( + "Certificate based mutual TLS authentication. " + "AUTHENTICATION must be set to 'CERT'. " + "This is done by the client verifying the server certificate and the server verifying the client certificate. " + "The client certificate Common Name (CN) should contain the username of a user that exists in the database. " + "Client certificate verification is handled by NGINX. " + "For more information, see https://www.rfc-editor.org/rfc/rfc5246." + ), + }, + "token": { + "type": "apiKey", + "description": ( + "Tokens are automatically generated for all users. You can " + "view yours by going to your User Details view in the " + "browsable API at `/api/v1/users/me` and looking for the " + "`auth_token` key. New user accounts do not initially " + "have a password and so can not log in to the browsable API. " + "To set a password for a user (for testing purposes), an " + "admin can do that in the Sensor Configuration Portal, but " + "only the account's token should be stored and used for " + "general purpose API access. " + 'Example cURL call: `curl -kLsS -H "Authorization: Token' + ' 529c30e6e04b3b546f2e073e879b75fdfa147c15" ' + "https://localhost/api/v1`. " + "AUTHENTICATION should be set to 'TOKEN'" + ), + "name": "Token", + "in": "header", + } + }, "APIS_SORTER": "alpha", "OPERATIONS_SORTER": "method", "VALIDATOR_URL": None, } -if AUTHENTICATION == "JWT": - SWAGGER_SETTINGS["SECURITY_DEFINITIONS"]["oAuth2JWT"] = { - "type": "oauth2", - "description": ( - "OAuth2 authentication using resource owner password flow." - "This is done by verifing JWT bearer tokens signed with RS256 algorithm." - "The JWT_PUBLIC_KEY_FILE setting controls the public key used for signature verification." - "Only authorizes users who have an authority matching the REQUIRED_ROLE setting." - "For more information, see https://tools.ietf.org/html/rfc6749#section-4.3." - ), - "flows": {"password": {"scopes": {}}}, # scopes are not used - } -else: - SWAGGER_SETTINGS["SECURITY_DEFINITIONS"]["token"] = { - "type": "apiKey", - "description": ( - "Tokens are automatically generated for all users. You can " - "view yours by going to your User Details view in the " - "browsable API at `/api/v1/users/me` and looking for the " - "`auth_token` key. New user accounts do not initially " - "have a password and so can not log in to the browsable API. " - "To set a password for a user (for testing purposes), an " - "admin can do that in the Sensor Configuration Portal, but " - "only the account's token should be stored and used for " - "general purpose API access. " - 'Example cURL call: `curl -kLsS -H "Authorization: Token' - ' 529c30e6e04b3b546f2e073e879b75fdfa147c15" ' - "https://localhost/api/v1`" - ), - "name": "Token", - "in": "header", - } - # Database # https://docs.djangoproject.com/en/1.11/ref/settings/#databases @@ -379,7 +378,7 @@ PATH_TO_CLIENT_CERT = env("PATH_TO_CLIENT_CERT", default="") if PATH_TO_CLIENT_CERT != "": PATH_TO_CLIENT_CERT = path.join(CERTS_DIR, PATH_TO_CLIENT_CERT) -# Trusted Certificate Authority certificate to verify authserver and callback URL server certificate +# Trusted Certificate Authority certificate to verify callback URL server certificate PATH_TO_VERIFY_CERT = env("PATH_TO_VERIFY_CERT", default="") if PATH_TO_VERIFY_CERT != "": PATH_TO_VERIFY_CERT = path.join(CERTS_DIR, PATH_TO_VERIFY_CERT) diff --git a/src/sensor/settings.py b/src/sensor/settings.py index c6fbc477..25c3549b 100644 --- a/src/sensor/settings.py +++ b/src/sensor/settings.py @@ -253,45 +253,44 @@ # https://drf-yasg.readthedocs.io/en/stable/settings.html SWAGGER_SETTINGS = { - "SECURITY_DEFINITIONS": {}, + "SECURITY_DEFINITIONS": { + "cert": { + "type": "cert", + "description": ( + "Certificate based mutual TLS authentication. " + "AUTHENTICATION must be set to 'CERT'. " + "This is done by the client verifying the server certificate and the server verifying the client certificate. " + "The client certificate Common Name (CN) should contain the username of a user that exists in the database. " + "Client certificate verification is handled by NGINX. " + "For more information, see https://www.rfc-editor.org/rfc/rfc5246." + ), + }, + "token": { + "type": "apiKey", + "description": ( + "Tokens are automatically generated for all users. You can " + "view yours by going to your User Details view in the " + "browsable API at `/api/v1/users/me` and looking for the " + "`auth_token` key. New user accounts do not initially " + "have a password and so can not log in to the browsable API. " + "To set a password for a user (for testing purposes), an " + "admin can do that in the Sensor Configuration Portal, but " + "only the account's token should be stored and used for " + "general purpose API access. " + 'Example cURL call: `curl -kLsS -H "Authorization: Token' + ' 529c30e6e04b3b546f2e073e879b75fdfa147c15" ' + "https://localhost/api/v1`. " + "AUTHENTICATION should be set to 'TOKEN'" + ), + "name": "Token", + "in": "header", + } + }, "APIS_SORTER": "alpha", "OPERATIONS_SORTER": "method", "VALIDATOR_URL": None, } -if AUTHENTICATION == "JWT": - SWAGGER_SETTINGS["SECURITY_DEFINITIONS"]["oAuth2JWT"] = { - "type": "oauth2", - "description": ( - "OAuth2 authentication using resource owner password flow." - "This is done by verifing JWT bearer tokens signed with RS256 algorithm." - "The JWT_PUBLIC_KEY_FILE setting controls the public key used for signature verification." - "Only authorizes users who have an authority matching the REQUIRED_ROLE setting." - "For more information, see https://tools.ietf.org/html/rfc6749#section-4.3." - ), - "flows": {"password": {"scopes": {}}}, # scopes are not used - } -else: - SWAGGER_SETTINGS["SECURITY_DEFINITIONS"]["token"] = { - "type": "apiKey", - "description": ( - "Tokens are automatically generated for all users. You can " - "view yours by going to your User Details view in the " - "browsable API at `/api/v1/users/me` and looking for the " - "`auth_token` key. New user accounts do not initially " - "have a password and so can not log in to the browsable API. " - "To set a password for a user (for testing purposes), an " - "admin can do that in the Sensor Configuration Portal, but " - "only the account's token should be stored and used for " - "general purpose API access. " - 'Example cURL call: `curl -kLsS -H "Authorization: Token' - ' 529c30e6e04b3b546f2e073e879b75fdfa147c15" ' - "https://localhost/api/v1`" - ), - "name": "Token", - "in": "header", - } - # Database # https://docs.djangoproject.com/en/1.11/ref/settings/#databases @@ -380,7 +379,7 @@ PATH_TO_CLIENT_CERT = env("PATH_TO_CLIENT_CERT", default="") if PATH_TO_CLIENT_CERT != "": PATH_TO_CLIENT_CERT = path.join(CERTS_DIR, PATH_TO_CLIENT_CERT) -# Trusted Certificate Authority certificate to verify authserver and callback URL server certificate +# Trusted Certificate Authority certificate to verify callback URL server certificate PATH_TO_VERIFY_CERT = env("PATH_TO_VERIFY_CERT", default="") if PATH_TO_VERIFY_CERT != "": PATH_TO_VERIFY_CERT = path.join(CERTS_DIR, PATH_TO_VERIFY_CERT) diff --git a/src/sensor/tests/certificate_auth_client.py b/src/sensor/tests/certificate_auth_client.py index 098555b4..fabc0592 100644 --- a/src/sensor/tests/certificate_auth_client.py +++ b/src/sensor/tests/certificate_auth_client.py @@ -1,15 +1,18 @@ -from django.test.client import Client, MULTIPART_CONTENT from django.conf import settings from django.core.serializers.json import DjangoJSONEncoder +from django.test.client import MULTIPART_CONTENT, Client from sensor.tests.utils import get_http_request_ssl_dn_header cert_auth_enabled = settings.AUTHENTICATION == "CERT" + class CertificateAuthClient(Client): """Adds SSL DN header if certificate authentication is being used""" - def __init__(self, enforce_csrf_checks=False, json_encoder=DjangoJSONEncoder, **defaults) -> None: + def __init__( + self, enforce_csrf_checks=False, json_encoder=DjangoJSONEncoder, **defaults + ) -> None: super().__init__(enforce_csrf_checks, json_encoder=json_encoder, **defaults) self.username = "" @@ -24,29 +27,79 @@ def get(self, path, data=None, follow=False, secure=False, **extra): assert secure return super().get(path, data, follow, secure, **self.get_kwargs(extra)) - def post(self, path, data=None, content_type=MULTIPART_CONTENT, follow=False, secure=False, **extra): + def post( + self, + path, + data=None, + content_type=MULTIPART_CONTENT, + follow=False, + secure=False, + **extra + ): assert secure - return super().post(path, data, content_type, follow, secure, **self.get_kwargs(extra)) + return super().post( + path, data, content_type, follow, secure, **self.get_kwargs(extra) + ) def head(self, path, data=None, follow=False, secure=False, **extra): assert secure return super().head(path, data, follow, secure, **self.get_kwargs(extra)) - def options(self, path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra): + def options( + self, + path, + data="", + content_type="application/octet-stream", + follow=False, + secure=False, + **extra + ): assert secure - return super().options(self, path, data, content_type, follow, secure, **self.get_kwargs(extra)) + return super().options( + self, path, data, content_type, follow, secure, **self.get_kwargs(extra) + ) - def put(self, path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra): + def put( + self, + path, + data="", + content_type="application/octet-stream", + follow=False, + secure=False, + **extra + ): assert secure - return super().put(path, data, content_type, follow, secure, **self.get_kwargs(extra)) + return super().put( + path, data, content_type, follow, secure, **self.get_kwargs(extra) + ) - def patch(self, path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra): + def patch( + self, + path, + data="", + content_type="application/octet-stream", + follow=False, + secure=False, + **extra + ): assert secure - return super().patch(path, data, content_type, follow, secure, **self.get_kwargs(extra)) + return super().patch( + path, data, content_type, follow, secure, **self.get_kwargs(extra) + ) - def delete(self, path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra): + def delete( + self, + path, + data="", + content_type="application/octet-stream", + follow=False, + secure=False, + **extra + ): assert secure - return super().delete(path, data, content_type, follow, secure, **self.get_kwargs(extra)) + return super().delete( + path, data, content_type, follow, secure, **self.get_kwargs(extra) + ) def trace(self, path, follow=False, secure=False, **extra): assert secure diff --git a/src/sensor/tests/utils.py b/src/sensor/tests/utils.py index 39f84203..aee48d73 100644 --- a/src/sensor/tests/utils.py +++ b/src/sensor/tests/utils.py @@ -15,11 +15,13 @@ def validate_response(response, expected_code=None): rjson = response.json() return rjson + def get_requests_ssl_dn_header(common_name): return { "X-Ssl-Client-Dn": f"C=TC,ST=test_state,L=test_locality,O=test_org,OU=test_ou,CN={common_name}", } + def get_http_request_ssl_dn_header(common_name): return { "HTTP_X-SSL-CLIENT-DN": f"C=TC,ST=test_state,L=test_locality,O=test_org,OU=test_ou,CN={common_name}", diff --git a/src/test_utils/task_test_utils.py b/src/test_utils/task_test_utils.py index 78ce02c2..d5f73077 100644 --- a/src/test_utils/task_test_utils.py +++ b/src/test_utils/task_test_utils.py @@ -187,7 +187,7 @@ def update_result_detail(client, schedule_entry_name, task_id, new_acquisition): "data": json.dumps(new_acquisition), "content_type": "application/json", "wsgi.url_scheme": "https", - "secure": True + "secure": True, } return client.put(url, **kwargs) From 6b21fc623a31d371dbda5731cf4f10588a4ca800 Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Fri, 27 Jan 2023 15:02:50 -0700 Subject: [PATCH 13/32] remove oauth/jwt from isort; autoformat settings, openapi.json --- docs/openapi.json | 2 +- src/.isort.cfg | 2 +- src/sensor/migration_settings.py | 2 +- src/sensor/runtime_settings.py | 2 +- src/sensor/settings.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/openapi.json b/docs/openapi.json index 602ce87d..fe9fd1ce 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1948,4 +1948,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/.isort.cfg b/src/.isort.cfg index d4c96609..29e00ce5 100644 --- a/src/.isort.cfg +++ b/src/.isort.cfg @@ -4,4 +4,4 @@ include_trailing_comma=True force_grid_wrap=0 use_parentheses=True line_length=88 -known_third_party=cryptography,django,drf_yasg,environs,its_preselector,jsonfield,jwt,numpy,oauthlib,pytest,requests,requests_mock,requests_oauthlib,rest_framework,scos_actions,sigmf +known_third_party=cryptography,django,drf_yasg,environs,its_preselector,jsonfield,numpy,pytest,requests,requests_mock,rest_framework,scos_actions,sigmf diff --git a/src/sensor/migration_settings.py b/src/sensor/migration_settings.py index 7b658e05..5b39d8a3 100644 --- a/src/sensor/migration_settings.py +++ b/src/sensor/migration_settings.py @@ -283,7 +283,7 @@ ), "name": "Token", "in": "header", - } + }, }, "APIS_SORTER": "alpha", "OPERATIONS_SORTER": "method", diff --git a/src/sensor/runtime_settings.py b/src/sensor/runtime_settings.py index 9f9dc89d..6159c571 100644 --- a/src/sensor/runtime_settings.py +++ b/src/sensor/runtime_settings.py @@ -283,7 +283,7 @@ ), "name": "Token", "in": "header", - } + }, }, "APIS_SORTER": "alpha", "OPERATIONS_SORTER": "method", diff --git a/src/sensor/settings.py b/src/sensor/settings.py index 25c3549b..a328c24a 100644 --- a/src/sensor/settings.py +++ b/src/sensor/settings.py @@ -284,7 +284,7 @@ ), "name": "Token", "in": "header", - } + }, }, "APIS_SORTER": "alpha", "OPERATIONS_SORTER": "method", From 89a3b4884fe3d6c86b542591a65230ceda2b09f0 Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Mon, 30 Jan 2023 13:17:40 -0700 Subject: [PATCH 14/32] remove old oauth params --- docker-compose.yml | 2 -- env.template | 3 --- 2 files changed, 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7b061cc3..6d36e758 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,8 +38,6 @@ services: - AUTHENTICATION - CALLBACK_AUTHENTICATION - CALLBACK_SSL_VERIFICATION - - CLIENT_ID - - CLIENT_SECRET - DEBUG - DOCKER_TAG - DOMAINS diff --git a/env.template b/env.template index 7550cc82..d7420f00 100644 --- a/env.template +++ b/env.template @@ -61,9 +61,6 @@ BASE_IMAGE=ghcr.io/ntia/scos-tekrsa/tekrsa_usb:0.2.1 # Set to CERT for certificate authentication CALLBACK_AUTHENTICATION=TOKEN -CLIENT_ID=sensor01.sms.internal -CLIENT_SECRET=sensor-secret - # Sensor certificate with private key used as client cert PATH_TO_CLIENT_CERT=sensor01.pem # Trusted Certificate Authority certificate to verify authserver and callback URL server certificate From 6d1a25696db9d3debb147eecb8c1b419cb1f5617 Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Tue, 31 Jan 2023 08:13:32 -0700 Subject: [PATCH 15/32] remove login url when using cert auth --- src/sensor/urls.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/sensor/urls.py b/src/sensor/urls.py index b04a8d00..88b7cb7c 100644 --- a/src/sensor/urls.py +++ b/src/sensor/urls.py @@ -23,12 +23,13 @@ from django.views.generic import RedirectView from rest_framework.urlpatterns import format_suffix_patterns -from . import settings +from django.conf import settings from .views import api_v1_root, schema_view # Matches api/v1, api/v2, etc... API_PREFIX = r"^api/(?Pv[0-9]+)/" DEFAULT_API_VERSION = settings.REST_FRAMEWORK["DEFAULT_VERSION"] +AUTHENTICATION = settings.AUTHENTICATION api_urlpatterns = format_suffix_patterns( ( @@ -57,5 +58,7 @@ path("admin/", admin.site.urls), path("api/", RedirectView.as_view(url="/api/{}/".format(DEFAULT_API_VERSION))), re_path(API_PREFIX, include(api_urlpatterns)), - path("api/auth/", include("rest_framework.urls")), ] + +if AUTHENTICATION != "CERT": + urlpatterns.append(path("api/auth/", include("rest_framework.urls"))) From a5fd9de275fe79c08fb9e51bb7d10a43509ea33c Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Tue, 31 Jan 2023 12:57:20 -0700 Subject: [PATCH 16/32] update last_login, nginx check ssl_client_verify --- nginx/conf.template | 4 ++++ src/authentication/auth.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/nginx/conf.template b/nginx/conf.template index f39db8c7..e561e3b7 100644 --- a/nginx/conf.template +++ b/nginx/conf.template @@ -47,6 +47,10 @@ server { # Pass off requests to Gunicorn location @proxy_to_wsgi_server { + if ($ssl_client_verify != SUCCESS) { + return 403; + } + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; proxy_set_header Host $http_host; diff --git a/src/authentication/auth.py b/src/authentication/auth.py index 448a9d30..09507302 100644 --- a/src/authentication/auth.py +++ b/src/authentication/auth.py @@ -1,3 +1,4 @@ +import datetime import logging import re @@ -26,6 +27,8 @@ def authenticate(self, request): try: cn = get_cn_from_dn(cert_dn) user = user_model.objects.get(username=cn) + user.last_login = datetime.datetime.now() + user.save() except user_model.DoesNotExist: raise exceptions.AuthenticationFailed("No matching username found!") except Exception: From fcbc88ef9dc7d888bd8b04a97780e4e2abb129b5 Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Wed, 29 Mar 2023 10:08:02 -0600 Subject: [PATCH 17/32] fix merge issue for requirements-dev.in --- src/requirements-dev.in | 1 - 1 file changed, 1 deletion(-) diff --git a/src/requirements-dev.in b/src/requirements-dev.in index e8dba275..7f6d00c8 100644 --- a/src/requirements-dev.in +++ b/src/requirements-dev.in @@ -1,6 +1,5 @@ -rrequirements.txt -cryptography>=36.0, <39.0 pre-commit>=2.0, <3.0 pytest-cov>=3.0, <4.0 pytest-django>=4.0, <5.0 From bfcc86c7c2365fd3f75086a22d64c8838de10405 Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Wed, 5 Apr 2023 09:05:16 -0600 Subject: [PATCH 18/32] fix comment --- src/tasks/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks/views.py b/src/tasks/views.py index 330bba8f..56d63048 100644 --- a/src/tasks/views.py +++ b/src/tasks/views.py @@ -217,7 +217,7 @@ def build_sigmf_archive(fileobj, schedule_entry_name, acquisitions): raw_data = acq.data.read() data = fernet.decrypt(raw_data) del raw_data - tmpdata.write(data) # decrypted data will be stored on disk in tmp file + tmpdata.write(data) # decrypted data stored in file del data else: tmpdata.write(acq.data.read()) From 7409e937e4c07ccd4ee0158975f496d9c839e15f Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Mon, 10 Apr 2023 14:59:54 -0600 Subject: [PATCH 19/32] update authentication information in readme --- README.md | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index a59cf7c7..cc2aa3d7 100644 --- a/README.md +++ b/README.md @@ -384,10 +384,9 @@ or using Django Rest Framework Token Authentication. #### Django Rest Framework Token Authentication -This is the default authentication method. To enable Django Rest Framework -Authentication, make sure `AUTHENTICATION` is set to `TOKEN` in the environment file -(this will be enabled if `AUTHENTICATION` set to anything other -than `CERT`). +To enable Django Rest Framework token and session authentication, make sure +`AUTHENTICATION` is set to `TOKEN` in the environment file (this will be enabled if +`AUTHENTICATION` set to anything other than `CERT`). A token is automatically created for each user. Django Rest Framework Token Authentication will check that the token in the Authorization header ("Token " + @@ -395,7 +394,8 @@ token) matches a user's token. #### Certificate Authentication -To enable Certificate Authentication, set `AUTHENTICATION` to `CERT` in the environment +This is the default authentication method. To enable Certificate Authentication, make +sure `AUTHENTICATION` is set to `CERT` in the environment file. To authenticate, the client will need to send a trusted client certificate. The Common Name must match the username of a user in the database. @@ -504,11 +504,31 @@ or client.pfx when communicating with the API programmatically. ###### Configure scos-sensor -The Nginx web server can be set to require client certificates (mutual TLS). This can -optionally be enabled. To require client certificates, make sure `ssl_verify_client` is -set to `on` in the [Nginx configuration file](nginx/conf.template). If you -use OCSP, also uncomment `ssl_ocsp on;`. Additional configuration may be needed for -Nginx to check certificate revocation lists (CRL). +The Nginx web server is configured by default to require client certificates (mutual +TLS). To require client certificates, make sure `ssl_verify_client` is set to `on` in +the [Nginx configuration file](nginx/conf.template). Comment out this line or set to +`off` to disable client certificates. This can also be set to `optional` or +`optional_no_ca`, but if a client certificate is not provided, scos-sensor +`AUTHENTICATION` setting must be set to `TOKEN` which requires a token for the API or a +username and password for the browsable API. If you use OCSP, also uncomment +`ssl_ocsp on;`. Additional configuration may be needed for Nginx to check certificate +revocation lists (CRL). Adjust the other Nginx parameters, such as `ssl_verify_depth`, +as desired. See the +[Nginx documentation](https://nginx.org/en/docs/http/ngx_http_ssl_module.html) for +more information about configuring Nginx. + +To disable client certificate authentication, comment out the +following in [nginx/conf.template](nginx/conf.template): + +``` +ssl_client_certificate /etc/ssl/certs/ca.crt; +ssl_verify_client on; +ssl_ocsp on; +... + if ($ssl_client_verify != SUCCESS) { # under location @proxy_to_wsgi_server { + return 403; + } +``` Copy the server certificate and server private key (sensor01_combined.pem) to `scos-sensor/configs/certs`. Then set `SSL_CERT_PATH` and `SSL_KEY_PATH` (in the From 761bcdc2fa447cd87e99a4c5c98e8d45b160ab77 Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Mon, 17 Apr 2023 09:30:22 -0600 Subject: [PATCH 20/32] keep logout url for page timeout --- src/sensor/urls.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sensor/urls.py b/src/sensor/urls.py index 88b7cb7c..d44326cb 100644 --- a/src/sensor/urls.py +++ b/src/sensor/urls.py @@ -17,13 +17,13 @@ """ +from django.conf import settings from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path, re_path from django.views.generic import RedirectView from rest_framework.urlpatterns import format_suffix_patterns -from django.conf import settings from .views import api_v1_root, schema_view # Matches api/v1, api/v2, etc... @@ -60,5 +60,5 @@ re_path(API_PREFIX, include(api_urlpatterns)), ] -if AUTHENTICATION != "CERT": - urlpatterns.append(path("api/auth/", include("rest_framework.urls"))) +# logout/login does not do anything if AUTHENTICATION is set to "CERT" +urlpatterns.append(path("api/auth/", include("rest_framework.urls"))) From 38d369f37f965aeb0ada99305719c64c7530adcd Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Wed, 27 Sep 2023 09:27:20 -0600 Subject: [PATCH 21/32] update dependencies, fix dependabot alert --- .pre-commit-config.yaml | 6 +- src/requirements-dev.txt | 160 ++++++++++++++++++++------------------- src/requirements.in | 1 + src/requirements.txt | 97 ++++++++++++------------ 4 files changed, 135 insertions(+), 129 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b33e7e19..02a8e5ce 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/asottile/pyupgrade - rev: v3.4.0 + rev: v3.13.0 hooks: - id: pyupgrade args: ["--py38-plus"] @@ -31,12 +31,12 @@ repos: types: [file, python] args: ["--profile", "black", "--filter-files", "--gitignore"] - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 23.9.1 hooks: - id: black types: [file, python] - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.35.0 + rev: v0.37.0 hooks: - id: markdownlint types: [file, markdown] diff --git a/src/requirements-dev.txt b/src/requirements-dev.txt index 4da1a068..110be9e4 100644 --- a/src/requirements-dev.txt +++ b/src/requirements-dev.txt @@ -16,21 +16,21 @@ aiosignal==1.3.1 # -r requirements.txt # aiohttp # ray -asgiref==3.6.0 +asgiref==3.7.2 # via # -r requirements.txt # django -async-timeout==4.0.2 +async-timeout==4.0.3 # via aiohttp -attrs==22.2.0 +attrs==23.1.0 # via # -r requirements.txt # aiohttp # jsonschema - # pytest + # referencing blessed==1.20.0 # via gpustat -cachetools==5.3.0 +cachetools==5.3.1 # via # google-auth # tox @@ -42,16 +42,16 @@ cffi==1.15.1 # via # -r requirements.txt # cryptography -cfgv==3.3.1 +cfgv==3.4.0 # via pre-commit -chardet==5.1.0 +chardet==5.2.0 # via tox -charset-normalizer==3.0.1 +charset-normalizer==3.2.0 # via # -r requirements.txt # aiohttp # requests -click==8.1.3 +click==8.1.7 # via # -r requirements.txt # ray @@ -59,16 +59,7 @@ colorama==0.4.6 # via tox colorful==0.5.5 # via ray -coreapi==2.3.3 - # via - # -r requirements.txt - # drf-yasg -coreschema==0.0.4 - # via - # -r requirements.txt - # coreapi - # drf-yasg -coverage[toml]==7.2.1 +coverage[toml]==7.3.1 # via pytest-cov cryptography==41.0.4 # via -r requirements.txt @@ -76,9 +67,9 @@ defusedxml==0.7.1 # via # -r requirements.txt # its-preselector -distlib==0.3.6 +distlib==0.3.7 # via virtualenv -django==3.2.20 +django==3.2.21 # via # -r requirements.txt # django-session-timeout @@ -92,7 +83,7 @@ djangorestframework==3.14.0 # via # -r requirements.txt # drf-yasg -drf-yasg==1.21.5 +drf-yasg==1.21.7 # via -r requirements.txt environs==9.5.0 # via @@ -101,39 +92,44 @@ environs==9.5.0 # scos-tekrsa exceptiongroup==1.1.3 # via pytest -filelock==3.9.0 +filelock==3.12.4 # via # -r requirements.txt # ray # tox # virtualenv -frozenlist==1.3.3 +frozenlist==1.4.0 # via # -r requirements.txt # aiohttp # aiosignal # ray -google-api-core==2.11.0 +google-api-core==2.12.0 # via opencensus -google-auth==2.17.3 +google-auth==2.23.1 # via google-api-core -googleapis-common-protos==1.59.0 +googleapis-common-protos==1.60.0 # via google-api-core -gpustat==1.1 +gpustat==1.1.1 # via ray -grpcio==1.51.3 +grpcio==1.58.0 # via # -r requirements.txt # ray gunicorn==20.1.0 # via -r requirements.txt -identify==2.5.18 +identify==2.5.29 # via pre-commit idna==3.4 # via # -r requirements.txt # requests # yarl +importlib-resources==6.1.0 + # via + # -r requirements.txt + # jsonschema + # jsonschema-specifications inflection==0.5.1 # via # -r requirements.txt @@ -144,33 +140,25 @@ its-preselector @ git+https://github.com/NTIA/Preselector@3.0.2 # via # -r requirements.txt # scos-actions -itypes==1.2.0 - # via - # -r requirements.txt - # coreapi -jinja2==3.1.2 - # via - # -r requirements.txt - # coreschema jsonfield==3.1.0 # via -r requirements.txt -jsonschema==3.2.0 +jsonschema==4.19.1 # via # -r requirements.txt # ray -markupsafe==2.1.2 +jsonschema-specifications==2023.7.1 # via # -r requirements.txt - # jinja2 -marshmallow==3.19.0 + # jsonschema +marshmallow==3.20.1 # via # -r requirements.txt # environs -msgpack==1.0.5 +msgpack==1.0.6 # via # -r requirements.txt # ray -msgspec==0.16.0 +msgspec==0.18.2 # via # -r requirements.txt # scos-actions @@ -178,13 +166,13 @@ multidict==6.0.4 # via # aiohttp # yarl -nodeenv==1.7.0 +nodeenv==1.8.0 # via pre-commit -numexpr==2.8.4 +numexpr==2.8.6 # via # -r requirements.txt # scos-actions -numpy==1.24.2 +numpy==1.24.4 # via # -r requirements.txt # numexpr @@ -193,13 +181,13 @@ numpy==1.24.2 # scos-actions # sigmf # tekrsa-api-wrap -nvidia-ml-py==11.525.112 +nvidia-ml-py==12.535.108 # via gpustat -opencensus==0.11.2 +opencensus==0.11.3 # via ray opencensus-context==0.1.3 # via opencensus -packaging==23.0 +packaging==23.1 # via # -r requirements.txt # drf-yasg @@ -208,19 +196,23 @@ packaging==23.0 # pytest # ray # tox -platformdirs==3.0.0 +pkgutil-resolve-name==1.3.10 + # via + # -r requirements.txt + # jsonschema +platformdirs==3.10.0 # via # tox # virtualenv -pluggy==1.0.0 +pluggy==1.3.0 # via # pytest # tox -pre-commit==3.3.3 +pre-commit==3.4.0 # via -r requirements-dev.in -prometheus-client==0.13.1 +prometheus-client==0.17.1 # via ray -protobuf==4.23.3 +protobuf==4.24.3 # via # -r requirements.txt # google-api-core @@ -231,7 +223,7 @@ psutil==5.9.5 # -r requirements.txt # gpustat # scos-actions -psycopg2-binary==2.9.5 +psycopg2-binary==2.9.7 # via -r requirements.txt py-spy==0.3.14 # via ray @@ -245,15 +237,11 @@ pycparser==2.21 # via # -r requirements.txt # cffi -pydantic==1.10.7 +pydantic==1.10.12 # via ray -pyproject-api==1.5.0 +pyproject-api==1.5.4 # via tox -pyrsistent==0.19.3 - # via - # -r requirements.txt - # jsonschema -pytest==7.2.1 +pytest==7.4.2 # via # pytest-cov # pytest-django @@ -265,11 +253,11 @@ python-dateutil==2.8.2 # via # -r requirements.txt # scos-actions -python-dotenv==0.21.1 +python-dotenv==1.0.0 # via # -r requirements.txt # environs -pytz==2022.7.1 +pytz==2023.3.post1 # via # -r requirements.txt # django @@ -278,29 +266,38 @@ pytz==2022.7.1 pyyaml==6.0.1 # via # -r requirements.txt + # drf-yasg # pre-commit # ray -ray[default]==2.6.3 +ray[default]==2.7.0 # via # -r requirements-dev.in # -r requirements.txt # scos-actions +referencing==0.30.2 + # via + # -r requirements.txt + # jsonschema + # jsonschema-specifications requests==2.31.0 # via # -r requirements.txt - # coreapi # google-api-core # its-preselector # ray # requests-mock -requests-mock==1.10.0 +requests-mock==1.11.0 # via -r requirements.txt +rpds-py==0.10.3 + # via + # -r requirements.txt + # jsonschema + # referencing rsa==4.9 # via google-auth -ruamel-yaml==0.17.21 +ruamel-yaml==0.17.32 # via # -r requirements.txt - # drf-yasg # scos-actions ruamel-yaml-clib==0.2.7 # via @@ -325,12 +322,10 @@ six==1.16.0 # -r requirements.txt # blessed # django-session-timeout - # google-auth - # jsonschema # python-dateutil # requests-mock # sigmf -smart-open==6.3.0 +smart-open==6.4.0 # via ray sqlparse==0.4.4 # via @@ -346,20 +341,23 @@ tomli==2.0.1 # pyproject-api # pytest # tox -tox==4.4.6 +tox==4.5.1.1 # via -r requirements-dev.in -typing-extensions==4.5.0 - # via pydantic +typing-extensions==4.8.0 + # via + # -r requirements.txt + # asgiref + # pydantic uritemplate==4.1.1 # via # -r requirements.txt - # coreapi # drf-yasg -urllib3==1.26.14 +urllib3==2.0.5 # via # -r requirements.txt + # google-auth # requests -virtualenv==20.20.0 +virtualenv==20.21.0 # via # pre-commit # ray @@ -368,6 +366,10 @@ wcwidth==0.2.6 # via blessed yarl==1.9.2 # via aiohttp +zipp==3.17.0 + # via + # -r requirements.txt + # importlib-resources # The following packages are considered to be unsafe in a requirements file: # setuptools diff --git a/src/requirements.in b/src/requirements.in index 16e860ab..65197d54 100644 --- a/src/requirements.in +++ b/src/requirements.in @@ -17,3 +17,4 @@ scos_tekrsa @ git+https://github.com/NTIA/scos-tekrsa@3.1.3 # higher minimum patch version than the dependencies which require them. # This is done to ensure the inclusion of specific security patches. pyyaml>=5.4.0 # CVE-2020-14343 +grpcio>=1.53.0 # CVE-2023-32732, CVE-2023-32731, CVE-2023-1428 \ No newline at end of file diff --git a/src/requirements.txt b/src/requirements.txt index 0a30f05f..ddf2a625 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -6,29 +6,25 @@ # aiosignal==1.3.1 # via ray -asgiref==3.6.0 +asgiref==3.7.2 # via django -attrs==22.2.0 - # via jsonschema +attrs==23.1.0 + # via + # jsonschema + # referencing certifi==2023.7.22 # via requests cffi==1.15.1 # via cryptography -charset-normalizer==3.0.1 +charset-normalizer==3.2.0 # via requests -click==8.1.3 +click==8.1.7 # via ray -coreapi==2.3.3 - # via drf-yasg -coreschema==0.0.4 - # via - # coreapi - # drf-yasg cryptography==41.0.4 # via -r requirements.in defusedxml==0.7.1 # via its-preselector -django==3.2.20 +django==3.2.21 # via # -r requirements.in # django-session-timeout @@ -42,50 +38,50 @@ djangorestframework==3.14.0 # via # -r requirements.in # drf-yasg -drf-yasg==1.21.5 +drf-yasg==1.21.7 # via -r requirements.in environs==9.5.0 # via # -r requirements.in # scos-actions # scos-tekrsa -filelock==3.9.0 +filelock==3.12.4 # via # -r requirements.in # ray -frozenlist==1.3.3 +frozenlist==1.4.0 # via # aiosignal # ray -grpcio==1.51.3 - # via ray +grpcio==1.58.0 + # via -r requirements.in gunicorn==20.1.0 # via -r requirements.in idna==3.4 # via requests +importlib-resources==6.1.0 + # via + # jsonschema + # jsonschema-specifications inflection==0.5.1 # via drf-yasg its-preselector @ git+https://github.com/NTIA/Preselector@3.0.2 # via scos-actions -itypes==1.2.0 - # via coreapi -jinja2==3.1.2 - # via coreschema jsonfield==3.1.0 # via -r requirements.in -jsonschema==3.2.0 +jsonschema==4.19.1 # via ray -markupsafe==2.1.2 - # via jinja2 -marshmallow==3.19.0 +jsonschema-specifications==2023.7.1 + # via jsonschema +marshmallow==3.20.1 # via environs -msgpack==1.0.5 +msgpack==1.0.6 # via ray -msgspec==0.16.0 +msgspec==0.18.2 # via scos-actions -numexpr==2.8.4 +numexpr==2.8.6 # via scos-actions -numpy==1.24.2 +numpy==1.24.4 # via # numexpr # ray @@ -93,27 +89,27 @@ numpy==1.24.2 # scos-actions # sigmf # tekrsa-api-wrap -packaging==23.0 +packaging==23.1 # via # -r requirements.in # drf-yasg # marshmallow # ray -protobuf==4.23.3 +pkgutil-resolve-name==1.3.10 + # via jsonschema +protobuf==4.24.3 # via ray psutil==5.9.5 # via scos-actions -psycopg2-binary==2.9.5 +psycopg2-binary==2.9.7 # via -r requirements.in pycparser==2.21 # via cffi -pyrsistent==0.19.3 - # via jsonschema python-dateutil==2.8.2 # via scos-actions -python-dotenv==0.21.1 +python-dotenv==1.0.0 # via environs -pytz==2022.7.1 +pytz==2023.3.post1 # via # django # djangorestframework @@ -121,21 +117,27 @@ pytz==2022.7.1 pyyaml==6.0.1 # via # -r requirements.in + # drf-yasg # ray -ray==2.6.3 +ray==2.7.0 # via scos-actions +referencing==0.30.2 + # via + # jsonschema + # jsonschema-specifications requests==2.31.0 # via - # coreapi # its-preselector # ray # requests-mock -requests-mock==1.10.0 +requests-mock==1.11.0 # via -r requirements.in -ruamel-yaml==0.17.21 +rpds-py==0.10.3 # via - # drf-yasg - # scos-actions + # jsonschema + # referencing +ruamel-yaml==0.17.32 + # via scos-actions ruamel-yaml-clib==0.2.7 # via ruamel-yaml scipy==1.10.1 @@ -151,7 +153,6 @@ sigmf @ git+https://github.com/NTIA/SigMF@multi-recording-archive six==1.16.0 # via # django-session-timeout - # jsonschema # python-dateutil # requests-mock # sigmf @@ -159,12 +160,14 @@ sqlparse==0.4.4 # via django tekrsa-api-wrap==1.3.2 # via scos-tekrsa +typing-extensions==4.8.0 + # via asgiref uritemplate==4.1.1 - # via - # coreapi - # drf-yasg -urllib3==1.26.14 + # via drf-yasg +urllib3==2.0.5 # via requests +zipp==3.17.0 + # via importlib-resources # The following packages are considered to be unsafe in a requirements file: # setuptools From a04ea354be6dfa055a9925ca049f6f304d4bf5ad Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Wed, 27 Sep 2023 09:30:52 -0600 Subject: [PATCH 22/32] formatting updates --- README.md | 2 +- docs/openapi.json | 42 +++++++++++++++++----------------- src/requirements-dev.in | 2 +- src/requirements.in | 2 +- src/sensor/runtime_settings.py | 1 - 5 files changed, 24 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 4de7027c..51cffb9e 100644 --- a/README.md +++ b/README.md @@ -518,7 +518,7 @@ more information about configuring Nginx. To disable client certificate authentication, comment out the following in [nginx/conf.template](nginx/conf.template): -``` +```text ssl_client_certificate /etc/ssl/certs/ca.crt; ssl_verify_client on; ssl_ocsp on; diff --git a/docs/openapi.json b/docs/openapi.json index fe9fd1ce..feee0bb5 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -649,13 +649,13 @@ }, "parameters": [ { - "name": "format", + "name": "schedule_entry_name", "in": "path", "required": true, "type": "string" }, { - "name": "schedule_entry_name", + "name": "format", "in": "path", "required": true, "type": "string" @@ -759,12 +759,6 @@ ] }, "parameters": [ - { - "name": "format", - "in": "path", - "required": true, - "type": "string" - }, { "name": "schedule_entry_name", "in": "path", @@ -777,6 +771,12 @@ "description": "The id of the task relative to the result", "required": true, "type": "integer" + }, + { + "name": "format", + "in": "path", + "required": true, + "type": "string" } ] }, @@ -811,12 +811,6 @@ ] }, "parameters": [ - { - "name": "format", - "in": "path", - "required": true, - "type": "string" - }, { "name": "schedule_entry_name", "in": "path", @@ -829,6 +823,12 @@ "description": "The id of the task relative to the result", "required": true, "type": "integer" + }, + { + "name": "format", + "in": "path", + "required": true, + "type": "string" } ] }, @@ -918,13 +918,13 @@ }, "parameters": [ { - "name": "format", + "name": "schedule_entry_name", "in": "path", "required": true, "type": "string" }, { - "name": "schedule_entry_name", + "name": "format", "in": "path", "required": true, "type": "string" @@ -1577,17 +1577,17 @@ }, "parameters": [ { - "name": "format", + "name": "id", "in": "path", + "description": "A unique integer value identifying this user.", "required": true, - "type": "string" + "type": "integer" }, { - "name": "id", + "name": "format", "in": "path", - "description": "A unique integer value identifying this user.", "required": true, - "type": "integer" + "type": "string" } ] }, diff --git a/src/requirements-dev.in b/src/requirements-dev.in index cf2964ad..7a80f7f1 100644 --- a/src/requirements-dev.in +++ b/src/requirements-dev.in @@ -9,4 +9,4 @@ tox>=4.0,<5.0 # The following are sub-dependencies for which SCOS Sensor enforces a # higher minimum patch version than the dependencies which require them. # This is done to ensure the inclusion of specific security patches. -aiohttp>=3.8.5 # CVE-2023-37276 \ No newline at end of file +aiohttp>=3.8.5 # CVE-2023-37276 diff --git a/src/requirements.in b/src/requirements.in index 65197d54..7d9924e9 100644 --- a/src/requirements.in +++ b/src/requirements.in @@ -17,4 +17,4 @@ scos_tekrsa @ git+https://github.com/NTIA/scos-tekrsa@3.1.3 # higher minimum patch version than the dependencies which require them. # This is done to ensure the inclusion of specific security patches. pyyaml>=5.4.0 # CVE-2020-14343 -grpcio>=1.53.0 # CVE-2023-32732, CVE-2023-32731, CVE-2023-1428 \ No newline at end of file +grpcio>=1.53.0 # CVE-2023-32732, CVE-2023-32731, CVE-2023-1428 diff --git a/src/sensor/runtime_settings.py b/src/sensor/runtime_settings.py index a427970c..d7c3f47b 100644 --- a/src/sensor/runtime_settings.py +++ b/src/sensor/runtime_settings.py @@ -39,7 +39,6 @@ SCOS_SENSOR_GIT_TAG = env("SCOS_SENSOR_GIT_TAG", default="Unknown") if not DOCKER_TAG or DOCKER_TAG == "latest": - VERSION_STRING = GIT_BRANCH else: VERSION_STRING = DOCKER_TAG From 457e38cad1a1a5e58fa0e82d7de0943eff9a2333 Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Tue, 24 Oct 2023 10:49:29 -0600 Subject: [PATCH 23/32] Update env.template remove SENTRY_DSN which is no longer used --- env.template | 3 --- 1 file changed, 3 deletions(-) diff --git a/env.template b/env.template index c6bc3abc..62b5275b 100644 --- a/env.template +++ b/env.template @@ -45,9 +45,6 @@ ADMIN_PASSWORD=password # `openssl rand -base64 12` POSTGRES_PASSWORD="$(python3 -c 'import secrets; import base64; print(base64.b64encode(secrets.token_bytes(32)).decode("utf-8"))')" -# Set to enable monitoring sensors with your sentry.io account -SENTRY_DSN= - if $DEBUG; then GUNICORN_LOG_LEVEL=debug RAY_record_ref_creation_sites=1 From 8850365f1a717cf3d9cb92886d16ae310a2c4bbe Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Wed, 1 Nov 2023 10:00:08 -0600 Subject: [PATCH 24/32] dependency updates --- src/requirements-dev.txt | 61 ++++++++++++++++++++-------------------- src/requirements.txt | 42 ++++++++++++++------------- 2 files changed, 53 insertions(+), 50 deletions(-) diff --git a/src/requirements-dev.txt b/src/requirements-dev.txt index 6baaba0a..b611a76b 100644 --- a/src/requirements-dev.txt +++ b/src/requirements-dev.txt @@ -4,7 +4,7 @@ # # pip-compile requirements-dev.in # -aiohttp==3.8.5 +aiohttp==3.8.6 # via # -r requirements-dev.in # aiohttp-cors @@ -30,7 +30,7 @@ attrs==23.1.0 # referencing blessed==1.20.0 # via gpustat -cachetools==5.3.1 +cachetools==5.3.2 # via # google-auth # tox @@ -38,7 +38,7 @@ certifi==2023.7.22 # via # -r requirements.txt # requests -cffi==1.15.1 +cffi==1.16.0 # via # -r requirements.txt # cryptography @@ -46,7 +46,7 @@ cfgv==3.4.0 # via pre-commit chardet==5.2.0 # via tox -charset-normalizer==3.2.0 +charset-normalizer==3.3.2 # via # -r requirements.txt # aiohttp @@ -59,9 +59,9 @@ colorama==0.4.6 # via tox colorful==0.5.5 # via ray -coverage[toml]==7.3.1 +coverage[toml]==7.3.2 # via pytest-cov -cryptography==41.0.4 +cryptography==41.0.5 # via -r requirements.txt defusedxml==0.7.1 # via @@ -69,7 +69,7 @@ defusedxml==0.7.1 # its-preselector distlib==0.3.7 # via virtualenv -django==3.2.21 +django==3.2.23 # via # -r requirements.txt # django-session-timeout @@ -92,7 +92,7 @@ environs==9.5.0 # scos-tekrsa exceptiongroup==1.1.3 # via pytest -filelock==3.12.4 +filelock==3.13.1 # via # -r requirements.txt # ray @@ -106,19 +106,19 @@ frozenlist==1.4.0 # ray google-api-core==2.12.0 # via opencensus -google-auth==2.23.1 +google-auth==2.23.4 # via google-api-core -googleapis-common-protos==1.60.0 +googleapis-common-protos==1.61.0 # via google-api-core gpustat==1.1.1 # via ray -grpcio==1.58.0 +grpcio==1.59.2 # via # -r requirements.txt # ray gunicorn==20.1.0 # via -r requirements.txt -identify==2.5.29 +identify==2.5.31 # via pre-commit idna==3.4 # via @@ -142,7 +142,7 @@ its-preselector @ git+https://github.com/NTIA/Preselector@3.0.2 # scos-actions jsonfield==3.1.0 # via -r requirements.txt -jsonschema==4.19.1 +jsonschema==4.19.2 # via # -r requirements.txt # ray @@ -154,11 +154,11 @@ marshmallow==3.20.1 # via # -r requirements.txt # environs -msgpack==1.0.6 +msgpack==1.0.7 # via # -r requirements.txt # ray -msgspec==0.18.2 +msgspec==0.18.4 # via # -r requirements.txt # scos-actions @@ -187,7 +187,7 @@ opencensus==0.11.3 # via ray opencensus-context==0.1.3 # via opencensus -packaging==23.1 +packaging==23.2 # via # -r requirements.txt # drf-yasg @@ -200,7 +200,7 @@ pkgutil-resolve-name==1.3.10 # via # -r requirements.txt # jsonschema -platformdirs==3.10.0 +platformdirs==3.11.0 # via # tox # virtualenv @@ -208,22 +208,22 @@ pluggy==1.3.0 # via # pytest # tox -pre-commit==3.4.0 +pre-commit==3.5.0 # via -r requirements-dev.in -prometheus-client==0.17.1 +prometheus-client==0.18.0 # via ray -protobuf==4.24.3 +protobuf==4.24.4 # via # -r requirements.txt # google-api-core # googleapis-common-protos # ray -psutil==5.9.5 +psutil==5.9.6 # via # -r requirements.txt # gpustat # scos-actions -psycopg2-binary==2.9.7 +psycopg2-binary==2.9.9 # via -r requirements.txt py-spy==0.3.14 # via ray @@ -237,17 +237,17 @@ pycparser==2.21 # via # -r requirements.txt # cffi -pydantic==1.10.12 +pydantic==1.10.13 # via ray pyproject-api==1.5.4 # via tox -pytest==7.4.2 +pytest==7.4.3 # via # pytest-cov # pytest-django pytest-cov==3.0.0 # via -r requirements-dev.in -pytest-django==4.5.2 +pytest-django==4.6.0 # via -r requirements-dev.in python-dateutil==2.8.2 # via @@ -269,7 +269,7 @@ pyyaml==6.0.1 # drf-yasg # pre-commit # ray -ray[default]==2.7.0 +ray[default]==2.7.1 # via # -r requirements-dev.in # -r requirements.txt @@ -288,14 +288,14 @@ requests==2.31.0 # requests-mock requests-mock==1.11.0 # via -r requirements.txt -rpds-py==0.10.3 +rpds-py==0.10.6 # via # -r requirements.txt # jsonschema # referencing rsa==4.9 # via google-auth -ruamel-yaml==0.17.32 +ruamel-yaml==0.18.3 # via # -r requirements.txt # scos-actions @@ -352,17 +352,16 @@ uritemplate==4.1.1 # via # -r requirements.txt # drf-yasg -urllib3==2.0.5 +urllib3==2.0.7 # via # -r requirements.txt - # google-auth # requests virtualenv==20.21.0 # via # pre-commit # ray # tox -wcwidth==0.2.6 +wcwidth==0.2.9 # via blessed yarl==1.9.2 # via aiohttp diff --git a/src/requirements.txt b/src/requirements.txt index 56c714e7..58b80ea9 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -14,17 +14,17 @@ attrs==23.1.0 # referencing certifi==2023.7.22 # via requests -cffi==1.15.1 +cffi==1.16.0 # via cryptography -charset-normalizer==3.2.0 +charset-normalizer==3.3.2 # via requests click==8.1.7 # via ray -cryptography==41.0.4 +cryptography==41.0.5 # via -r requirements.in defusedxml==0.7.1 # via its-preselector -django==3.2.21 +django==3.2.23 # via # -r requirements.in # django-session-timeout @@ -45,7 +45,7 @@ environs==9.5.0 # -r requirements.in # scos-actions # scos-tekrsa -filelock==3.12.4 +filelock==3.13.1 # via # -r requirements.in # ray @@ -53,7 +53,7 @@ frozenlist==1.4.0 # via # aiosignal # ray -grpcio==1.58.0 +grpcio==1.59.2 # via -r requirements.in gunicorn==20.1.0 # via -r requirements.in @@ -69,15 +69,15 @@ its-preselector @ git+https://github.com/NTIA/Preselector@3.0.2 # via scos-actions jsonfield==3.1.0 # via -r requirements.in -jsonschema==4.19.1 +jsonschema==4.19.2 # via ray jsonschema-specifications==2023.7.1 # via jsonschema marshmallow==3.20.1 # via environs -msgpack==1.0.6 +msgpack==1.0.7 # via ray -msgspec==0.18.2 +msgspec==0.18.4 # via scos-actions numexpr==2.8.6 # via scos-actions @@ -89,7 +89,7 @@ numpy==1.24.4 # scos-actions # sigmf # tekrsa-api-wrap -packaging==23.1 +packaging==23.2 # via # -r requirements.in # drf-yasg @@ -97,11 +97,11 @@ packaging==23.1 # ray pkgutil-resolve-name==1.3.10 # via jsonschema -protobuf==4.24.3 +protobuf==4.24.4 # via ray -psutil==5.9.5 +psutil==5.9.6 # via scos-actions -psycopg2-binary==2.9.7 +psycopg2-binary==2.9.9 # via -r requirements.in pycparser==2.21 # via cffi @@ -119,7 +119,7 @@ pyyaml==6.0.1 # -r requirements.in # drf-yasg # ray -ray==2.7.0 +ray==2.7.1 # via scos-actions referencing==0.30.2 # via @@ -132,10 +132,12 @@ requests==2.31.0 # requests-mock requests-mock==1.11.0 # via -r requirements.in -rpds-py==0.10.3 +rpds-py==0.10.6 # via - # drf-yasg - # scos-actions + # jsonschema + # referencing +ruamel-yaml==0.18.3 + # via scos-actions ruamel-yaml-clib==0.2.8 # via ruamel-yaml scipy==1.10.1 @@ -160,8 +162,10 @@ typing-extensions==4.8.0 # via asgiref uritemplate==4.1.1 # via drf-yasg -urllib3==2.0.5 - # via requests +urllib3==2.0.7 + # via + # -r requirements.in + # requests zipp==3.17.0 # via importlib-resources From 740c89a0a417f596313a89a0fc9a4a97a9e727c8 Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Mon, 20 Nov 2023 12:59:29 -0700 Subject: [PATCH 25/32] support multiple users in create_superuser, fix get username from cert, additional tests, update readme --- README.md | 93 +++++++++++++++------- docker-compose.yml | 2 + env.template | 13 +-- scripts/create_superuser.py | 54 ++++++++++--- src/authentication/auth.py | 8 +- src/authentication/tests/test_cert_auth.py | 20 +++++ src/conftest.py | 6 +- 7 files changed, 148 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 51cffb9e..e4e59022 100644 --- a/README.md +++ b/README.md @@ -197,10 +197,13 @@ actions. ## Overview of scos-sensor Repo Structure - configs: This folder is used to store the sensor_definition.json file. + - certs: CA, server, and client certificates. - docker: Contains the docker files used by scos-sensor. - docs: Documentation including the [documentation hosted on GitHub pages]( ) generated from the OpenAPI specification. +- drivers: Driver files for signal anaylzers. - entrypoints: Docker entrypoint scripts which are executed when starting a container. +- files: Folder where task results are stored. - gunicorn: Gunicorn configuration file. - nginx: Nginx configuration template and SSL certificates. - scripts: Various utility scripts. @@ -208,6 +211,7 @@ actions. - actions: Code to discover actions in plugins and to perform a simple logger action. - authentication: Code related to user authentication. - capabilities: Code used to generate capabilities endpoint. + - constants: Constants shared by the other source code folders. - handlers: Code to handle signals received from actions. - schedule: Schedule API endpoint for scheduling actions. - scheduler: Scheduler responsible for executing actions. @@ -217,9 +221,13 @@ actions. - status: Status endpoint. - tasks: Tasks endpoint used to display upcoming and completed tasks. - templates: HTML templates used by the browsable API. + - test_utils: Utility code used in tests. + - utils: Utility code shared by the other source code folders. - conftest.py: Used to configure pytest fixtures. - manage.py: Django’s command line tool for administrative tasks. - - requirements.txt and requirements-dev.txt: Python dependencies. + - requirements.in and requirements-dev.in: Direct Python dependencies. + - requirements.txt and requirements-dev.txt: Python dependencies including transitive + dependencies. - tox.ini: Used to configure tox. - docker-compose.yml: Used by Docker Compose to create services from containers. This is needed to run scos-sensor. @@ -299,11 +307,19 @@ environment (env) file is created from the env.template file. These settings can be set in the environment file or set directly in docker-compose.yml. Here are the settings in the environment file: +- ADDITIONAL_USER_NAMES: Comma separated list of additional admin usernames. +- ADDITIONAL_USER_PASSWORD: Password for additional admin users. +- ADMIN_NAME: Username for the admin user. - ADMIN_EMAIL: Email used to generate admin user. Change in production. - ADMIN_PASSWORD: Password used to generate admin user. Change in production. +- AUTHENTICATION: Authentication method used for scos-sensor. Supports `TOKEN` or + `CERT`. - BASE_IMAGE: Base docker image used to build the API container. +- CALLBACK_AUTHENTICATION: Sets how to authenticate to the callback URL. Supports + `TOKEN` or `CERT`. - CALLBACK_SSL_VERIFICATION: Set to “true” in production environment. If false, the SSL certificate validation will be ignored when posting results to the callback URL. +- CALLBACK_TIMEOUT: The timeout for the requests sent to the callback URL. - DEBUG: Django debug mode. Set to False in production. - DOCKER_TAG: Always set to “latest” to install newest version of docker containers. - DOMAINS: A space separated list of domain names. Used to generate [ALLOWED_HOSTS]( @@ -321,9 +337,14 @@ settings in the environment file: results. Defaults to 85%. This disk usage detected by scos-sensor (using the Python `shutil.disk_usage` function) may not match the usage reported by the Linux `df` command. +- PATH_TO_CLIENT_CERT: Path to file containing certificate and private key used as + client certificate when CALLBACK_AUTHENTICATION is `CERT`. +- PATH_TO_VERIFY_CERT: Trusted CA certificate to verify callback URL server + certificate. - POSTGRES_PASSWORD: Sets password for the Postgres database for the “postgres” user. Change in production. The env.template file sets to a randomly generated value. - REPO_ROOT: Root folder of the repository. Should be correctly set by default. +- SCOS_SENSOR_GIT_TAG: The scos-sensor branch name. - SECRET_KEY: Used by Django to provide cryptographic signing. Change to a unique, unpredictable value. See . The env.template @@ -332,6 +353,8 @@ settings in the environment file: scos-sensor repository with a valid certificate in production. - SSL_KEY_PATH: Path to server SSL private key. Use the private key for your valid certificate in production. +- SSL_CA_PATH: Path to a CA certificate used to verify scos-sensor client + certificate(s) when authentication is set to CERT. ### Sensor Definition File @@ -388,7 +411,8 @@ To enable Django Rest Framework token and session authentication, make sure A token is automatically created for each user. Django Rest Framework Token Authentication will check that the token in the Authorization header ("Token " + -token) matches a user's token. +token) matches a user's token. Login session authentication with username and password +is used for the browsable API. #### Certificate Authentication @@ -413,16 +437,16 @@ Below instructions adapted from This is the SSL certificate used for the scos-sensor web server and is always required. -To be able to sign server-side and client-side certificates, we need to create our own -self-signed root CA certificate first. The command will prompt you to enter a -password and the values for the CA subject. +To be able to sign server-side and client-side certificates in this example, we need to +create our own self-signed root CA certificate first. The command will prompt you to +enter a password and the values for the CA subject. ```bash openssl req -x509 -sha512 -days 365 -newkey rsa:4096 -keyout scostestca.key -out scostestca.pem ``` -Generate a host certificate signing request. Replace the values in square brackets in the -subject for the server certificate. +Generate a host certificate signing request. Replace the values in square brackets in +the subject for the server certificate. ```bash openssl req -new -newkey rsa:4096 -keyout sensor01.key -out sensor01.csr -subj "/C=[2 letter country code]/ST=[state or province]/L=[locality]/O=[organization]/OU=[organizational unit]/CN=[common name]" @@ -430,20 +454,20 @@ openssl req -new -newkey rsa:4096 -keyout sensor01.key -out sensor01.csr -subj " Before we proceed with openssl, we need to create a configuration file -- sensor01.ext. It'll store some additional parameters needed when signing the certificate. Adjust the -settings, especially DNS names and IP addresses, in the below example for your sensor: +settings, especially DNS names, in the below example for your sensor. For more +information and to customize your certificate, see the X.509 standard +[here](https://www.rfc-editor.org/rfc/rfc5280). ```text -authorityKeyIdentifier=keyid,issuer:always +authorityKeyIdentifier=keyid basicConstraints=CA:FALSE subjectAltName = @alt_names subjectKeyIdentifier = hash keyUsage = critical, digitalSignature, keyEncipherment -extendedKeyUsage = serverAuth, clientAuth +extendedKeyUsage = serverAuth, # add , clientAuth to use as client SSL cert (2-way SSL) [alt_names] -DNS.1 = sensor01.domain -DNS.2 = localhost -IP.1 = xxx.xxx.xxx.xxx -IP.2 = 127.0.0.1 +DNS.1 = localhost +# Add additional DNS names as needed, e.g. DNS.2, DNS.3, etc ``` Sign the host certificate. @@ -467,7 +491,8 @@ cat sensor01_decrypted.key sensor01.pem > sensor01_combined.pem ##### Client Certificate This certificate is required for using the sensor with mutual TLS certificate -authentication. +authentication (2 way SSL, AUTHENTICATION=CERT). This example uses the same self-signed +CA used for creating the example scos-sensor server certificate. Replace the brackets with the information specific to your user and organization. @@ -480,8 +505,8 @@ Create client.ext with the following: ```text basicConstraints = CA:FALSE subjectKeyIdentifier = hash -authorityKeyIdentifier = keyid,issuer -keyUsage = digitalSignature +authorityKeyIdentifier = keyid +keyUsage = critical, digitalSignature extendedKeyUsage = clientAuth ``` @@ -539,21 +564,22 @@ mutual TLS, also copy the CA certificate to the same directory. Then, set If you are using client certificates, use client.pfx to connect to the browsable API by importing this certificate into your browser. -For callback functionality with a server that uses certificate authentication, set -`PATH_TO_CLIENT_CERT` and `PATH_TO_VERIFY_CERT`, both relative to configs/certs. -Depending on the configuration of the callback URL server and the authorization server, -the sensor server certificate could be used as a client certificate by setting -`PATH_TO_CLIENT_CERT` to the path of sensor01_combined.pem relative to configs/certs. -Also the CA used to verify the client certificate could potentially be used to verify -the callback URL server certificate by setting `PATH_TO_VERIFY_CERT` to the same file -as used for `SSL_CA_PATH` (scostestca.pem). - #### Permissions and Users The API requires the user to be a superuser. New users created using the API initially do not have superuser access. However, an admin can mark a user as a superuser in the Sensor Configuration Portal. +When scos-sensor starts, an admin user is created using the ADMIN_NAME, ADMIN_EMAIL and +ADMIN_PASSWORD environment variables. The ADMIN_NAME is the username for the admin +user. Additional admin users can be created using the ADDITIONAL_USER_NAMES and +ADDITIONAL_USER_PASSWORD environment variables. ADDITIONAL_USER_NAMES is a comma +separated list. ADDITIONAL_USER_PASSWORD is a single password used for each additional +admin user. If ADDITIONAL_USER_PASSWORD is not specified, the additional users will +be created with an unusable password, which is sufficient if only using certificates +or tokens to authenticate. However, a password is required to access the Sensor +Configuration Portal. + ### Callback URL Authentication Certificate and token authentication are supported for authenticating against the @@ -577,7 +603,7 @@ used. #### Certificate -Certificate authetnication (mutual TLS) is supported for callback URL authentication. +Certificate authentication (mutual TLS) is supported for callback URL authentication. The following settings in the environment file are used to configure certificate authentication for the callback URL. @@ -590,6 +616,19 @@ authentication for the callback URL. https://requests.readthedocs.io/en/master/user/advanced/#ca-certificates) will be used. +Set `PATH_TO_CLIENT_CERT` and `PATH_TO_VERIFY_CERT` relative to configs/certs. +Depending on the configuration of the callback URL server, the scos-sensor server +certificate could be used as a client certificate (if created with clientAuth extended +key usage) by setting `PATH_TO_CLIENT_CERT` to the same value as `SSL_CERT_PATH` +if the private key is bundled with the certificate. Also +the CA used to verify the scos-sensor client certificate(s) could potentially be used +to verify the callback URL server certificate by setting `PATH_TO_VERIFY_CERT` to the +same file as used for `SSL_CA_PATH`. This would require the callback URL server +certificate to be issued by the same CA as the scos-sensor client certficate(s) or have +the callback URL server's CA cert bundled with the scos-sensor client CA cert. Make +sure to consider the security implications of these configurations and settings, +especially using the same files for multiple settings. + ### Data File Encryption The data files are encrypted on disk by default using Cryptography Fernet module. The diff --git a/docker-compose.yml b/docker-compose.yml index 1963ec82..4abfd274 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,6 +35,8 @@ services: - ADMIN_NAME - ADMIN_EMAIL - ADMIN_PASSWORD + - ADDITIONAL_USER_NAMES + - ADDITIONAL_USER_PASSWORD - AUTHENTICATION - CALLBACK_AUTHENTICATION - CALLBACK_SSL_VERIFICATION diff --git a/env.template b/env.template index 52919680..23fb714a 100644 --- a/env.template +++ b/env.template @@ -39,6 +39,9 @@ GIT_BRANCH="git:$(git rev-parse --abbrev-ref HEAD)@$(git rev-parse --short HEAD) # If admin user email and password set, admin user will be generated. ADMIN_EMAIL="admin@example.com" ADMIN_PASSWORD=password +ADMIN_NAME=Admin +ADDITIONAL_USER_NAMES="" # comma separated +ADDITIONAL_USER_PASSWORD="" # Session password for Postgres. Username is "postgres". # SECURITY WARNING: generate unique key with something like @@ -65,11 +68,11 @@ BASE_IMAGE=ghcr.io/ntia/scos-tekrsa/tekrsa_usb:0.2.3 CALLBACK_AUTHENTICATION=TOKEN CALLBACK_TIMEOUT=2 -# Sensor certificate with private key used as client cert +# Sensor certificate with private key used as client cert for callback URL +# Paths relative to configs/certs PATH_TO_CLIENT_CERT=sensor01.pem -# Trusted Certificate Authority certificate to verify authserver and callback URL server certificate +# Trusted Certificate Authority certificate to verify callback URL server certificate PATH_TO_VERIFY_CERT=scos_test_ca.crt -# Path relative to configs/certs -# set to CERT to enable certificate authentication + +# set to CERT to enable scos-sensor certificate authentication AUTHENTICATION=CERT -ADMIN_NAME=Admin diff --git a/scripts/create_superuser.py b/scripts/create_superuser.py index dea05b30..89529023 100755 --- a/scripts/create_superuser.py +++ b/scripts/create_superuser.py @@ -13,24 +13,60 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sensor.settings") django.setup() +UserModel = get_user_model() + + +def add_user(username, password, email=None): + try: + admin_user = UserModel._default_manager.get(username=username) + if email: + admin_user.email = email + admin_user.set_password(password) + print("Reset admin account password and email from environment") + except UserModel.DoesNotExist: + UserModel._default_manager.create_superuser(username, email, password) + print("Created admin account with password and email from environment") + try: password = os.environ["ADMIN_PASSWORD"] print("Retreived admin password from environment variable ADMIN_PASSWORD") email = os.environ["ADMIN_EMAIL"] print("Retreived admin email from environment variable ADMIN_EMAIL") + username = os.environ["ADMIN_NAME"] + print("Retreived admin name from environment variable ADMIN_NAME") + add_user(username, password, email) except KeyError: print("Not on a managed sensor, so not auto-generating admin account.") print("You can add an admin later with `./manage.py createsuperuser`") sys.exit(0) -UserModel = get_user_model() -username = os.environ["ADMIN_NAME"] +additional_user_names = "" +additional_user_password = "" try: - admin_user = UserModel._default_manager.get(username=username) - admin_user.email = email - admin_user.set_password(password) - print("Reset admin account password and email from environment") -except UserModel.DoesNotExist: - UserModel._default_manager.create_superuser(username, email, password) - print("Created admin account with password and email from environment") + additional_user_names = os.environ["ADDITIONAL_USER_NAMES"] + print( + "Retreived additional user names from environment variable ADDITIONAL_USER_NAMES" + ) + if ( + "ADDITIONAL_USER_PASSWORD" in os.environ + and os.environ["ADDITIONAL_USER_PASSWORD"] + ): + additional_user_password = os.environ["ADDITIONAL_USER_PASSWORD"] + else: + # user will have unusable password + # https://docs.djangoproject.com/en/3.2/ref/contrib/auth/#django.contrib.auth.models.UserManager.create_user + additional_user_password = None + print( + "Retreived additional user password from environment variable ADDITIONAL_USER_PASSWORD" + ) +except KeyError: + print("Not creating any additonal users.") + + +if additional_user_names != "" and additional_user_password != "": + if "," in additional_user_names: + for additional_user_name in additional_user_names.split(","): + add_user(additional_user_name, additional_user_password) + else: + add_user(additional_user_names, additional_user_password) diff --git a/src/authentication/auth.py b/src/authentication/auth.py index 09507302..3af870e9 100644 --- a/src/authentication/auth.py +++ b/src/authentication/auth.py @@ -40,10 +40,10 @@ def authenticate(self, request): def get_cn_from_dn(cert_dn): - p = re.compile(r"CN=(.*?)(?:,|\+|$)") + p = re.compile(r"CN=[a-zA-Z0-9\s.]*") match = p.search(cert_dn) if not match: raise Exception("No CN found in certificate!") - uid_raw = match.group() - uid = uid_raw.split("=")[1].rstrip(",") - return uid + cn = match.group() + cn = cn[3:] + return cn diff --git a/src/authentication/tests/test_cert_auth.py b/src/authentication/tests/test_cert_auth.py index a041cccc..9d98cb07 100644 --- a/src/authentication/tests/test_cert_auth.py +++ b/src/authentication/tests/test_cert_auth.py @@ -179,3 +179,23 @@ def test_empty_dn_unauthorized(live_server, admin_user): } response = client.get(f"{live_server.url}", headers=headers) assert response.status_code == 403 + + +@pytest.mark.django_db +def test_dn_with_uid(live_server, admin_user): + client = RequestsClient() + headers = { + "X-Ssl-Client-Dn": f"C=TC,ST=test_state,L=test_locality,O=test_org,OU=test_ou,CN={admin_user.username}+UID=11111", + } + response = client.get(f"{live_server.url}", headers=headers) + assert response.status_code == 200 + + +@pytest.mark.django_db +def test_dn_cn_space_reverse_order(live_server, alt_admin_user): + client = RequestsClient() + headers = { + "X-Ssl-Client-Dn": f"CN={alt_admin_user.username}+UID=111111,OU=test_ou,O=test_org,L=test_locality,ST=test_state,C=TC", + } + response = client.get(f"{live_server.url}", headers=headers) + assert response.status_code == 200 diff --git a/src/conftest.py b/src/conftest.py index 9897bcdc..58b70608 100644 --- a/src/conftest.py +++ b/src/conftest.py @@ -115,15 +115,15 @@ def alt_admin_user(db, django_user_model, django_username_field): username_field = django_username_field try: - user = UserModel._default_manager.get(**{username_field: "alt_admin"}) + user = UserModel._default_manager.get(**{username_field: "ALT ADMIN"}) except UserModel.DoesNotExist: extra_fields = {} if username_field != "username": - extra_fields[username_field] = "alt_admin" + extra_fields[username_field] = "ALT ADMIN" user = UserModel._default_manager.create_superuser( - "alt_admin", "alt_admin@example.com", "password", **extra_fields + "ALT ADMIN", "alt_admin@example.com", "password", **extra_fields ) return user From 0f9e3b3a9593f03ac803d411bc42f445361acd34 Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Mon, 20 Nov 2023 14:37:18 -0700 Subject: [PATCH 26/32] update min versions for dependabot alerts --- src/requirements-dev.in | 2 +- src/requirements.in | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/requirements-dev.in b/src/requirements-dev.in index 7a80f7f1..bbcf7391 100644 --- a/src/requirements-dev.in +++ b/src/requirements-dev.in @@ -9,4 +9,4 @@ tox>=4.0,<5.0 # The following are sub-dependencies for which SCOS Sensor enforces a # higher minimum patch version than the dependencies which require them. # This is done to ensure the inclusion of specific security patches. -aiohttp>=3.8.5 # CVE-2023-37276 +aiohttp>=3.8.6 # CVE-2023-37276, CVE-2023-47627 diff --git a/src/requirements.in b/src/requirements.in index f0448d63..e49746e2 100644 --- a/src/requirements.in +++ b/src/requirements.in @@ -1,5 +1,5 @@ cryptography>=41.0.4 -django>=3.2.20, <4.0 +django>=3.2.23, <4.0 djangorestframework>=3.0, <4.0 django-session-timeout>=0.1, <1.0 drf-yasg>=1.0, <2.0 From 4de3426965e0a7ea3f6a4baba9ae82d89bb75bd5 Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Wed, 29 Nov 2023 15:35:55 -0700 Subject: [PATCH 27/32] set defaults for token auth --- env.template | 2 +- nginx/conf.template | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/env.template b/env.template index 23fb714a..2f6429c4 100644 --- a/env.template +++ b/env.template @@ -75,4 +75,4 @@ PATH_TO_CLIENT_CERT=sensor01.pem PATH_TO_VERIFY_CERT=scos_test_ca.crt # set to CERT to enable scos-sensor certificate authentication -AUTHENTICATION=CERT +AUTHENTICATION=TOKEN diff --git a/nginx/conf.template b/nginx/conf.template index 8ca7aadb..085f8843 100644 --- a/nginx/conf.template +++ b/nginx/conf.template @@ -33,8 +33,8 @@ server { ssl_certificate /etc/ssl/certs/ssl-cert.pem; ssl_certificate_key /etc/ssl/private/ssl-cert.key; ssl_protocols TLSv1.2 TLSv1.3; - ssl_client_certificate /etc/ssl/certs/ca.crt; - ssl_verify_client on; + # ssl_client_certificate /etc/ssl/certs/ca.crt; + # ssl_verify_client on; # ssl_ocsp on; # Enable OCSP validation ssl_verify_depth 4; # path for static files @@ -49,9 +49,9 @@ server { # Pass off requests to Gunicorn location @proxy_to_wsgi_server { - if ($ssl_client_verify != SUCCESS) { - return 403; - } + # if ($ssl_client_verify != SUCCESS) { + # return 403; + # } proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; From 2c11834d44eba51f2d0d46e125c9153c3fe463d5 Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Fri, 1 Dec 2023 08:44:34 -0700 Subject: [PATCH 28/32] update requirements --- src/requirements-dev.txt | 61 ++++++++++++++++------------------------ src/requirements.txt | 32 +++++++++------------ 2 files changed, 38 insertions(+), 55 deletions(-) diff --git a/src/requirements-dev.txt b/src/requirements-dev.txt index 05b3a01e..6d983ae4 100644 --- a/src/requirements-dev.txt +++ b/src/requirements-dev.txt @@ -4,7 +4,7 @@ # # pip-compile requirements-dev.in # -aiohttp==3.9.0 +aiohttp==3.9.1 # via # -r requirements-dev.in # aiohttp-cors @@ -34,7 +34,7 @@ cachetools==5.3.2 # via # google-auth # tox -certifi==2023.7.22 +certifi==2023.11.17 # via # -r requirements.txt # requests @@ -58,20 +58,9 @@ colorama==0.4.6 # via tox colorful==0.5.5 # via ray -coreapi==2.3.3 - # via - # -r requirements.txt - # drf-yasg -coreschema==0.0.4 - # via - # -r requirements.txt - # coreapi - # drf-yasg coverage[toml]==7.3.2 - # via - # coverage - # pytest-cov -cryptography==41.0.6 + # via pytest-cov +cryptography==41.0.7 # via -r requirements.txt defusedxml==0.7.1 # via @@ -100,7 +89,7 @@ environs==9.5.0 # -r requirements.txt # scos-actions # scos-tekrsa -exceptiongroup==1.1.3 +exceptiongroup==1.2.0 # via pytest filelock==3.13.1 # via @@ -114,28 +103,28 @@ frozenlist==1.4.0 # aiohttp # aiosignal # ray -google-api-core==2.12.0 +google-api-core==2.14.0 # via opencensus -google-auth==2.23.4 +google-auth==2.24.0 # via google-api-core googleapis-common-protos==1.61.0 # via google-api-core gpustat==1.1.1 # via ray -grpcio==1.59.2 +grpcio==1.59.3 # via # -r requirements.txt # ray gunicorn==20.1.0 # via -r requirements.txt -identify==2.5.31 +identify==2.5.32 # via pre-commit -idna==3.4 +idna==3.6 # via # -r requirements.txt # requests # yarl -importlib-resources==6.1.0 +importlib-resources==6.1.1 # via # -r requirements.txt # jsonschema @@ -152,11 +141,11 @@ its-preselector @ git+https://github.com/NTIA/Preselector@3.0.2 # scos-actions jsonfield==3.1.0 # via -r requirements.txt -jsonschema==4.19.2 +jsonschema==4.20.0 # via # -r requirements.txt # ray -jsonschema-specifications==2023.7.1 +jsonschema-specifications==2023.11.2 # via # -r requirements.txt # jsonschema @@ -191,7 +180,7 @@ numpy==1.24.4 # scos-actions # sigmf # tekrsa-api-wrap -nvidia-ml-py==12.535.108 +nvidia-ml-py==12.535.133 # via gpustat opencensus==0.11.3 # via ray @@ -220,9 +209,9 @@ pluggy==1.3.0 # tox pre-commit==3.5.0 # via -r requirements-dev.in -prometheus-client==0.18.0 +prometheus-client==0.19.0 # via ray -protobuf==4.24.4 +protobuf==4.25.1 # via # -r requirements.txt # google-api-core @@ -237,7 +226,7 @@ psycopg2-binary==2.9.9 # via -r requirements.txt py-spy==0.3.14 # via ray -pyasn1==0.5.0 +pyasn1==0.5.1 # via # pyasn1-modules # rsa @@ -257,7 +246,7 @@ pytest==7.4.3 # pytest-django pytest-cov==3.0.0 # via -r requirements-dev.in -pytest-django==4.6.0 +pytest-django==4.7.0 # via -r requirements-dev.in python-dateutil==2.8.2 # via @@ -279,12 +268,12 @@ pyyaml==6.0.1 # drf-yasg # pre-commit # ray -ray[default]==2.7.1 +ray[default]==2.8.1 # via # -r requirements-dev.in # -r requirements.txt # scos-actions -referencing==0.30.2 +referencing==0.31.1 # via # -r requirements.txt # jsonschema @@ -298,14 +287,14 @@ requests==2.31.0 # requests-mock requests-mock==1.11.0 # via -r requirements.txt -rpds-py==0.10.6 +rpds-py==0.13.2 # via # -r requirements.txt # jsonschema # referencing rsa==4.9 # via google-auth -ruamel-yaml==0.18.3 +ruamel-yaml==0.18.5 # via # -r requirements.txt # scos-actions @@ -362,7 +351,7 @@ uritemplate==4.1.1 # via # -r requirements.txt # drf-yasg -urllib3==2.0.7 +urllib3==2.1.0 # via # -r requirements.txt # requests @@ -371,9 +360,9 @@ virtualenv==20.21.0 # pre-commit # ray # tox -wcwidth==0.2.9 +wcwidth==0.2.12 # via blessed -yarl==1.9.2 +yarl==1.9.3 # via aiohttp zipp==3.17.0 # via diff --git a/src/requirements.txt b/src/requirements.txt index 7359a545..191be082 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -12,7 +12,7 @@ attrs==23.1.0 # via # jsonschema # referencing -certifi==2023.7.22 +certifi==2023.11.17 # via requests cffi==1.16.0 # via cryptography @@ -20,13 +20,7 @@ charset-normalizer==3.3.2 # via requests click==8.1.7 # via ray -coreapi==2.3.3 - # via drf-yasg -coreschema==0.0.4 - # via - # coreapi - # drf-yasg -cryptography==41.0.6 +cryptography==41.0.7 # via -r requirements.in defusedxml==0.7.1 # via its-preselector @@ -59,13 +53,13 @@ frozenlist==1.4.0 # via # aiosignal # ray -grpcio==1.59.2 +grpcio==1.59.3 # via -r requirements.in gunicorn==20.1.0 # via -r requirements.in -idna==3.4 +idna==3.6 # via requests -importlib-resources==6.1.0 +importlib-resources==6.1.1 # via # jsonschema # jsonschema-specifications @@ -75,9 +69,9 @@ its-preselector @ git+https://github.com/NTIA/Preselector@3.0.2 # via scos-actions jsonfield==3.1.0 # via -r requirements.in -jsonschema==4.19.2 +jsonschema==4.20.0 # via ray -jsonschema-specifications==2023.7.1 +jsonschema-specifications==2023.11.2 # via jsonschema marshmallow==3.20.1 # via environs @@ -103,7 +97,7 @@ packaging==23.2 # ray pkgutil-resolve-name==1.3.10 # via jsonschema -protobuf==4.24.4 +protobuf==4.25.1 # via ray psutil==5.9.6 # via scos-actions @@ -125,9 +119,9 @@ pyyaml==6.0.1 # -r requirements.in # drf-yasg # ray -ray==2.7.1 +ray==2.8.1 # via scos-actions -referencing==0.30.2 +referencing==0.31.1 # via # jsonschema # jsonschema-specifications @@ -138,11 +132,11 @@ requests==2.31.0 # requests-mock requests-mock==1.11.0 # via -r requirements.in -rpds-py==0.10.6 +rpds-py==0.13.2 # via # jsonschema # referencing -ruamel-yaml==0.18.3 +ruamel-yaml==0.18.5 # via scos-actions ruamel-yaml-clib==0.2.8 # via ruamel-yaml @@ -168,7 +162,7 @@ typing-extensions==4.8.0 # via asgiref uritemplate==4.1.1 # via drf-yasg -urllib3==2.0.7 +urllib3==2.1.0 # via # -r requirements.in # requests From e33d47917d1be9590ecaf1daf92911ceddffbcc2 Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Fri, 1 Dec 2023 13:24:10 -0700 Subject: [PATCH 29/32] revert ray version --- src/requirements-dev.txt | 2 +- src/requirements.txt | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/requirements-dev.txt b/src/requirements-dev.txt index 6d983ae4..42e98189 100644 --- a/src/requirements-dev.txt +++ b/src/requirements-dev.txt @@ -268,7 +268,7 @@ pyyaml==6.0.1 # drf-yasg # pre-commit # ray -ray[default]==2.8.1 +ray[default]==2.6.3 # via # -r requirements-dev.in # -r requirements.txt diff --git a/src/requirements.txt b/src/requirements.txt index 191be082..249111d3 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -54,7 +54,9 @@ frozenlist==1.4.0 # aiosignal # ray grpcio==1.59.3 - # via -r requirements.in + # via + # -r requirements.in + # ray gunicorn==20.1.0 # via -r requirements.in idna==3.6 @@ -119,7 +121,7 @@ pyyaml==6.0.1 # -r requirements.in # drf-yasg # ray -ray==2.8.1 +ray==2.6.3 # via scos-actions referencing==0.31.1 # via From 3590675efc0316848a34425f40a6d337454a3bd3 Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Mon, 8 Jan 2024 09:19:06 -0700 Subject: [PATCH 30/32] address feedback, remove unneeded env variables from tox.ini --- README.md | 15 +++++++-------- scripts/create_superuser.py | 2 +- src/authentication/auth.py | 5 ++++- src/tox.ini | 2 -- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index e4e59022..54c5fc8b 100644 --- a/README.md +++ b/README.md @@ -405,9 +405,9 @@ or using Django Rest Framework Token Authentication. #### Django Rest Framework Token Authentication -To enable Django Rest Framework token and session authentication, make sure -`AUTHENTICATION` is set to `TOKEN` in the environment file (this will be enabled if -`AUTHENTICATION` set to anything other than `CERT`). +This is the default authentication method. To enable Django Rest Framework token and +session authentication, make sure `AUTHENTICATION` is set to `TOKEN` in the environment +file (this will be enabled if `AUTHENTICATION` set to anything other than `CERT`). A token is automatically created for each user. Django Rest Framework Token Authentication will check that the token in the Authorization header ("Token " + @@ -416,10 +416,9 @@ is used for the browsable API. #### Certificate Authentication -This is the default authentication method. To enable Certificate Authentication, make -sure `AUTHENTICATION` is set to `CERT` in the environment -file. To authenticate, the client will need to send a trusted client certificate. The -Common Name must match the username of a user in the database. +To enable Certificate Authentication, make sure `AUTHENTICATION` is set to `CERT` in +the environment file. To authenticate, the client will need to send a trusted client +certificate. The Common Name must match the username of a user in the database. #### Certificates @@ -527,7 +526,7 @@ or client.pfx when communicating with the API programmatically. ###### Configure scos-sensor -The Nginx web server is configured by default to require client certificates (mutual +The Nginx web server is not configured by default to require client certificates (mutual TLS). To require client certificates, make sure `ssl_verify_client` is set to `on` in the [Nginx configuration file](nginx/conf.template). Comment out this line or set to `off` to disable client certificates. This can also be set to `optional` or diff --git a/scripts/create_superuser.py b/scripts/create_superuser.py index 89529023..97cd9b4c 100755 --- a/scripts/create_superuser.py +++ b/scripts/create_superuser.py @@ -67,6 +67,6 @@ def add_user(username, password, email=None): if additional_user_names != "" and additional_user_password != "": if "," in additional_user_names: for additional_user_name in additional_user_names.split(","): - add_user(additional_user_name, additional_user_password) + add_user(additional_user_name.strip(), additional_user_password) else: add_user(additional_user_names, additional_user_password) diff --git a/src/authentication/auth.py b/src/authentication/auth.py index 3af870e9..920a5db0 100644 --- a/src/authentication/auth.py +++ b/src/authentication/auth.py @@ -30,8 +30,11 @@ def authenticate(self, request): user.last_login = datetime.datetime.now() user.save() except user_model.DoesNotExist: + logger.error("No matching username found!") raise exceptions.AuthenticationFailed("No matching username found!") - except Exception: + except Exception as ex: + logger.error("Error occurred during certificate authentication!") + logger.error(ex) raise exceptions.AuthenticationFailed( "Error occurred during certificate authentication!" ) diff --git a/src/tox.ini b/src/tox.ini index 5f677be7..d44c4172 100644 --- a/src/tox.ini +++ b/src/tox.ini @@ -19,8 +19,6 @@ envlist = py38,py39,py310 setenv = AUTHENTICATION=CERT CALLBACK_AUTHENTICATION=CERT - PATH_TO_CLIENT_CERT=test/sensor01.pem - PATH_TO_VERIFY_CERT=test/scos_test_ca.crt SWITCH_CONFIGS_DIR=../configs/switches [testenv:coverage] From e9b2f30b58a65d3e9bdf860d35c6909dc1198aea Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Mon, 8 Jan 2024 12:48:25 -0700 Subject: [PATCH 31/32] address more feedback --- README.md | 40 ++++++++++++++++++++-------------------- nginx/conf.template | 3 --- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 54c5fc8b..bb593504 100644 --- a/README.md +++ b/README.md @@ -526,30 +526,30 @@ or client.pfx when communicating with the API programmatically. ###### Configure scos-sensor -The Nginx web server is not configured by default to require client certificates (mutual -TLS). To require client certificates, make sure `ssl_verify_client` is set to `on` in -the [Nginx configuration file](nginx/conf.template). Comment out this line or set to -`off` to disable client certificates. This can also be set to `optional` or -`optional_no_ca`, but if a client certificate is not provided, scos-sensor -`AUTHENTICATION` setting must be set to `TOKEN` which requires a token for the API or a -username and password for the browsable API. If you use OCSP, also uncomment -`ssl_ocsp on;`. Additional configuration may be needed for Nginx to check certificate -revocation lists (CRL). Adjust the other Nginx parameters, such as `ssl_verify_depth`, -as desired. See the -[Nginx documentation](https://nginx.org/en/docs/http/ngx_http_ssl_module.html) for -more information about configuring Nginx. - -To disable client certificate authentication, comment out the -following in [nginx/conf.template](nginx/conf.template): +The Nginx web server is not configured by default to require client certificates +(mutual TLS). To require client certificates, uncomment out the following in +[nginx/conf.template](nginx/conf.template): ```text ssl_client_certificate /etc/ssl/certs/ca.crt; ssl_verify_client on; -ssl_ocsp on; -... - if ($ssl_client_verify != SUCCESS) { # under location @proxy_to_wsgi_server { - return 403; - } +``` + +Note that additional configuration may be needed for Nginx to +use OCSP validation and/or check certificate revocation lists (CRL). Adjust the other +Nginx parameters, such as `ssl_verify_depth`, as desired. See the +[Nginx documentation](https://nginx.org/en/docs/http/ngx_http_ssl_module.html) for more +information about configuring Nginx SSL settings. The `ssl_verify_client` setting can +also be set to `optional` or `optional_no_ca`, but if a client certificate is not +provided, scos-sensor `AUTHENTICATION` setting must be set to `TOKEN` which requires a +token for the API or a username and password for the browsable API. + +To disable client certificate authentication, comment out the following in +[nginx/conf.template](nginx/conf.template): + +```text +# ssl_client_certificate /etc/ssl/certs/ca.crt; +# ssl_verify_client on; ``` Copy the server certificate and server private key (sensor01_combined.pem) to diff --git a/nginx/conf.template b/nginx/conf.template index 085f8843..e5e0d46b 100644 --- a/nginx/conf.template +++ b/nginx/conf.template @@ -49,9 +49,6 @@ server { # Pass off requests to Gunicorn location @proxy_to_wsgi_server { - # if ($ssl_client_verify != SUCCESS) { - # return 403; - # } proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; From f15ae64fac7cb1414b3a8fa0ebe5054cd13b3830 Mon Sep 17 00:00:00 2001 From: Justin Haze Date: Tue, 16 Jan 2024 10:04:55 -0700 Subject: [PATCH 32/32] add some additional fixes to create_supseruser.py, address feedback for auth.py log message --- scripts/create_superuser.py | 7 ++++--- src/authentication/auth.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/create_superuser.py b/scripts/create_superuser.py index 97cd9b4c..522825b0 100755 --- a/scripts/create_superuser.py +++ b/scripts/create_superuser.py @@ -22,6 +22,7 @@ def add_user(username, password, email=None): if email: admin_user.email = email admin_user.set_password(password) + admin_user.save() print("Reset admin account password and email from environment") except UserModel.DoesNotExist: UserModel._default_manager.create_superuser(username, email, password) @@ -35,7 +36,7 @@ def add_user(username, password, email=None): print("Retreived admin email from environment variable ADMIN_EMAIL") username = os.environ["ADMIN_NAME"] print("Retreived admin name from environment variable ADMIN_NAME") - add_user(username, password, email) + add_user(username.strip(), password.strip(), email.strip()) except KeyError: print("Not on a managed sensor, so not auto-generating admin account.") print("You can add an admin later with `./manage.py createsuperuser`") @@ -52,7 +53,7 @@ def add_user(username, password, email=None): "ADDITIONAL_USER_PASSWORD" in os.environ and os.environ["ADDITIONAL_USER_PASSWORD"] ): - additional_user_password = os.environ["ADDITIONAL_USER_PASSWORD"] + additional_user_password = os.environ["ADDITIONAL_USER_PASSWORD"].strip() else: # user will have unusable password # https://docs.djangoproject.com/en/3.2/ref/contrib/auth/#django.contrib.auth.models.UserManager.create_user @@ -69,4 +70,4 @@ def add_user(username, password, email=None): for additional_user_name in additional_user_names.split(","): add_user(additional_user_name.strip(), additional_user_password) else: - add_user(additional_user_names, additional_user_password) + add_user(additional_user_names.strip(), additional_user_password) diff --git a/src/authentication/auth.py b/src/authentication/auth.py index 920a5db0..07605e77 100644 --- a/src/authentication/auth.py +++ b/src/authentication/auth.py @@ -30,7 +30,7 @@ def authenticate(self, request): user.last_login = datetime.datetime.now() user.save() except user_model.DoesNotExist: - logger.error("No matching username found!") + logger.error(f"No username matching {cn} found in database!") raise exceptions.AuthenticationFailed("No matching username found!") except Exception as ex: logger.error("Error occurred during certificate authentication!")