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

Replace old email penn directory with new API #172

Merged
merged 4 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ ENV LANG C.UTF-8

WORKDIR /app/

# Update PGP key for NGINX
# https://blog.nginx.org/blog/updating-pgp-key-for-nginx-software
RUN wget -O/etc/apt/trusted.gpg.d/nginx.asc https://nginx.org/keys/nginx_signing.key

# Install dependencies
RUN apt-get update \
&& apt-get install --no-install-recommends -y python3.11-dev pipenv python3-distutils libpq-dev gcc \
Expand Down
11 changes: 7 additions & 4 deletions backend/Platform/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,13 @@
# Enable admin login through shibboleth
SHIB_ADMIN = True

# Other settings
EMAIL_WEB_SERVICE_URL = os.environ.get("EMAIL_WEB_SERVICE_URL", "http://127.0.0.1")
EMAIL_WEB_SERVICE_USERNAME = os.environ.get("EMAIL_WEB_SERVICE_USERNAME", "")
EMAIL_WEB_SERVICE_PASSWORD = os.environ.get("EMAIL_WEB_SERVICE_PASSWORD", "")
# Email web service
EMAIL_OAUTH_CLIENT_ID = os.environ.get("EMAIL_OAUTH_CLIENT_ID", "")
EMAIL_OAUTH_CLIENT_SECRET = os.environ.get("EMAIL_OAUTH_CLIENT_SECRET", "")
EMAIL_OAUTH_TOKEN_URL = os.environ.get("EMAIL_OAUTH_TOKEN_URL", "http://127.0.0.1")
EMAIL_OAUTH_API_URL_BASE = os.environ.get(
"EMAIL_OAUTH_API_URL_BASE", "http://127.0.0.1"
)

# Twilio Settings

Expand Down
50 changes: 34 additions & 16 deletions backend/accounts/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import RemoteUserBackend
from django.contrib.auth.models import Group
from requests.auth import HTTPBasicAuth

from accounts.models import Email

Expand All @@ -14,23 +15,40 @@ class ShibbolethRemoteUserBackend(RemoteUserBackend):
"""

def get_email(self, pennid):
try:
response = requests.get(
settings.EMAIL_WEB_SERVICE_URL + str(pennid),
auth=(
settings.EMAIL_WEB_SERVICE_USERNAME,
settings.EMAIL_WEB_SERVICE_PASSWORD,
),
)
response = response.json()
response = response["result_data"]

# Check if Penn ID doesn't exist somehow
if len(response) == 0:
return None
"""
Use Penn Directory API with OAuth2 to get the email of a user given their Penn ID.
This is necessary to ensure that we have the correct domain (@seas vs. @wharton, etc.)
for various services like Clubs emails.
"""
auth = HTTPBasicAuth(
settings.EMAIL_OAUTH_CLIENT_ID, settings.EMAIL_OAUTH_CLIENT_SECRET
)
token_response = requests.post(
settings.EMAIL_OAUTH_TOKEN_URL,
auth=auth,
data={"grant_type": "client_credentials"},
)

if token_response.status_code != 200:
return None

return response[0]["email"]
except requests.exceptions.RequestException:
tokens = token_response.json()
access_token = tokens["access_token"]

headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}

api_url = settings.EMAIL_OAUTH_API_URL_BASE + str(pennid)
response = requests.get(api_url, headers=headers)

if response.status_code == 200:
data = response.json()["result_data"]
if not data:
return None
return data[0]["email"]
else:
return None

def authenticate(self, request, remote_user, shibboleth_attributes):
Expand Down
30 changes: 25 additions & 5 deletions backend/tests/accounts/test_backends.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from unittest.mock import patch
from unittest.mock import MagicMock, patch

from django.contrib import auth
from django.contrib.auth import get_user_model
Expand Down Expand Up @@ -108,16 +108,36 @@ def test_create_student_object(self, mock_get_email):
user = auth.authenticate(remote_user=1, shibboleth_attributes=attributes)
self.assertEqual(len(Student.objects.filter(user=user)), 1)

@patch("accounts.backends.requests.post")
@patch("accounts.backends.requests.get")
def test_get_email_exists(self, mock_response):
mock_response.return_value.json.return_value = {
def test_get_email_exists(self, mock_get, mock_post):
mock_response_get = MagicMock()
mock_response_get.status_code = 200
mock_response_get.json.return_value = {
"result_data": [{"email": "[email protected]"}]
}
mock_get.return_value = mock_response_get

mock_response_post = MagicMock()
mock_response_post.status_code = 200
mock_response_post.json.return_value = {"access_token": "my-access-token"}
mock_post.return_value = mock_response_post

backend = ShibbolethRemoteUserBackend()
self.assertEqual(backend.get_email(1), "[email protected]")

@patch("accounts.backends.requests.post")
@patch("accounts.backends.requests.get")
def test_get_email_no_exists(self, mock_response):
mock_response.return_value.json.return_value = {"result_data": []}
def test_get_email_no_exists(self, mock_get, mock_post):
mock_response_get = MagicMock()
mock_response_get.status_code = 200
mock_response_get.json.return_value = {"result_data": []}
mock_get.return_value = mock_response_get

mock_response_post = MagicMock()
mock_response_post.status_code = 200
mock_response_post.json.return_value = {"access_token": "my-access-token"}
mock_post.return_value = mock_response_post

backend = ShibbolethRemoteUserBackend()
self.assertEqual(backend.get_email(1), None)
43 changes: 40 additions & 3 deletions backend/tests/accounts/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os
import sys
from importlib import reload
from unittest.mock import MagicMock, patch
from urllib.parse import quote

from django.conf import settings
Expand Down Expand Up @@ -61,7 +62,19 @@ def test_invalid_shibboleth_response(self):
response = self.client.get(reverse("accounts:login"))
self.assertEqual(response.status_code, 500)

def test_valid_shibboleth(self):
@patch("accounts.backends.requests.post")
@patch("accounts.backends.requests.get")
def test_valid_shibboleth(self, mock_get, mock_post):
mock_response_get = MagicMock()
mock_response_get.status_code = 200
mock_response_get.json.return_value = {"result_data": []}
mock_get.return_value = mock_response_get

mock_response_post = MagicMock()
mock_response_post.status_code = 200
mock_response_post.json.return_value = {"access_token": "my-access-token"}
mock_post.return_value = mock_response_post

headers = {
"HTTP_EMPLOYEENUMBER": "1",
"HTTP_EPPN": "test",
Expand Down Expand Up @@ -121,15 +134,39 @@ def test_get_login_page(self):
response = self.client.get(reverse("accounts:login"))
self.assertEqual(response.status_code, 200)

def test_login_valid_choice(self):
@patch("accounts.backends.requests.post")
@patch("accounts.backends.requests.get")
def test_login_valid_choice(self, mock_get, mock_post):
mock_response_get = MagicMock()
mock_response_get.status_code = 200
mock_response_get.json.return_value = {"result_data": []}
mock_get.return_value = mock_response_get

mock_response_post = MagicMock()
mock_response_post.status_code = 200
mock_response_post.json.return_value = {"access_token": "my-access-token"}
mock_post.return_value = mock_response_post

self.client.post(reverse("accounts:login"), data={"userChoice": 1})
# sample_response = reverse("application:homepage")
expected_user_pennid = 1
actual_user_pennid = int(self.client.session["_auth_user_id"])
self.assertTrue(expected_user_pennid, actual_user_pennid)
# self.assertRedirects(response, sample_response, fetch_redirect_response=False)

def test_login_invalid_choice(self):
@patch("accounts.backends.requests.post")
@patch("accounts.backends.requests.get")
def test_login_invalid_choice(self, mock_get, mock_post):
mock_response_get = MagicMock()
mock_response_get.status_code = 200
mock_response_get.json.return_value = {"result_data": []}
mock_get.return_value = mock_response_get

mock_response_post = MagicMock()
mock_response_post.status_code = 200
mock_response_post.json.return_value = {"access_token": "my-access-token"}
mock_post.return_value = mock_response_post

self.client.post(reverse("accounts:login"), data={"userChoice": 24})
self.assertTrue("_auth_user_id" in self.client.session)

Expand Down
Loading