From 1ea8095a4fc66f67a35993b79b05644db3ff6a5a Mon Sep 17 00:00:00 2001 From: Hina Shah Date: Tue, 12 Mar 2024 10:25:19 -0400 Subject: [PATCH] BREAKING CHANGE: BREAKING FEAT: Updating Django to 4.2 --- .env.sample | 3 +- Dockerfile | 2 +- Makefile | 4 +++ appstore/api/urls.py | 3 +- appstore/api/v1/views.py | 5 +-- appstore/appstore/settings/base.py | 36 ++++++++++++++----- appstore/appstore/settings/heal_settings.py | 2 +- appstore/appstore/settings/helx_settings.py | 2 +- appstore/core/models.py | 12 +++++++ .../middleware/filter_whitelist_middleware.py | 19 +++++++++- appstore/middleware/tests.py | 3 +- requirements.txt | 32 ++++++++--------- 12 files changed, 89 insertions(+), 34 deletions(-) diff --git a/.env.sample b/.env.sample index 9a186f4f8..a437bc951 100644 --- a/.env.sample +++ b/.env.sample @@ -17,4 +17,5 @@ GITHUB_SECRET="" OAUTH_PROVIDERS="github" SECRET_KEY="" NAMESPACE="default" -stdnfsPvc="stdnfs" \ No newline at end of file +stdnfsPvc="stdnfs" +CSRF_DOMAINS="https://*.renci.org" diff --git a/Dockerfile b/Dockerfile index 7e42daf43..ff85daaa0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ RUN adduser --disabled-login --home $HOME --shell /bin/bash --uid $UID $USER && RUN set -x && apt-get update && \ chown -R $UID:$UID $APP_HOME && \ - apt-get install -y build-essential git xmlsec1 + apt-get install -y build-essential git xmlsec1 libpq5 gcc # Removing but leaving commented in case Tycho needs this for swagger. # Version 3.3.1 currently, if not complaints v3.3.3 this can be diff --git a/Makefile b/Makefile index 815ccc03f..4174a4289 100644 --- a/Makefile +++ b/Makefile @@ -84,6 +84,10 @@ clean: ${MANAGE} flush ${PYTHON} -m pip uninstall -y -r requirements.txt +#install: Install application along with required development packages +install: + ${PYTHON} -m pip install --upgrade pip + ${PYTHON} -m pip install -r requirements.txt #test: Run all tests test: diff --git a/appstore/api/urls.py b/appstore/api/urls.py index aceac12bb..1f51eb754 100644 --- a/appstore/api/urls.py +++ b/appstore/api/urls.py @@ -1,5 +1,4 @@ -from django.conf.urls import re_path -from django.urls import include +from django.urls import include, re_path from .v1.router import v1_urlpatterns diff --git a/appstore/api/v1/views.py b/appstore/api/v1/views.py index 332e889fe..536548bbb 100644 --- a/appstore/api/v1/views.py +++ b/appstore/api/v1/views.py @@ -684,10 +684,11 @@ def _get_social_providers(self, request, settings): "allauth.account.auth_backends.AuthenticationBackend" in settings.AUTHENTICATION_BACKENDS ): - for provider in socialaccount.providers.registry.get_list(): + for provider in socialaccount.providers.registry.get_class_list(): + inst = provider(request, "allauth.socialaccount") provider_data.append( asdict( - LoginProvider(provider.name, provider.get_login_url(request)) + LoginProvider(inst.name, inst.get_login_url(request)) ) ) diff --git a/appstore/appstore/settings/base.py b/appstore/appstore/settings/base.py index c15bd75ad..232e1d4a1 100644 --- a/appstore/appstore/settings/base.py +++ b/appstore/appstore/settings/base.py @@ -41,6 +41,7 @@ if DEBUG_STRING.lower() == "false": DEBUG_STRING = "" DEBUG = bool(DEBUG_STRING) + # stub, local, dev, val, prod. DEV_PHASE = os.environ.get("DEV_PHASE", "local") TYCHO_MODE = os.environ.get("TYCHO_MODE", "null" if DEV_PHASE == "stub" else "live") @@ -80,6 +81,7 @@ "django.contrib.auth", "django.contrib.messages", "django.contrib.sites", + "django_saml2_auth", ] THIRD_PARTY_APPS = [ @@ -122,9 +124,10 @@ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", - "django.contrib.auth.middleware.RemoteUserMiddleware", + "django.contrib.auth.middleware.PersistentRemoteUserMiddleware", "middleware.filter_whitelist_middleware.AllowWhiteListedUserOnly", "middleware.session_idle_timeout.SessionIdleTimeout", + "allauth.account.middleware.AccountMiddleware" ] SESSION_IDLE_TIMEOUT = int(os.environ.get("DJANGO_SESSION_IDLE_TIMEOUT", 300)) @@ -143,8 +146,8 @@ ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 1 ACCOUNT_EMAIL_VERIFICATION = "none" -ACCOUNT_LOGIN_ATTEMPTS_LIMIT = 3 -ACCOUNT_LOGIN_ATTEMPTS_TIMEOUT = 86400 # 1 day in seconds +ACCOUNT_RATE_LIMITS= {'login_failed':10} +#deprecated ACCOUNT_LOGIN_ATTEMPTS_TIMEOUT = 86400 # 1 day in seconds ACCOUNT_LOGOUT_REDIRECT_URL = "/helx" LOGIN_REDIRECT_URL = "/helx/workspaces/login/success" LOGIN_URL = "/accounts/login" @@ -152,11 +155,13 @@ OIDC_SESSION_MANAGEMENT_ENABLE = True SAML_URL = "/accounts/saml" SAML_ACS_URL = "/saml2_auth/acs/" +#SAML_ACS_URL = "/sso/acs/" SOCIALACCOUNT_QUERY_EMAIL = ACCOUNT_EMAIL_REQUIRED SOCIALACCOUNT_STORE_TOKENS = True SOCIALACCOUNT_PROVIDERS = { "google": {"SCOPE": ["profile", "email"], "AUTH_PARAMS": {"access_type": "offline"}} } +SECURE_CROSS_ORIGIN_OPENER_POLICY = None TEMPLATES = [ { @@ -339,6 +344,9 @@ }, } +csrf_strings = os.environ.get("CSRF_DOMAINS", "") +CSRF_TRUSTED_ORIGINS = [] if len(csrf_strings) == 0 else csrf_strings.split(',') + # All debug settings if DEBUG and DEV_PHASE in ("local", "stub", "dev"): INSTALLED_APPS += [ @@ -349,6 +357,13 @@ "127.0.0.1", ] + CSRF_TRUSTED_ORIGINS += [ + "https://localhost", + "https://127.0.0.1", + "http://localhost", + "http://127.0.0.1", + ] + CORS_ALLOWED_ORIGINS = [ "https://localhost:3000", "https://127.0.0.1:3000", @@ -359,11 +374,6 @@ # We don't want to create security vulnerabilities through CORS policy. Only allow on dev deployments where the UI may be running on another origin. CORS_ALLOW_CREDENTIALS = True - CSRF_TRUSTED_ORIGINS = [ - "localhost", - "127.0.0.1", - ] - DEBUG_MIDDLEWARE = [ "corsheaders.middleware.CorsMiddleware", "debug_toolbar.middleware.DebugToolbarMiddleware", @@ -389,10 +399,18 @@ "first_name": "givenName", "last_name": "sn", }, + "TRIGGER": { + "CREATE_USER": "core.models.update_user", + }, "ASSERTION_URL": os.environ.get("SAML2_AUTH_ASSERTION_URL"), "ENTITY_ID": os.environ.get( "SAML2_AUTH_ENTITY_ID" ), # Populates the Issuer element in authn request + "USE_JWT": False, + 'WANT_ASSERTIONS_SIGNED': True, + 'AUTHN_REQUESTS_SIGNED': False, + 'WANT_RESPONSE_SIGNED': False, + 'TOKEN_REQUIRED': False, } # Metadata is required, either remote url or local file path, check the environment @@ -407,3 +425,5 @@ SAML2_AUTH["METADATA_AUTO_CONF_URL"] = metadata_source else: SAML2_AUTH["METADATA_LOCAL_FILE_PATH"] = metadata_source else: SAML2_AUTH["METADATA_LOCAL_FILE_PATH"] = metadata_source + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/appstore/appstore/settings/heal_settings.py b/appstore/appstore/settings/heal_settings.py index 16875adfc..861b70816 100644 --- a/appstore/appstore/settings/heal_settings.py +++ b/appstore/appstore/settings/heal_settings.py @@ -8,5 +8,5 @@ title="NIH HEAL Initiative", logo_url="/static/images/heal/logo.png", color_scheme=ProductColorScheme("#8a5a91", "#505057"), - links=None, + links=[], ) diff --git a/appstore/appstore/settings/helx_settings.py b/appstore/appstore/settings/helx_settings.py index 9dbc68308..80b4bbf31 100644 --- a/appstore/appstore/settings/helx_settings.py +++ b/appstore/appstore/settings/helx_settings.py @@ -8,5 +8,5 @@ title="HeLx", logo_url="/static/images/helx/logo.png", color_scheme=ProductColorScheme("#8a5a91", "#505057"), - links=None, + links=[], ) diff --git a/appstore/core/models.py b/appstore/core/models.py index 1135ef126..bae48c4dd 100644 --- a/appstore/core/models.py +++ b/appstore/core/models.py @@ -1,5 +1,17 @@ from django.db import models +from django.contrib.auth import get_user_model +from django_saml2_auth.user import get_user +def update_user(user): + # as of Django_saml2_auth v3.12.0 does not add email address by default + # to the created use entry in django db according to: + # https://github.com/grafana/django-saml2-auth/blob/11b97beaa2a431209e2c54103cb49c033c42ff54/django_saml2_auth/user.py#L93 + # https://github.com/grafana/django-saml2-auth/blob/11b97beaa2a431209e2c54103cb49c033c42ff54/django_saml2_auth/user.py#L165 + # This trigger gets and set the email field in the django user db + _user = get_user(user) + _user.email = user['email'] + _user.save() + return _user class AuthorizedUser(models.Model): email = models.EmailField(max_length=254) diff --git a/appstore/middleware/filter_whitelist_middleware.py b/appstore/middleware/filter_whitelist_middleware.py index ef2affd16..c423cf7eb 100644 --- a/appstore/middleware/filter_whitelist_middleware.py +++ b/appstore/middleware/filter_whitelist_middleware.py @@ -18,9 +18,16 @@ class AllowWhiteListedUserOnly(MiddlewareMixin): + def __init__(self, get_response=None): + if get_response is not None: + self.get_response = get_response + else: + self.get_response = self._get_response + def process_request(self, request): user = request.user logger.debug(f"testing user: {user}") + if user.is_authenticated and not user.is_superuser: if not any( [ @@ -34,7 +41,6 @@ def process_request(self, request): request.path.startswith("/api/v1/providers"), ] ): - if self.is_authorized(user): logger.debug(f"Adding user {user} to whitelist") whitelist_group = Group.objects.get(name="whitelisted") @@ -56,6 +62,17 @@ def process_request(self, request): logger.info(f"accepting user {user}") return None + def _get_response(self, request): + """ + Call the next middleware in the chain to get a response. + """ + # Call the next middleware in the chain to get a response + if hasattr(self, 'process_response'): + return self.process_response(request) + else: + # If there's no process_response method, return None + return None + @staticmethod def is_whitelisted(user): if user.groups.filter(name="whitelisted").exists(): diff --git a/appstore/middleware/tests.py b/appstore/middleware/tests.py index 873d47c54..43f8ff117 100644 --- a/appstore/middleware/tests.py +++ b/appstore/middleware/tests.py @@ -48,6 +48,7 @@ def _create_user_and_login( return user def test_login_whitelisted_user(self): + print("---- TESTING FOR WHITELISTED USER (steve_whitelist) ----- ") user = self._create_user_and_login( username="Steve_whitelist", email="steve@renci.com", password="admin" ) @@ -55,7 +56,7 @@ def test_login_whitelisted_user(self): self.request.user = user self.request.session = self.client.session response = self.middleware.process_request(self.request) - self.assertTrue(isinstance(response, HttpResponseRedirect)) + self.assertFalse(isinstance(response, HttpResponseRedirect)) self.assertEqual( list(self.request.user.groups.values_list("name", flat=True))[0], self.groups.name, diff --git a/requirements.txt b/requirements.txt index 7d6acb3c9..1ef691b25 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,23 +1,23 @@ -Django==3.2.23 -django-allauth==0.54.0 -django-cors-headers==3.7.0 -django-crispy-forms==1.11.2 -django-debug-toolbar==3.2 -django-extensions==3.1.1 -django-saml2-auth==2.2.1 -djangorestframework==3.12.2 -drf-spectacular==0.15.1 +Django==4.2 +django-allauth==0.61.1 +django-cors-headers==4.3.1 +django-crispy-forms==2.1 +django-debug-toolbar==4.3.0 +django-extensions==3.2.3 +grafana-django-saml2-auth==3.12.0 +djangorestframework==3.14.0 +drf-spectacular==0.27.1 flake8==3.9.0 gunicorn==20.1.0 mock==4.0.2 -pysaml2 -python3-openid==3.1.0 +pysaml2==7.4.2 +python3-openid==3.2.0 requests==2.31.0 -requests-oauthlib +requests-oauthlib==1.4.0 selenium==3.141.0 tycho-api>=1.17 webdriver-manager==3.2.1 -sqlparse==0.4.2 -asgiref==3.4.1 -psycopg2-binary -python-irodsclient==1.1.5 \ No newline at end of file +sqlparse==0.4.4 +asgiref==3.7.2 +psycopg[binary] +python-irodsclient==1.1.5