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 logging
import datetime
from testfixtures import LogCapture
import httpretty
import jwt
import mock
from django.core.cache import cache
......@@ -18,10 +14,7 @@ from django.test.utils import override_settings
from django_dynamic_fixture import G
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 courses.permissions import set_user_course_permissions, user_can_view_course, get_user_course_permissions
......@@ -197,148 +190,6 @@ class LogoutViewTests(RedirectTestCaseMixin, UserTestCaseMixin, TestCase):
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):
auto_auth_path = reverse_lazy('auto_auth')
......
......@@ -71,11 +71,12 @@ def refresh_user_course_permissions(user):
raise UserNotAssociatedWithBackendError
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:
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
# issue with the backend provider.
......@@ -88,14 +89,14 @@ def refresh_user_course_permissions(user):
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."""
# 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
# `settings.COURSE_PERMISSIONS_CLAIMS` is a list of the claims that contain the courses.
try:
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:
raise PermissionsRetrievalFailedError(e)
......@@ -132,9 +133,12 @@ def user_can_view_course(user, course_id):
"""
Returns boolean indicating if specified user can view specified course.
Arguments
Arguments:
user (User) -- User whose permissions are being checked
course_id (str) -- Course to check
Returns:
bool -- True, if user can view course; otherwise, False.
"""
if user.is_superuser:
......
import logging
from auth_backends.backends import EdXOpenIdConnect
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.cache import cache
from django.test import TestCase
......@@ -177,3 +179,12 @@ class PermissionsTests(TestCase):
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):
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
djangorestframework==3.3.1 # BSD
django-soapbox==1.1 # BSD
django-waffle==0.10 # BSD
edx-auth-backends==0.2.1
edx-rest-api-client>=1.5.0, <1.6.0 # Apache
# other versions cause a segment fault when running compression
libsass==0.5.1 # MIT
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.
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