Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Admin: Add Google SSO for Compiler users #1855

Merged
merged 23 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1ce210d
feat(admin): first pass @ django_google_sso; allow compiler.la domains
machikoyasuda Jan 17, 2024
60d3d95
fix(settings): allow wikimedia link
machikoyasuda Jan 17, 2024
33edf7d
fix(pyproject): require django-google-sso
machikoyasuda Jan 17, 2024
e1a61c9
fix(csp): add admin.js files, add google sso user icons to allowlist
machikoyasuda Jan 17, 2024
f603890
fix(tests): only install django-google-sso, only add sso url if admin
machikoyasuda Jan 17, 2024
7360fd1
fix(settings): move app to installed_apps if admin
machikoyasuda Jan 17, 2024
bbb64a1
feat(admin): fetch and save admin user's ggoogle email, first and las…
machikoyasuda Jan 17, 2024
575ef39
refactor(settings): save allowable_domains as a dictionary in Terraform
machikoyasuda Jan 17, 2024
b8e0acb
refactor(settings): add google sso svg, remove wikimedia from csp
machikoyasuda Jan 17, 2024
35a6a46
fix(pyproject): unpin version
machikoyasuda Jan 17, 2024
8c21ca2
fix(pyproject): use ==
machikoyasuda Jan 17, 2024
de6f66d
feat(admin): create allow list for staff and admin + terraform vars
machikoyasuda Jan 19, 2024
38e9a07
fix(settings): remove 120.. from allowed_hosts
machikoyasuda Jan 30, 2024
0bee9a9
refactor(sso): use requests, not httpx
machikoyasuda Jan 30, 2024
660f269
feat(test): test in progress
machikoyasuda Feb 2, 2024
0d5984e
fix(tests): use mocker; fixed tests yay
machikoyasuda Feb 6, 2024
2e28ec7
chore: undo test changes
machikoyasuda Feb 6, 2024
a0c4b2e
refactor(admin): move code to Admin.py
machikoyasuda Feb 6, 2024
4f98e63
fix(settings): remove if ADMIN checks
machikoyasuda Feb 6, 2024
0c28146
fix(test): fix test
machikoyasuda Feb 7, 2024
5c00e19
fix: rename to core, remove unused file
machikoyasuda Feb 7, 2024
49c78a3
fix(tests): remove tests, un-remove admin check
machikoyasuda Feb 7, 2024
147cd51
fix: undo admin fixture
machikoyasuda Feb 7, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions benefits/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import requests

from django.conf import settings
from loguru import logger


def pre_login_user(user, request):
thekaveman marked this conversation as resolved.
Show resolved Hide resolved
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)
thekaveman marked this conversation as resolved.
Show resolved Hide resolved
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()
thekaveman marked this conversation as resolved.
Show resolved Hide resolved
26 changes: 24 additions & 2 deletions benefits/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def _filter_empty(ls):

ADMIN = os.environ.get("DJANGO_ADMIN", "False").lower() == "true"
thekaveman marked this conversation as resolved.
Show resolved Hide resolved

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

Expand All @@ -37,11 +37,27 @@ 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 = _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
]
)

Expand Down Expand Up @@ -282,7 +298,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:",
"*.googleusercontent.com",
]

# Configuring strict Content Security Policy
# https://django-csp.readthedocs.io/en/latest/nonce.html
Expand All @@ -294,9 +314,11 @@ 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",
"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)
Expand Down
7 changes: 7 additions & 0 deletions benefits/static/img/icon/google_sso_logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions benefits/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,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")
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 9 additions & 0 deletions terraform/app_service.tf
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,15 @@ 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",
"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)",
"SENTRY_ENVIRONMENT" = local.env_name,
Expand Down
22 changes: 20 additions & 2 deletions tests/pytest/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
thekaveman marked this conversation as resolved.
Show resolved Hide resolved


@pytest.fixture
Expand All @@ -32,6 +35,21 @@ def app_request(rf):
return app_request


@pytest.fixture
def model_AdminUser():
user = User.objects.create(
email="[email protected]",
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(
Expand Down
31 changes: 31 additions & 0 deletions tests/pytest/test_admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import pytest
thekaveman marked this conversation as resolved.
Show resolved Hide resolved

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 == "[email protected]"
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": "[email protected]",
"given_name": "Admin",
"family_name": "User",
"email": "[email protected]",
}
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"]
Loading