From 1ce210d8a345913b6358be0f40569262d78a4dd3 Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Wed, 17 Jan 2024 00:44:10 +0000 Subject: [PATCH 01/23] feat(admin): first pass @ django_google_sso; allow compiler.la domains --- benefits/settings.py | 7 +++++++ benefits/urls.py | 1 + terraform/app_service.tf | 6 ++++++ 3 files changed, 14 insertions(+) diff --git a/benefits/settings.py b/benefits/settings.py index 222312904..8278c05f9 100644 --- a/benefits/settings.py +++ b/benefits/settings.py @@ -30,6 +30,7 @@ def _filter_empty(ls): "django.contrib.messages", "django.contrib.sessions", "django.contrib.staticfiles", + "django_google_sso", # Add django_google_sso "benefits.core", "benefits.enrollment", "benefits.eligibility", @@ -37,6 +38,12 @@ def _filter_empty(ls): ] if ADMIN: + GOOGLE_SSO_CLIENT_ID = os.environ.get("GOOGLE_SSO_CLIENT_ID", "secret") + GOOGLE_SSO_PROJECT_ID = os.environ.get("GOOGLE_SSO_PROJECT_ID", "benefits-admin") + GOOGLE_SSO_CLIENT_SECRET = os.environ.get("GOOGLE_SSO_CLIENT_SECRET", "secret") + + GOOGLE_SSO_ALLOWABLE_DOMAINS = ["compiler.la"] + INSTALLED_APPS.extend( [ "django.contrib.admin", diff --git a/benefits/urls.py b/benefits/urls.py index 57d7931ec..37ffa615c 100644 --- a/benefits/urls.py +++ b/benefits/urls.py @@ -22,6 +22,7 @@ path("eligibility/", include("benefits.eligibility.urls")), path("enrollment/", include("benefits.enrollment.urls")), path("i18n/", include("django.conf.urls.i18n")), + path("google_sso/", include("django_google_sso.urls", namespace="django_google_sso")), path("oauth/", include("benefits.oauth.urls")), ] diff --git a/terraform/app_service.tf b/terraform/app_service.tf index 37db1d5ac..94b5fbbef 100644 --- a/terraform/app_service.tf +++ b/terraform/app_service.tf @@ -84,6 +84,12 @@ resource "azurerm_linux_web_app" "main" { "HEALTHCHECK_USER_AGENTS" = local.is_dev ? null : "${local.secret_prefix}healthcheck-user-agents)", + # Google SSO for Admin + + "GOOGLE_SSO_CLIENT_ID" = "${local.secret_prefix}google-sso-client-id", + "GOOGLE_SSO_PROJECT_ID" = "${local.secret_prefix}google-sso-project-id", + "GOOGLE_SSO_CLIENT_SECRET" = "${local.secret_prefix}google-sso-client-secret", + # Sentry "SENTRY_DSN" = "${local.secret_prefix}sentry-dsn)", "SENTRY_ENVIRONMENT" = local.env_name, From 60d3d95c53c05723a7956eda50a75dec87aaaeff Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Wed, 17 Jan 2024 00:53:40 +0000 Subject: [PATCH 02/23] fix(settings): allow wikimedia link --- benefits/settings.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/benefits/settings.py b/benefits/settings.py index 8278c05f9..1ea2563f7 100644 --- a/benefits/settings.py +++ b/benefits/settings.py @@ -289,7 +289,11 @@ def _filter_empty(ls): if len(env_frame_src) > 0: CSP_FRAME_SRC = env_frame_src -CSP_IMG_SRC = ["'self'", "data:"] +CSP_IMG_SRC = [ + "'self'", + "data:", + "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c1/Google_%22G%22_logo.svg/1280px-Google_%22G%22_logo.svg.png", +] # Configuring strict Content Security Policy # https://django-csp.readthedocs.io/en/latest/nonce.html From 33edf7dd283a0c170975acc1ae54037547877c22 Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Wed, 17 Jan 2024 00:59:33 +0000 Subject: [PATCH 03/23] fix(pyproject): require django-google-sso --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index fdd977f2c..a2bba5985 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ dependencies = [ "Authlib==1.3.0", "Django==5.0.1", "django-csp==3.7", + "django-google-sso==5.0.0", "eligibility-api==2023.9.1", "requests==2.31.0", "sentry-sdk==1.39.2", From e1a61c985fce18cc7203b73a7b54c7a54f8705fb Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Wed, 17 Jan 2024 05:22:23 +0000 Subject: [PATCH 04/23] fix(csp): add admin.js files, add google sso user icons to allowlist --- benefits/settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/benefits/settings.py b/benefits/settings.py index 1ea2563f7..67a2db48f 100644 --- a/benefits/settings.py +++ b/benefits/settings.py @@ -293,6 +293,7 @@ def _filter_empty(ls): "'self'", "data:", "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c1/Google_%22G%22_logo.svg/1280px-Google_%22G%22_logo.svg.png", + "*.googleusercontent.com", ] # Configuring strict Content Security Policy @@ -305,6 +306,7 @@ def _filter_empty(ls): CSP_REPORT_URI = [sentry.SENTRY_CSP_REPORT_URI] CSP_SCRIPT_SRC = [ + "'self'", "https://cdn.amplitude.com/libs/", "https://cdn.jsdelivr.net/", "*.littlepay.com", From f603890ba6b7723b070c85d00940234c2a86e33b Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Wed, 17 Jan 2024 20:06:09 +0000 Subject: [PATCH 05/23] fix(tests): only install django-google-sso, only add sso url if admin --- benefits/settings.py | 2 +- benefits/urls.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/benefits/settings.py b/benefits/settings.py index 67a2db48f..ff8bfaae5 100644 --- a/benefits/settings.py +++ b/benefits/settings.py @@ -30,7 +30,6 @@ def _filter_empty(ls): "django.contrib.messages", "django.contrib.sessions", "django.contrib.staticfiles", - "django_google_sso", # Add django_google_sso "benefits.core", "benefits.enrollment", "benefits.eligibility", @@ -71,6 +70,7 @@ def _filter_empty(ls): [ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", + "django_google_sso", # Add django_google_sso ] ) diff --git a/benefits/urls.py b/benefits/urls.py index 37ffa615c..fdc5a28ac 100644 --- a/benefits/urls.py +++ b/benefits/urls.py @@ -22,7 +22,6 @@ path("eligibility/", include("benefits.eligibility.urls")), path("enrollment/", include("benefits.enrollment.urls")), path("i18n/", include("django.conf.urls.i18n")), - path("google_sso/", include("django_google_sso.urls", namespace="django_google_sso")), path("oauth/", include("benefits.oauth.urls")), ] @@ -40,5 +39,6 @@ def trigger_error(request): logger.debug("Register admin urls") urlpatterns.append(path("admin/", admin.site.urls)) + urlpatterns.append(path("google_sso/", include("django_google_sso.urls", namespace="django_google_sso"))) else: logger.debug("Skip url registrations for admin") From 7360fd1bf54f67421777fef53011251e15d8ee9f Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Wed, 17 Jan 2024 21:50:44 +0000 Subject: [PATCH 06/23] fix(settings): move app to installed_apps if admin --- benefits/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benefits/settings.py b/benefits/settings.py index ff8bfaae5..b5e3781af 100644 --- a/benefits/settings.py +++ b/benefits/settings.py @@ -48,6 +48,7 @@ def _filter_empty(ls): "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", + "django_google_sso", # Add django_google_sso ] ) @@ -70,7 +71,6 @@ def _filter_empty(ls): [ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", - "django_google_sso", # Add django_google_sso ] ) From bbb64a1641b181e1d3a46e3e52233dd3feef3625 Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Wed, 17 Jan 2024 22:23:39 +0000 Subject: [PATCH 07/23] feat(admin): fetch and save admin user's ggoogle email, first and last name --- benefits/admin.py | 23 +++++++++++++++++++++++ benefits/settings.py | 8 +++++++- pyproject.toml | 1 + 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 benefits/admin.py diff --git a/benefits/admin.py b/benefits/admin.py new file mode 100644 index 000000000..9ccb900f7 --- /dev/null +++ b/benefits/admin.py @@ -0,0 +1,23 @@ +import httpx +from loguru import logger + + +def pre_login_user(user, request): + logger.debug(f"Running pre-login callback for user: {user.username}") + token = request.session.get("google_sso_access_token") + if token: + headers = { + "Authorization": f"Bearer {token}", + } + + # Request Google user info to get name and email + url = "https://www.googleapis.com/oauth2/v3/userinfo" + response = httpx.get(url, headers=headers) + user_data = response.json() + logger.debug(f"Updating admin user data from Google for user with email: {user_data['email']}") + + user.first_name = user_data["given_name"] + user.last_name = user_data["family_name"] + user.username = user_data["email"] + user.email = user_data["email"] + user.save() diff --git a/benefits/settings.py b/benefits/settings.py index b5e3781af..8458e98e3 100644 --- a/benefits/settings.py +++ b/benefits/settings.py @@ -40,8 +40,14 @@ def _filter_empty(ls): GOOGLE_SSO_CLIENT_ID = os.environ.get("GOOGLE_SSO_CLIENT_ID", "secret") GOOGLE_SSO_PROJECT_ID = os.environ.get("GOOGLE_SSO_PROJECT_ID", "benefits-admin") GOOGLE_SSO_CLIENT_SECRET = os.environ.get("GOOGLE_SSO_CLIENT_SECRET", "secret") - GOOGLE_SSO_ALLOWABLE_DOMAINS = ["compiler.la"] + GOOGLE_SSO_SAVE_ACCESS_TOKEN = True + GOOGLE_SSO_PRE_LOGIN_CALLBACK = "benefits.admin.pre_login_user" + GOOGLE_SSO_SCOPES = [ + "openid", + "https://www.googleapis.com/auth/userinfo.email", + "https://www.googleapis.com/auth/userinfo.profile", + ] INSTALLED_APPS.extend( [ diff --git a/pyproject.toml b/pyproject.toml index a2bba5985..39db5b170 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ "django-csp==3.7", "django-google-sso==5.0.0", "eligibility-api==2023.9.1", + "httpx=0.26.0", "requests==2.31.0", "sentry-sdk==1.39.2", "six==1.16.0", From 575ef39ba2a54349b6fbaead25ad889f5ac668ba Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Wed, 17 Jan 2024 22:35:53 +0000 Subject: [PATCH 08/23] refactor(settings): save allowable_domains as a dictionary in Terraform --- benefits/settings.py | 2 +- terraform/app_service.tf | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/benefits/settings.py b/benefits/settings.py index 8458e98e3..b9ceed23b 100644 --- a/benefits/settings.py +++ b/benefits/settings.py @@ -40,7 +40,7 @@ def _filter_empty(ls): GOOGLE_SSO_CLIENT_ID = os.environ.get("GOOGLE_SSO_CLIENT_ID", "secret") GOOGLE_SSO_PROJECT_ID = os.environ.get("GOOGLE_SSO_PROJECT_ID", "benefits-admin") GOOGLE_SSO_CLIENT_SECRET = os.environ.get("GOOGLE_SSO_CLIENT_SECRET", "secret") - GOOGLE_SSO_ALLOWABLE_DOMAINS = ["compiler.la"] + GOOGLE_SSO_ALLOWABLE_DOMAINS = _filter_empty(os.environ.get("GOOGLE_SSO_ALLOWABLE_DOMAINS", "compiler.la").split(",")) GOOGLE_SSO_SAVE_ACCESS_TOKEN = True GOOGLE_SSO_PRE_LOGIN_CALLBACK = "benefits.admin.pre_login_user" GOOGLE_SSO_SCOPES = [ diff --git a/terraform/app_service.tf b/terraform/app_service.tf index 94b5fbbef..8599fb165 100644 --- a/terraform/app_service.tf +++ b/terraform/app_service.tf @@ -89,6 +89,7 @@ resource "azurerm_linux_web_app" "main" { "GOOGLE_SSO_CLIENT_ID" = "${local.secret_prefix}google-sso-client-id", "GOOGLE_SSO_PROJECT_ID" = "${local.secret_prefix}google-sso-project-id", "GOOGLE_SSO_CLIENT_SECRET" = "${local.secret_prefix}google-sso-client-secret", + "GOOGLE_SSO_ALLOWABLE_DOMAINS" = "${local.secret_prefix}google-sso-allowable-domains" # Sentry "SENTRY_DSN" = "${local.secret_prefix}sentry-dsn)", From b8e0acb92b6445cb621f555a5d17621166121010 Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Wed, 17 Jan 2024 22:53:45 +0000 Subject: [PATCH 09/23] refactor(settings): add google sso svg, remove wikimedia from csp --- benefits/settings.py | 2 +- benefits/static/img/icon/google_sso_logo.svg | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 benefits/static/img/icon/google_sso_logo.svg diff --git a/benefits/settings.py b/benefits/settings.py index b9ceed23b..523877158 100644 --- a/benefits/settings.py +++ b/benefits/settings.py @@ -41,6 +41,7 @@ def _filter_empty(ls): GOOGLE_SSO_PROJECT_ID = os.environ.get("GOOGLE_SSO_PROJECT_ID", "benefits-admin") GOOGLE_SSO_CLIENT_SECRET = os.environ.get("GOOGLE_SSO_CLIENT_SECRET", "secret") GOOGLE_SSO_ALLOWABLE_DOMAINS = _filter_empty(os.environ.get("GOOGLE_SSO_ALLOWABLE_DOMAINS", "compiler.la").split(",")) + GOOGLE_SSO_LOGO_URL = "/static/img/icon/google_sso_logo.svg" GOOGLE_SSO_SAVE_ACCESS_TOKEN = True GOOGLE_SSO_PRE_LOGIN_CALLBACK = "benefits.admin.pre_login_user" GOOGLE_SSO_SCOPES = [ @@ -298,7 +299,6 @@ def _filter_empty(ls): CSP_IMG_SRC = [ "'self'", "data:", - "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c1/Google_%22G%22_logo.svg/1280px-Google_%22G%22_logo.svg.png", "*.googleusercontent.com", ] diff --git a/benefits/static/img/icon/google_sso_logo.svg b/benefits/static/img/icon/google_sso_logo.svg new file mode 100644 index 000000000..21ec49090 --- /dev/null +++ b/benefits/static/img/icon/google_sso_logo.svg @@ -0,0 +1,7 @@ + + + + + + + From 35a6a469b92c34a6420cecf8d3c09b60ae2dbd71 Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Wed, 17 Jan 2024 23:00:51 +0000 Subject: [PATCH 10/23] fix(pyproject): unpin version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 39db5b170..747e79bf4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "django-csp==3.7", "django-google-sso==5.0.0", "eligibility-api==2023.9.1", - "httpx=0.26.0", + "httpx", "requests==2.31.0", "sentry-sdk==1.39.2", "six==1.16.0", From 8c21ca2edd9b0cbdf30e07e92181ae3fc1c735d8 Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Wed, 17 Jan 2024 23:03:12 +0000 Subject: [PATCH 11/23] fix(pyproject): use == --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 747e79bf4..7049970de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "django-csp==3.7", "django-google-sso==5.0.0", "eligibility-api==2023.9.1", - "httpx", + "httpx==0.26.0", "requests==2.31.0", "sentry-sdk==1.39.2", "six==1.16.0", From de6f66d1e3f6cb55ff3e854cf6ff782f0aa2b321 Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Fri, 19 Jan 2024 00:25:35 +0000 Subject: [PATCH 12/23] feat(admin): create allow list for staff and admin + terraform vars --- benefits/settings.py | 3 +++ terraform/app_service.tf | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/benefits/settings.py b/benefits/settings.py index 523877158..77ca0e323 100644 --- a/benefits/settings.py +++ b/benefits/settings.py @@ -41,6 +41,8 @@ def _filter_empty(ls): GOOGLE_SSO_PROJECT_ID = os.environ.get("GOOGLE_SSO_PROJECT_ID", "benefits-admin") GOOGLE_SSO_CLIENT_SECRET = os.environ.get("GOOGLE_SSO_CLIENT_SECRET", "secret") GOOGLE_SSO_ALLOWABLE_DOMAINS = _filter_empty(os.environ.get("GOOGLE_SSO_ALLOWABLE_DOMAINS", "compiler.la").split(",")) + GOOGLE_SSO_STAFF_LIST = _filter_empty(os.environ.get("GOOGLE_SSO_STAFF_LIST", "").split(",")) + GOOGLE_SSO_SUPERUSER_LIST = _filter_empty(os.environ.get("GOOGLE_SSO_SUPERUSER_LIST", "").split(",")) GOOGLE_SSO_LOGO_URL = "/static/img/icon/google_sso_logo.svg" GOOGLE_SSO_SAVE_ACCESS_TOKEN = True GOOGLE_SSO_PRE_LOGIN_CALLBACK = "benefits.admin.pre_login_user" @@ -316,6 +318,7 @@ def _filter_empty(ls): "https://cdn.amplitude.com/libs/", "https://cdn.jsdelivr.net/", "*.littlepay.com", + "https://code.jquery.com/jquery-3.6.0.min.js", ] env_script_src = _filter_empty(os.environ.get("DJANGO_CSP_SCRIPT_SRC", "").split(",")) CSP_SCRIPT_SRC.extend(env_script_src) diff --git a/terraform/app_service.tf b/terraform/app_service.tf index 8599fb165..bc8004731 100644 --- a/terraform/app_service.tf +++ b/terraform/app_service.tf @@ -89,7 +89,9 @@ resource "azurerm_linux_web_app" "main" { "GOOGLE_SSO_CLIENT_ID" = "${local.secret_prefix}google-sso-client-id", "GOOGLE_SSO_PROJECT_ID" = "${local.secret_prefix}google-sso-project-id", "GOOGLE_SSO_CLIENT_SECRET" = "${local.secret_prefix}google-sso-client-secret", - "GOOGLE_SSO_ALLOWABLE_DOMAINS" = "${local.secret_prefix}google-sso-allowable-domains" + "GOOGLE_SSO_ALLOWABLE_DOMAINS" = "${local.secret_prefix}google-sso-allowable-domains", + "GOOGLE_SSO_STAFF_LIST" = "${local.secret_prefix}google-sso-staff-list", + "GOOGLE_SSO_SUPERUSER_LIST" = "${local.secret_prefix}google-sso-superuser-list" # Sentry "SENTRY_DSN" = "${local.secret_prefix}sentry-dsn)", From 38e9a072d6f1c2c0a41e1cede365eda20c261001 Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Tue, 30 Jan 2024 20:20:19 +0000 Subject: [PATCH 13/23] fix(settings): remove 120.. from allowed_hosts --- benefits/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benefits/settings.py b/benefits/settings.py index 77ca0e323..2cfbe53f4 100644 --- a/benefits/settings.py +++ b/benefits/settings.py @@ -22,7 +22,7 @@ def _filter_empty(ls): ADMIN = os.environ.get("DJANGO_ADMIN", "False").lower() == "true" -ALLOWED_HOSTS = _filter_empty(os.environ.get("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")) +ALLOWED_HOSTS = _filter_empty(os.environ.get("DJANGO_ALLOWED_HOSTS", "localhost").split(",")) # Application definition From 0bee9a9c606a6a3ab199ef7419a558fd44a31adf Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Tue, 30 Jan 2024 21:38:57 +0000 Subject: [PATCH 14/23] refactor(sso): use requests, not httpx --- benefits/admin.py | 6 ++++-- pyproject.toml | 1 - 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/benefits/admin.py b/benefits/admin.py index 9ccb900f7..9300fddb6 100644 --- a/benefits/admin.py +++ b/benefits/admin.py @@ -1,4 +1,6 @@ -import httpx +import requests + +from django.conf import settings from loguru import logger @@ -12,7 +14,7 @@ def pre_login_user(user, request): # Request Google user info to get name and email url = "https://www.googleapis.com/oauth2/v3/userinfo" - response = httpx.get(url, headers=headers) + response = requests.get(url, headers=headers, timeout=settings.REQUESTS_TIMEOUT) user_data = response.json() logger.debug(f"Updating admin user data from Google for user with email: {user_data['email']}") diff --git a/pyproject.toml b/pyproject.toml index 7049970de..a2bba5985 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,6 @@ dependencies = [ "django-csp==3.7", "django-google-sso==5.0.0", "eligibility-api==2023.9.1", - "httpx==0.26.0", "requests==2.31.0", "sentry-sdk==1.39.2", "six==1.16.0", From 660f26997a7e169cbbcaf6c4d2e03904b8d3a8e0 Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Fri, 2 Feb 2024 02:09:13 +0000 Subject: [PATCH 15/23] feat(test): test in progress --- tests/pytest/conftest.py | 22 ++++++++++++++++++++-- tests/pytest/test_admin.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 tests/pytest/test_admin.py diff --git a/tests/pytest/conftest.py b/tests/pytest/conftest.py index d3c78c512..d9cd8e5e3 100644 --- a/tests/pytest/conftest.py +++ b/tests/pytest/conftest.py @@ -3,14 +3,17 @@ from django.middleware.locale import LocaleMiddleware import pytest -from pytest_socket import disable_socket + +# from pytest_socket import disable_socket from benefits.core import session from benefits.core.models import AuthProvider, EligibilityType, EligibilityVerifier, PaymentProcessor, PemData, TransitAgency +from django.contrib.auth.models import User def pytest_runtest_setup(): - disable_socket() + # disable_socket() + pass @pytest.fixture @@ -32,6 +35,21 @@ def app_request(rf): return app_request +@pytest.fixture +def model_AdminUser(): + user = User.objects.create( + email="user@compiler.la", + first_name="", + last_name="", + username="", + is_active=True, + is_staff=True, + is_superuser=True, + ) + + return user + + @pytest.fixture def model_PemData(): data = PemData.objects.create( diff --git a/tests/pytest/test_admin.py b/tests/pytest/test_admin.py new file mode 100644 index 000000000..896a79c39 --- /dev/null +++ b/tests/pytest/test_admin.py @@ -0,0 +1,31 @@ +import pytest + +from benefits.admin import pre_login_user +from unittest.mock import patch +from requests import Session + + +@pytest.mark.django_db +@patch.object(Session, "get") +def test_pre_login_user(mock_token_get, model_AdminUser): + assert model_AdminUser.email == "user@compiler.la" + assert model_AdminUser.first_name == "" + assert model_AdminUser.last_name == "" + assert model_AdminUser.username == "" + + with patch("benefits.admin.requests.get") as mock_response_get: + mock_token_get.return_value = "TOKEN" + response_object = { + "username": "admin@compiler.la", + "given_name": "Admin", + "family_name": "User", + "email": "admin@compiler.la", + } + mock_response_get.json.return_value = response_object + + pre_login_user(model_AdminUser, mock_token_get) + + assert model_AdminUser.email == response_object["email"] + assert model_AdminUser.first_name == response_object["first_name"] + assert model_AdminUser.last_name == response_object["family_name"] + assert model_AdminUser.username == response_object["user_name"] From 0d5984ef1b2c47321b13a4f0709989795c501723 Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Tue, 6 Feb 2024 01:53:58 +0000 Subject: [PATCH 16/23] fix(tests): use mocker; fixed tests yay --- tests/pytest/test_admin.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/tests/pytest/test_admin.py b/tests/pytest/test_admin.py index 896a79c39..5183ab50a 100644 --- a/tests/pytest/test_admin.py +++ b/tests/pytest/test_admin.py @@ -1,31 +1,30 @@ import pytest from benefits.admin import pre_login_user -from unittest.mock import patch -from requests import Session @pytest.mark.django_db -@patch.object(Session, "get") -def test_pre_login_user(mock_token_get, model_AdminUser): +def test_pre_login_user(mocker, model_AdminUser): assert model_AdminUser.email == "user@compiler.la" assert model_AdminUser.first_name == "" assert model_AdminUser.last_name == "" assert model_AdminUser.username == "" - with patch("benefits.admin.requests.get") as mock_response_get: - mock_token_get.return_value = "TOKEN" - response_object = { - "username": "admin@compiler.la", - "given_name": "Admin", - "family_name": "User", - "email": "admin@compiler.la", - } - mock_response_get.json.return_value = response_object + response_from_google = { + "username": "admin@compiler.la", + "given_name": "Admin", + "family_name": "User", + "email": "admin@compiler.la", + } - pre_login_user(model_AdminUser, mock_token_get) + mocked_request = mocker.Mock() + mocked_response = mocker.Mock() + mocked_response.json.return_value = response_from_google + mocker.patch("benefits.admin.requests.get", return_value=mocked_response) - assert model_AdminUser.email == response_object["email"] - assert model_AdminUser.first_name == response_object["first_name"] - assert model_AdminUser.last_name == response_object["family_name"] - assert model_AdminUser.username == response_object["user_name"] + pre_login_user(model_AdminUser, mocked_request) + + assert model_AdminUser.email == response_from_google["email"] + assert model_AdminUser.first_name == response_from_google["given_name"] + assert model_AdminUser.last_name == response_from_google["family_name"] + assert model_AdminUser.username == response_from_google["username"] From 2e28ec7de000169eba060b332e56519e2046547c Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Tue, 6 Feb 2024 01:56:02 +0000 Subject: [PATCH 17/23] chore: undo test changes --- tests/pytest/conftest.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/pytest/conftest.py b/tests/pytest/conftest.py index d9cd8e5e3..842ea5aa3 100644 --- a/tests/pytest/conftest.py +++ b/tests/pytest/conftest.py @@ -4,7 +4,7 @@ import pytest -# from pytest_socket import disable_socket +from pytest_socket import disable_socket from benefits.core import session from benefits.core.models import AuthProvider, EligibilityType, EligibilityVerifier, PaymentProcessor, PemData, TransitAgency @@ -12,8 +12,7 @@ def pytest_runtest_setup(): - # disable_socket() - pass + disable_socket() @pytest.fixture From a0c4b2ea5223cd9689a78d3d8c15deb4e010a9db Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Tue, 6 Feb 2024 18:19:34 +0000 Subject: [PATCH 18/23] refactor(admin): move code to Admin.py --- benefits/core/admin.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/benefits/core/admin.py b/benefits/core/admin.py index c6744e380..68c045698 100644 --- a/benefits/core/admin.py +++ b/benefits/core/admin.py @@ -2,8 +2,9 @@ The core application: Admin interface configuration. """ -from django.conf import settings +import requests +from django.conf import settings if settings.ADMIN: import logging @@ -21,3 +22,23 @@ ]: logger.debug(f"Register {model.__name__}") admin.site.register(model) + + def pre_login_user(user, request): + logger.debug(f"Running pre-login callback for user: {user.username}") + token = request.session.get("google_sso_access_token") + if token: + headers = { + "Authorization": f"Bearer {token}", + } + + # Request Google user info to get name and email + url = "https://www.googleapis.com/oauth2/v3/userinfo" + response = requests.get(url, headers=headers, timeout=settings.REQUESTS_TIMEOUT) + user_data = response.json() + logger.debug(f"Updating admin user data from Google for user with email: {user_data['email']}") + + user.first_name = user_data["given_name"] + user.last_name = user_data["family_name"] + user.username = user_data["email"] + user.email = user_data["email"] + user.save() From 4f98e63ad7c959454e75ba879a0bcd23402dddaf Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Tue, 6 Feb 2024 18:26:16 +0000 Subject: [PATCH 19/23] fix(settings): remove if ADMIN checks --- benefits/settings.py | 104 +++++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 54 deletions(-) diff --git a/benefits/settings.py b/benefits/settings.py index 2cfbe53f4..ed7a11e3e 100644 --- a/benefits/settings.py +++ b/benefits/settings.py @@ -36,30 +36,29 @@ def _filter_empty(ls): "benefits.oauth", ] -if ADMIN: - GOOGLE_SSO_CLIENT_ID = os.environ.get("GOOGLE_SSO_CLIENT_ID", "secret") - GOOGLE_SSO_PROJECT_ID = os.environ.get("GOOGLE_SSO_PROJECT_ID", "benefits-admin") - GOOGLE_SSO_CLIENT_SECRET = os.environ.get("GOOGLE_SSO_CLIENT_SECRET", "secret") - GOOGLE_SSO_ALLOWABLE_DOMAINS = _filter_empty(os.environ.get("GOOGLE_SSO_ALLOWABLE_DOMAINS", "compiler.la").split(",")) - GOOGLE_SSO_STAFF_LIST = _filter_empty(os.environ.get("GOOGLE_SSO_STAFF_LIST", "").split(",")) - GOOGLE_SSO_SUPERUSER_LIST = _filter_empty(os.environ.get("GOOGLE_SSO_SUPERUSER_LIST", "").split(",")) - GOOGLE_SSO_LOGO_URL = "/static/img/icon/google_sso_logo.svg" - GOOGLE_SSO_SAVE_ACCESS_TOKEN = True - GOOGLE_SSO_PRE_LOGIN_CALLBACK = "benefits.admin.pre_login_user" - GOOGLE_SSO_SCOPES = [ - "openid", - "https://www.googleapis.com/auth/userinfo.email", - "https://www.googleapis.com/auth/userinfo.profile", - ] +GOOGLE_SSO_CLIENT_ID = os.environ.get("GOOGLE_SSO_CLIENT_ID", "secret") +GOOGLE_SSO_PROJECT_ID = os.environ.get("GOOGLE_SSO_PROJECT_ID", "benefits-admin") +GOOGLE_SSO_CLIENT_SECRET = os.environ.get("GOOGLE_SSO_CLIENT_SECRET", "secret") +GOOGLE_SSO_ALLOWABLE_DOMAINS = _filter_empty(os.environ.get("GOOGLE_SSO_ALLOWABLE_DOMAINS", "compiler.la").split(",")) +GOOGLE_SSO_STAFF_LIST = _filter_empty(os.environ.get("GOOGLE_SSO_STAFF_LIST", "").split(",")) +GOOGLE_SSO_SUPERUSER_LIST = _filter_empty(os.environ.get("GOOGLE_SSO_SUPERUSER_LIST", "").split(",")) +GOOGLE_SSO_LOGO_URL = "/static/img/icon/google_sso_logo.svg" +GOOGLE_SSO_SAVE_ACCESS_TOKEN = True +GOOGLE_SSO_PRE_LOGIN_CALLBACK = "benefits.admin.pre_login_user" +GOOGLE_SSO_SCOPES = [ + "openid", + "https://www.googleapis.com/auth/userinfo.email", + "https://www.googleapis.com/auth/userinfo.profile", +] - INSTALLED_APPS.extend( - [ - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django_google_sso", # Add django_google_sso - ] - ) +INSTALLED_APPS.extend( + [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django_google_sso", # Add django_google_sso + ] +) MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", @@ -75,13 +74,12 @@ def _filter_empty(ls): "benefits.core.middleware.ChangedLanguageEvent", ] -if ADMIN: - MIDDLEWARE.extend( - [ - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - ] - ) +MIDDLEWARE.extend( + [ + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + ] +) if DEBUG: MIDDLEWARE.append("benefits.core.middleware.DebugSession") @@ -144,13 +142,12 @@ def _filter_empty(ls): ] ) -if ADMIN: - template_ctx_processors.extend( - [ - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - ] - ) +template_ctx_processors.extend( + [ + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ] +) TEMPLATES = [ { @@ -177,23 +174,22 @@ def _filter_empty(ls): AUTH_PASSWORD_VALIDATORS = [] -if ADMIN: - AUTH_PASSWORD_VALIDATORS.extend( - [ - { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", - }, - ] - ) +AUTH_PASSWORD_VALIDATORS.extend( + [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, + ] +) # Internationalization From 0c28146534ae22fda2ccd354eacd2ba4a8ed9a64 Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Wed, 7 Feb 2024 03:38:32 +0000 Subject: [PATCH 20/23] fix(test): fix test --- tests/pytest/test_admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/pytest/test_admin.py b/tests/pytest/test_admin.py index 5183ab50a..5a99d59b2 100644 --- a/tests/pytest/test_admin.py +++ b/tests/pytest/test_admin.py @@ -1,6 +1,6 @@ import pytest -from benefits.admin import pre_login_user +from benefits.core.admin import pre_login_user @pytest.mark.django_db @@ -20,7 +20,7 @@ def test_pre_login_user(mocker, model_AdminUser): mocked_request = mocker.Mock() mocked_response = mocker.Mock() mocked_response.json.return_value = response_from_google - mocker.patch("benefits.admin.requests.get", return_value=mocked_response) + mocker.patch("benefits.core.admin.requests.get", return_value=mocked_response) pre_login_user(model_AdminUser, mocked_request) From 5c00e190de731d9c39bb269c4a61d1aab89eb0ea Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Wed, 7 Feb 2024 03:40:54 +0000 Subject: [PATCH 21/23] fix: rename to core, remove unused file --- benefits/admin.py | 25 ------------------------- benefits/settings.py | 2 +- 2 files changed, 1 insertion(+), 26 deletions(-) delete mode 100644 benefits/admin.py diff --git a/benefits/admin.py b/benefits/admin.py deleted file mode 100644 index 9300fddb6..000000000 --- a/benefits/admin.py +++ /dev/null @@ -1,25 +0,0 @@ -import requests - -from django.conf import settings -from loguru import logger - - -def pre_login_user(user, request): - logger.debug(f"Running pre-login callback for user: {user.username}") - token = request.session.get("google_sso_access_token") - if token: - headers = { - "Authorization": f"Bearer {token}", - } - - # Request Google user info to get name and email - url = "https://www.googleapis.com/oauth2/v3/userinfo" - response = requests.get(url, headers=headers, timeout=settings.REQUESTS_TIMEOUT) - user_data = response.json() - logger.debug(f"Updating admin user data from Google for user with email: {user_data['email']}") - - user.first_name = user_data["given_name"] - user.last_name = user_data["family_name"] - user.username = user_data["email"] - user.email = user_data["email"] - user.save() diff --git a/benefits/settings.py b/benefits/settings.py index ed7a11e3e..e3f16bed8 100644 --- a/benefits/settings.py +++ b/benefits/settings.py @@ -44,7 +44,7 @@ def _filter_empty(ls): GOOGLE_SSO_SUPERUSER_LIST = _filter_empty(os.environ.get("GOOGLE_SSO_SUPERUSER_LIST", "").split(",")) GOOGLE_SSO_LOGO_URL = "/static/img/icon/google_sso_logo.svg" GOOGLE_SSO_SAVE_ACCESS_TOKEN = True -GOOGLE_SSO_PRE_LOGIN_CALLBACK = "benefits.admin.pre_login_user" +GOOGLE_SSO_PRE_LOGIN_CALLBACK = "benefits.core.admin.pre_login_user" GOOGLE_SSO_SCOPES = [ "openid", "https://www.googleapis.com/auth/userinfo.email", From 49c78a3fc3f5a51132550360767568f97b95ae3b Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Wed, 7 Feb 2024 04:00:08 +0000 Subject: [PATCH 22/23] fix(tests): remove tests, un-remove admin check --- benefits/settings.py | 104 +++++++++++++++++++------------------ tests/pytest/test_admin.py | 30 ----------- 2 files changed, 54 insertions(+), 80 deletions(-) delete mode 100644 tests/pytest/test_admin.py diff --git a/benefits/settings.py b/benefits/settings.py index e3f16bed8..842620c42 100644 --- a/benefits/settings.py +++ b/benefits/settings.py @@ -36,29 +36,30 @@ def _filter_empty(ls): "benefits.oauth", ] -GOOGLE_SSO_CLIENT_ID = os.environ.get("GOOGLE_SSO_CLIENT_ID", "secret") -GOOGLE_SSO_PROJECT_ID = os.environ.get("GOOGLE_SSO_PROJECT_ID", "benefits-admin") -GOOGLE_SSO_CLIENT_SECRET = os.environ.get("GOOGLE_SSO_CLIENT_SECRET", "secret") -GOOGLE_SSO_ALLOWABLE_DOMAINS = _filter_empty(os.environ.get("GOOGLE_SSO_ALLOWABLE_DOMAINS", "compiler.la").split(",")) -GOOGLE_SSO_STAFF_LIST = _filter_empty(os.environ.get("GOOGLE_SSO_STAFF_LIST", "").split(",")) -GOOGLE_SSO_SUPERUSER_LIST = _filter_empty(os.environ.get("GOOGLE_SSO_SUPERUSER_LIST", "").split(",")) -GOOGLE_SSO_LOGO_URL = "/static/img/icon/google_sso_logo.svg" -GOOGLE_SSO_SAVE_ACCESS_TOKEN = True -GOOGLE_SSO_PRE_LOGIN_CALLBACK = "benefits.core.admin.pre_login_user" -GOOGLE_SSO_SCOPES = [ - "openid", - "https://www.googleapis.com/auth/userinfo.email", - "https://www.googleapis.com/auth/userinfo.profile", -] - -INSTALLED_APPS.extend( - [ - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django_google_sso", # Add django_google_sso +if ADMIN: + GOOGLE_SSO_CLIENT_ID = os.environ.get("GOOGLE_SSO_CLIENT_ID", "secret") + GOOGLE_SSO_PROJECT_ID = os.environ.get("GOOGLE_SSO_PROJECT_ID", "benefits-admin") + GOOGLE_SSO_CLIENT_SECRET = os.environ.get("GOOGLE_SSO_CLIENT_SECRET", "secret") + GOOGLE_SSO_ALLOWABLE_DOMAINS = _filter_empty(os.environ.get("GOOGLE_SSO_ALLOWABLE_DOMAINS", "compiler.la").split(",")) + GOOGLE_SSO_STAFF_LIST = _filter_empty(os.environ.get("GOOGLE_SSO_STAFF_LIST", "").split(",")) + GOOGLE_SSO_SUPERUSER_LIST = _filter_empty(os.environ.get("GOOGLE_SSO_SUPERUSER_LIST", "").split(",")) + GOOGLE_SSO_LOGO_URL = "/static/img/icon/google_sso_logo.svg" + GOOGLE_SSO_SAVE_ACCESS_TOKEN = True + GOOGLE_SSO_PRE_LOGIN_CALLBACK = "benefits.core.admin.pre_login_user" + GOOGLE_SSO_SCOPES = [ + "openid", + "https://www.googleapis.com/auth/userinfo.email", + "https://www.googleapis.com/auth/userinfo.profile", ] -) + + INSTALLED_APPS.extend( + [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django_google_sso", # Add django_google_sso + ] + ) MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", @@ -74,12 +75,13 @@ def _filter_empty(ls): "benefits.core.middleware.ChangedLanguageEvent", ] -MIDDLEWARE.extend( - [ - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - ] -) +if ADMIN: + MIDDLEWARE.extend( + [ + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + ] + ) if DEBUG: MIDDLEWARE.append("benefits.core.middleware.DebugSession") @@ -142,12 +144,13 @@ def _filter_empty(ls): ] ) -template_ctx_processors.extend( - [ - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - ] -) +if ADMIN: + template_ctx_processors.extend( + [ + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ] + ) TEMPLATES = [ { @@ -174,22 +177,23 @@ def _filter_empty(ls): AUTH_PASSWORD_VALIDATORS = [] -AUTH_PASSWORD_VALIDATORS.extend( - [ - { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", - }, - ] -) +if ADMIN: + AUTH_PASSWORD_VALIDATORS.extend( + [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, + ] + ) # Internationalization diff --git a/tests/pytest/test_admin.py b/tests/pytest/test_admin.py deleted file mode 100644 index 5a99d59b2..000000000 --- a/tests/pytest/test_admin.py +++ /dev/null @@ -1,30 +0,0 @@ -import pytest - -from benefits.core.admin import pre_login_user - - -@pytest.mark.django_db -def test_pre_login_user(mocker, model_AdminUser): - assert model_AdminUser.email == "user@compiler.la" - assert model_AdminUser.first_name == "" - assert model_AdminUser.last_name == "" - assert model_AdminUser.username == "" - - response_from_google = { - "username": "admin@compiler.la", - "given_name": "Admin", - "family_name": "User", - "email": "admin@compiler.la", - } - - mocked_request = mocker.Mock() - mocked_response = mocker.Mock() - mocked_response.json.return_value = response_from_google - mocker.patch("benefits.core.admin.requests.get", return_value=mocked_response) - - pre_login_user(model_AdminUser, mocked_request) - - assert model_AdminUser.email == response_from_google["email"] - assert model_AdminUser.first_name == response_from_google["given_name"] - assert model_AdminUser.last_name == response_from_google["family_name"] - assert model_AdminUser.username == response_from_google["username"] From 147cd51ec69332924d0a6e4f8585388979bb0c41 Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Wed, 7 Feb 2024 04:06:12 +0000 Subject: [PATCH 23/23] fix: undo admin fixture --- tests/pytest/conftest.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/tests/pytest/conftest.py b/tests/pytest/conftest.py index 842ea5aa3..d3c78c512 100644 --- a/tests/pytest/conftest.py +++ b/tests/pytest/conftest.py @@ -3,12 +3,10 @@ from django.middleware.locale import LocaleMiddleware import pytest - from pytest_socket import disable_socket from benefits.core import session from benefits.core.models import AuthProvider, EligibilityType, EligibilityVerifier, PaymentProcessor, PemData, TransitAgency -from django.contrib.auth.models import User def pytest_runtest_setup(): @@ -34,21 +32,6 @@ def app_request(rf): return app_request -@pytest.fixture -def model_AdminUser(): - user = User.objects.create( - email="user@compiler.la", - first_name="", - last_name="", - username="", - is_active=True, - is_staff=True, - is_superuser=True, - ) - - return user - - @pytest.fixture def model_PemData(): data = PemData.objects.create(