Commit 335726eb by Clinton Blackburn

Updated edx-auth-backends to 0.2.1

This version of the package stores the token_type in the User.extra_data. Backend tests have been removed.

ECOM-4263
parent 3781546a
from django.conf import settings
from social.tests.backends.oauth import OAuth2Test
from social.tests.backends.open_id import OpenIdConnectTestMixin
from courses.permissions import get_user_course_permissions
DUMMY_AUTHORIZED_COURSE = 'dummy/course/id'
class EdXOpenIdConnectTests(OpenIdConnectTestMixin, OAuth2Test):
backend_path = 'auth_backends.backends.EdXOpenIdConnect'
issuer = settings.SOCIAL_AUTH_EDX_OIDC_URL_ROOT
expected_username = 'test_user'
def get_id_token(self, *args, **kwargs):
data = super(EdXOpenIdConnectTests, self).get_id_token(*args, **kwargs)
# Set the field used to derive the username of the logged user.
data['preferred_username'] = self.expected_username
# Include a dummy list of authorized courses.
claim_name = settings.COURSE_PERMISSIONS_CLAIMS[0]
data[claim_name] = [DUMMY_AUTHORIZED_COURSE]
return data
def test_course_permissions(self):
user = self.do_login()
authorized_courses = get_user_course_permissions(user)
self.assertEqual(len(authorized_courses), 1)
self.assertIn(DUMMY_AUTHORIZED_COURSE, authorized_courses)
from calendar import timegm
import json import json
import logging import logging
import datetime
from testfixtures import LogCapture from testfixtures import LogCapture
import httpretty
import jwt
import mock import mock
from django.core.cache import cache from django.core.cache import cache
...@@ -18,10 +14,7 @@ from django.test.utils import override_settings ...@@ -18,10 +14,7 @@ from django.test.utils import override_settings
from django_dynamic_fixture import G from django_dynamic_fixture import G
from analyticsclient.exceptions import TimeoutError from analyticsclient.exceptions import TimeoutError
from social.exceptions import AuthException
from social.utils import parse_qs
from auth_backends.backends import EdXOpenIdConnect
from core.views import OK, UNAVAILABLE from core.views import OK, UNAVAILABLE
from courses.permissions import set_user_course_permissions, user_can_view_course, get_user_course_permissions from courses.permissions import set_user_course_permissions, user_can_view_course, get_user_course_permissions
...@@ -197,148 +190,6 @@ class LogoutViewTests(RedirectTestCaseMixin, UserTestCaseMixin, TestCase): ...@@ -197,148 +190,6 @@ class LogoutViewTests(RedirectTestCaseMixin, UserTestCaseMixin, TestCase):
self.assertRedirectsNoFollow(response, reverse('login')) self.assertRedirectsNoFollow(response, reverse('login'))
@override_settings(SOCIAL_AUTH_EDX_OIDC_KEY='123',
SOCIAL_AUTH_EDX_OIDC_SECRET='abc',
SOCIAL_AUTH_EDX_OIDC_ID_TOKEN_DECRYPTION_KEY='abc')
class OpenIdConnectTests(UserTestCaseMixin, RedirectTestCaseMixin, TestCase):
DEFAULT_USERNAME = 'edx'
backend_name = 'edx-oidc'
backend_class = EdXOpenIdConnect
user_is_administrator = False
def setUp(self):
super(OpenIdConnectTests, self).setUp()
self.oauth2_init_path = reverse('social:begin', args=[self.backend_name])
def _access_token_body(self, request, _url, headers, username):
nonce = parse_qs(request.body).get('nonce')
body = json.dumps(self.get_access_token_response(nonce, username))
return 200, headers, body
# pylint: disable=unused-argument
def get_access_token_response(self, nonce, username):
client_secret = settings.SOCIAL_AUTH_EDX_OIDC_SECRET
access_token = {
'access_token': '12345',
'refresh_token': 'abcde',
'expires_in': 900,
'preferred_username': username,
'id_token': jwt.encode(self.get_id_token(nonce, username), client_secret).decode('utf-8')
}
return access_token
def get_id_token(self, nonce, username):
client_key = settings.SOCIAL_AUTH_EDX_OIDC_KEY
now = datetime.datetime.utcnow()
expiration_datetime = now + datetime.timedelta(seconds=30)
issue_datetime = now
id_token = {
'iss': EdXOpenIdConnect.ID_TOKEN_ISSUER,
'nonce': nonce,
'aud': client_key,
'azp': client_key,
'exp': timegm(expiration_datetime.utctimetuple()),
'iat': timegm(issue_datetime.utctimetuple()),
'sub': '1234',
'preferred_username': username,
'email': 'edx@example.org',
'name': 'Ed Xavier',
'given_name': 'Ed',
'family_name': 'Xavier',
'locale': 'en_US',
'administrator': self.user_is_administrator
}
return id_token
@httpretty.activate
def _check_oauth2_handshake(self, username=DEFAULT_USERNAME, failure=False):
""" Performs an OAuth2 handshake to login a user.
Arguments:
username -- Username of the user to login (or create if one does not exist)
failure -- Determines if handshake should fail
"""
# Generate an OAuth2 request
response = self.client.get(self.oauth2_init_path)
self.assertEqual(response.status_code, 302)
state = self.client.session['{}_state'.format(self.backend_name)]
# Mock the access token POST body
httpretty.register_uri(httpretty.POST, self.backend_class.ACCESS_TOKEN_URL,
body=lambda request, url, headers: self._access_token_body(request, url, headers,
username))
# Send the response to this application's OAuth2 consumer URL
oauth2_complete_path = '{0}?state={1}'.format(reverse('social:complete', args=[self.backend_name]), state)
if failure:
oauth2_complete_path += '&error=access_denied'
self.assertRaises(AuthException, self.client.get, oauth2_complete_path)
else:
response = self.client.get(oauth2_complete_path)
self.assertEqual(response.status_code, 302)
self.assertEqual(response['Location'], 'http://testserver{}'.format(settings.LOGIN_REDIRECT_URL))
def test_new_user(self):
"""
A new user should be created if the username from the OAuth2 provider is not linked to an existing account.
"""
original_user_count = User.objects.count()
self._check_oauth2_handshake()
# Verify new user created
self.assertEqual(User.objects.count(), original_user_count + 1)
user = self.get_latest_user()
self.assertEqual(user.username, self.DEFAULT_USERNAME)
self.assertUserLoggedIn(user)
def test_existing_user(self):
"""
Verify system logs in a user (and does not create a new account) when the username from the OAuth2 provider
matches an existing account in the system.
"""
user = self.user
original_user_count = User.objects.count()
self._check_oauth2_handshake(user.username)
# Verify no new users created
self.assertEqual(User.objects.count(), original_user_count)
self.assertUserLoggedIn(user)
def test_access_denied(self):
self._check_oauth2_handshake(failure=True)
def test_user_details(self):
# Create a new user
self.test_new_user()
user = self.get_latest_user()
# Validate the user's details
self.assertEqual(user.username, 'edx')
self.assertEqual(user.email, 'edx@example.org')
self.assertEqual(user.first_name, 'Ed')
self.assertEqual(user.last_name, 'Xavier')
self.assertEqual(user.language, 'en-us')
def test_administrator(self):
# Create an administrator via OAuth2
self.user_is_administrator = True
self._check_oauth2_handshake()
user = self.get_latest_user()
self.assertTrue(user.is_superuser)
self.assertTrue(user.is_staff)
class AutoAuthTests(UserTestCaseMixin, TestCase): class AutoAuthTests(UserTestCaseMixin, TestCase):
auto_auth_path = reverse_lazy('auto_auth') auto_auth_path = reverse_lazy('auto_auth')
......
...@@ -71,11 +71,12 @@ def refresh_user_course_permissions(user): ...@@ -71,11 +71,12 @@ def refresh_user_course_permissions(user):
raise UserNotAssociatedWithBackendError raise UserNotAssociatedWithBackendError
access_token = user_social_auth.extra_data.get('access_token') access_token = user_social_auth.extra_data.get('access_token')
token_type = user_social_auth.extra_data.get('token_type', 'Bearer')
if not access_token: if not access_token:
raise InvalidAccessTokenError raise InvalidAccessTokenError
courses = _get_user_courses(access_token, backend) courses = _get_user_courses(access_token, token_type, backend)
# If the backend does not provide course permissions, assign no permissions and log a warning as there may be an # If the backend does not provide course permissions, assign no permissions and log a warning as there may be an
# issue with the backend provider. # issue with the backend provider.
...@@ -88,14 +89,14 @@ def refresh_user_course_permissions(user): ...@@ -88,14 +89,14 @@ def refresh_user_course_permissions(user):
return courses return courses
def _get_user_courses(access_token, backend): def _get_user_courses(access_token, token_type, backend):
""" Return a list of courses that the user has access to.""" """ Return a list of courses that the user has access to."""
# The authorized courses can come form different claims according to the user role. For example there could be a # The authorized courses can come form different claims according to the user role. For example there could be a
# list of courses the user has access as staff and another that the user has access as instructor. The variable # list of courses the user has access as staff and another that the user has access as instructor. The variable
# `settings.COURSE_PERMISSIONS_CLAIMS` is a list of the claims that contain the courses. # `settings.COURSE_PERMISSIONS_CLAIMS` is a list of the claims that contain the courses.
try: try:
claims = settings.COURSE_PERMISSIONS_CLAIMS claims = settings.COURSE_PERMISSIONS_CLAIMS
data = backend.get_user_claims(access_token, claims) data = backend.get_user_claims(access_token, claims, token_type=token_type)
except Exception as e: except Exception as e:
raise PermissionsRetrievalFailedError(e) raise PermissionsRetrievalFailedError(e)
...@@ -132,9 +133,12 @@ def user_can_view_course(user, course_id): ...@@ -132,9 +133,12 @@ def user_can_view_course(user, course_id):
""" """
Returns boolean indicating if specified user can view specified course. Returns boolean indicating if specified user can view specified course.
Arguments Arguments:
user (User) -- User whose permissions are being checked user (User) -- User whose permissions are being checked
course_id (str) -- Course to check course_id (str) -- Course to check
Returns:
bool -- True, if user can view course; otherwise, False.
""" """
if user.is_superuser: if user.is_superuser:
......
import logging import logging
from auth_backends.backends import EdXOpenIdConnect
from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.cache import cache from django.core.cache import cache
from django.test import TestCase from django.test import TestCase
...@@ -177,3 +179,12 @@ class PermissionsTests(TestCase): ...@@ -177,3 +179,12 @@ class PermissionsTests(TestCase):
G(UserSocialAuth, user=self.user, provider='edx-oidc', extra_data={'access_token': '1234'}) G(UserSocialAuth, user=self.user, provider='edx-oidc', extra_data={'access_token': '1234'})
with mock.patch('auth_backends.backends.EdXOpenIdConnect.get_json', side_effect=Exception): with mock.patch('auth_backends.backends.EdXOpenIdConnect.get_json', side_effect=Exception):
self.assertRaises(PermissionsRetrievalFailedError, permissions.get_user_course_permissions, self.user) self.assertRaises(PermissionsRetrievalFailedError, permissions.get_user_course_permissions, self.user)
def test_on_auth_complete(self):
""" Verify the function receives the auth_complete_signal signal, and updates course permissions. """
permissions.set_user_course_permissions(self.user, [])
self.assertFalse(permissions.user_can_view_course(self.user, self.course_id))
id_token = {claim: [self.course_id] for claim in settings.COURSE_PERMISSIONS_CLAIMS}
EdXOpenIdConnect.auth_complete_signal.send(None, user=self.user, id_token=id_token)
self.assertTrue(permissions.user_can_view_course(self.user, self.course_id))
...@@ -10,16 +10,16 @@ django-model-utils==1.5.0 # BSD ...@@ -10,16 +10,16 @@ django-model-utils==1.5.0 # BSD
djangorestframework==3.3.1 # BSD djangorestframework==3.3.1 # BSD
django-soapbox==1.1 # BSD django-soapbox==1.1 # BSD
django-waffle==0.10 # BSD django-waffle==0.10 # BSD
edx-auth-backends==0.2.1
edx-rest-api-client>=1.5.0, <1.6.0 # Apache edx-rest-api-client>=1.5.0, <1.6.0 # Apache
# other versions cause a segment fault when running compression # other versions cause a segment fault when running compression
libsass==0.5.1 # MIT libsass==0.5.1 # MIT
logutils==0.3.3 # BSD logutils==0.3.3 # BSD
# TODO Remove this once https://github.com/omab/python-social-auth/pull/908 is released.
python-social-auth==0.2.14
# versions above 0.2.3 introduce a breaking change
python-social-auth==0.2.3
edx-auth-backends==0.1.2 # AGPL
# TODO Use the PyPi package once it is updated. # TODO Use the PyPi package once it is updated.
git+https://github.com/pinax/django-announcements.git@f85e690705e038a62407abe54ac195f60760934b#egg=django-announcements # MIT git+https://github.com/pinax/django-announcements.git@f85e690705e038a62407abe54ac195f60760934b#egg=django-announcements # MIT
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment