Skip to content

Commit

Permalink
Pull in custom backend and pipeline steps from analytics-dashboard
Browse files Browse the repository at this point in the history
This includes pared-down unit tests from analytics-dashboard.
  • Loading branch information
Renzo Lucioni committed Feb 19, 2015
1 parent 41430dd commit 7e5b2fd
Show file tree
Hide file tree
Showing 19 changed files with 1,125 additions and 1 deletion.
9 changes: 9 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
language: python
python:
- "2.7"
install:
- pip install -qr requirements.txt
- pip install coveralls
script: ./test.sh
after_success:
coveralls
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Renzo Lucioni <[email protected]>
9 changes: 9 additions & 0 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
How To Contribute
=================

Contributions are very welcome.

Please read `How To Contribute <https://github.com/edx/edx-platform/blob/master/CONTRIBUTING.rst>`_ for details.

Even though it was written with ``edx-platform`` in mind, the guidelines
should be followed for Open edX code in general.
671 changes: 671 additions & 0 deletions LICENSE.txt

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include README.rst
1 change: 0 additions & 1 deletion README.md

This file was deleted.

77 changes: 77 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
auth-backends |Travis|_ |Coveralls|_
=============
.. |Travis| image:: https://travis-ci.org/edx/auth-backends.svg?branch=master
.. _Travis: https://travis-ci.org/edx/auth-backends

.. |Coveralls| image:: https://img.shields.io/coveralls/edx/auth-backends.svg
.. _Coveralls: https://coveralls.io/r/edx/auth-backends?branch=master

This repo houses custom authentication backends and pipeline steps used by edX
projects such as the `edx-analytics-dashboard <https://github.com/edx/edx-analytics-dashboard>`_
and `edx-ecommerce <https://github.com/edx/edx-ecommerce>`_.

This project is new and under active development.

Overview
--------

Included backends:

=============== ============================================
Backend Purpose
--------------- --------------------------------------------
Open ID Connect Authenticate with the LMS, an OIDC provider.
=============== ============================================

This package requires Django 1.7. Required Django settings:

============================================ ============================================
Setting Default
-------------------------------------------- --------------------------------------------
SOCIAL_AUTH_EDX_OIDC_KEY None
SOCIAL_AUTH_EDX_OIDC_SECRET None
SOCIAL_AUTH_EDX_OIDC_ID_TOKEN_DECRYPTION_KEY None
SOCIAL_AUTH_EDX_OIDC_URL_ROOT None
EXTRA_SCOPE []
COURSE_PERMISSIONS_CLAIMS []
============================================ ============================================

Set these to the correct values for your OAuth2/OpenID Connect provider. ``SOCIAL_AUTH_EDX_OIDC_ID_TOKEN_DECRYPTION_KEY``
should be the same as ``SOCIAL_AUTH_EDX_OIDC_SECRET``. Set ``EXTRA_SCOPE`` equal to a list of scope strings to request
additional information from the edX OAuth2 provider at the moment of authentication (e.g., provide course permissions bits
to get a full list of courses).

Testing
-------

Execute ``test.sh`` to run the test suite.

License
-------

The code in this repository is licensed under the AGPL unless otherwise noted.

Please see ``LICENSE.txt`` for details.

How To Contribute
-----------------

Contributions are very welcome!

Please read `How To Contribute <https://github.com/edx/edx-platform/blob/master/CONTRIBUTING.rst>`_ for details.

Even though it was written with `edx-platform <https://github.com/edx/edx-platform>`_ in mind,
the guidelines should be followed for Open edX code in general.

Reporting Security Issues
-------------------------

Please do not report security issues in public. Please email [email protected].

Mailing List and IRC Channel
----------------------------

You can discuss this code on the `edx-code Google Group`__ or in the
``#edx-code`` IRC channel on Freenode.

__ https://groups.google.com/forum/#!forum/edx-code
Empty file added auth_backends/__init__.py
Empty file.
114 changes: 114 additions & 0 deletions auth_backends/backends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"""Django authentication backends.
For more information visit https://docs.djangoproject.com/en/dev/topics/auth/customizing/.
"""
import json

from django.conf import settings
import django.dispatch
from social.backends.open_id import OpenIdConnectAuth


# pylint: disable=abstract-method
class EdXOpenIdConnect(OpenIdConnectAuth):
name = 'edx-oidc'

ACCESS_TOKEN_METHOD = 'POST'
REDIRECT_STATE = False
ID_KEY = 'preferred_username'

DEFAULT_SCOPE = ['openid', 'profile', 'email'] + getattr(settings, 'EXTRA_SCOPE', [])
ID_TOKEN_ISSUER = getattr(settings, 'SOCIAL_AUTH_EDX_OIDC_URL_ROOT', None)
AUTHORIZATION_URL = '{0}/authorize/'.format(ID_TOKEN_ISSUER)
ACCESS_TOKEN_URL = '{0}/access_token/'.format(ID_TOKEN_ISSUER)
USER_INFO_URL = '{0}/user_info/'.format(ID_TOKEN_ISSUER)

PROFILE_TO_DETAILS_KEY_MAP = {
'preferred_username': u'username',
'email': u'email',
'name': u'full_name',
'given_name': u'first_name',
'family_name': u'last_name',
'locale': u'language',
}

auth_complete_signal = django.dispatch.Signal(providing_args=["user", "id_token"])

def user_data(self, _access_token, *_args, **_kwargs):
# Include decoded id_token fields in user data.
return self.id_token

def auth_complete_params(self, state=None):
params = super(EdXOpenIdConnect, self).auth_complete_params(state)

# TODO: Due a limitation in the OIDC provider in the LMS, the list of all course permissions
# is computed during the authentication process. As an optimization, we explicitly request
# the list here, avoiding further roundtrips. This is no longer necessary once the limitation
# is resolved and instead the course permissions can be requested on a need to have basis,
# reducing overhead significantly.
claim_names = getattr(settings, 'COURSE_PERMISSIONS_CLAIMS', [])
courses_claims_request = {name: {'essential': True} for name in claim_names}
params['claims'] = json.dumps({'id_token': courses_claims_request})

return params

def auth_complete(self, *args, **kwargs):
# WARNING: During testing, the user model class is `social.tests.models` and not the one
# specified for the application.
user = super(EdXOpenIdConnect, self).auth_complete(*args, **kwargs)
self.auth_complete_signal.send(sender=self.__class__, user=user, id_token=self.id_token)
return user

def get_user_claims(self, access_token, claims=None):
"""Returns a dictionary with the values for each claim requested."""

data = self.get_json(
self.USER_INFO_URL,
headers={'Authorization': 'Bearer {0}'.format(access_token)}
)

if claims:
claims_names = set(claims)
data = {k: v for (k, v) in data.iteritems() if k in claims_names}

return data

def get_user_details(self, response):
details = self._map_user_details(response)

# Limits the scope of languages we can use
locale = response.get('locale')
if locale:
details[u'language'] = _to_language(response['locale'])

# Set superuser bit if the provider determines the user is an administrator
details[u'is_superuser'] = details[u'is_staff'] = response.get('administrator', False)

return details

def _map_user_details(self, response):
"""Maps key/values from the response to key/values in the user model.
Does not transfer any key/value that is empty or not present in the reponse.
"""
dest = {}
for source_key, dest_key in self.PROFILE_TO_DETAILS_KEY_MAP.items():
value = response.get(source_key)
if value is not None:
dest[dest_key] = value

return dest


def _to_language(locale):
"""Convert locale name to language code if necessary.
OpenID Connect locale needs to be converted to Django's language
code. In general however, the differences between the locale names
and language code are not very clear among different systems.
For more information, refer to:
http://openid.net/specs/openid-connect-basic-1_0.html#StandardClaims
https://docs.djangoproject.com/en/1.6/topics/i18n/#term-translation-string
"""
return locale.replace('_', '-').lower()
31 changes: 31 additions & 0 deletions auth_backends/pipeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Python-social-auth pipeline functions.
For more info visit http://python-social-auth.readthedocs.org/en/latest/pipeline.html.
"""
from django.contrib.auth import get_user_model


User = get_user_model()


# pylint: disable=unused-argument
# The function parameters must be named exactly as they are below.
# Do not change them to appease Pylint.
def get_user_if_exists(strategy, details, user=None, *args, **kwargs):
"""Return a User with the given username iff the User exists."""
if user:
return {'is_new': False}
try:
username = details.get('username')

# Return the user if it exists
return {
'is_new': False,
'user': User.objects.get(username=username)
}
except User.DoesNotExist:
# Fall to the default return value
pass

# Nothing to return since we don't have a user
return {}
Empty file added auth_backends/tests/__init__.py
Empty file.
21 changes: 21 additions & 0 deletions auth_backends/tests/test_backends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.conf import settings
from social.tests.backends.oauth import OAuth2Test
from social.tests.backends.open_id import OpenIdConnectTestMixin


class EdXOpenIdConnectTests(OpenIdConnectTestMixin, OAuth2Test):
backend_path = 'auth_backends.backends.EdXOpenIdConnect'
issuer = getattr(settings, 'SOCIAL_AUTH_EDX_OIDC_URL_ROOT', None)
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

return data

def test_login(self):
user = self.do_login()
self.assertIsNotNone(user)
27 changes: 27 additions & 0 deletions auth_backends/tests/test_pipeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from django.contrib.auth import get_user_model
from django.test import TestCase
from django_dynamic_fixture import G

from auth_backends.pipeline import get_user_if_exists


class PipelineTests(TestCase):
def setUp(self):
self.user = get_user_model()

def test_get_user_if_exists(self):
username = 'edx'
details = {'username': username}

# If no user exists, return an empty dict
actual = get_user_if_exists(None, details)
self.assertDictEqual(actual, {})

# If user exists, return dict with user and any additional information
user = G(self.user, username=username)
actual = get_user_if_exists(None, details)
self.assertDictEqual(actual, {'is_new': False, 'user': user})

# If user passed to function, just return the additional information
actual = get_user_if_exists(None, details, user=user)
self.assertDictEqual(actual, {'is_new': False})
10 changes: 10 additions & 0 deletions manage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env python
import os
import sys

if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")

from django.core.management import execute_from_command_line

execute_from_command_line(sys.argv)
12 changes: 12 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Django>=1.7 # BSD

# PSA package on PyPI hasn't been updated to include a fix for a breaking change in PyJWT
# python-social-auth==0.2.1 # BSD License
git+https://github.com/omab/python-social-auth.git@bdf69d67d109acfda1016d4a2a63a1cc0a3aba84#egg=python-social-auth

# For running tests
django-dynamic-fixture==1.8.1 # MIT
django-nose==1.3
httpretty==0.6.5
unittest2==0.5.1
coverage==3.7.1
37 changes: 37 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from setuptools import setup


def readme():
with open('README.rst') as f:
return f.read()


setup(
name='edx-auth-backends',
version='0.1',
description='Custom edX authentication backends and pipeline steps',
long_description=readme(),
classifiers=[
'Development Status :: 4 - Beta',
'License :: OSI Approved :: GNU Affero General Public License v3',
'Programming Language :: Python :: 2.7',
'Topic :: Internet',
'Intended Audience :: Developers',
'Environment :: Web Environment',
],
keywords='authentication edx',
url='https://github.com/edx/auth-backends',
author='edX',
author_email='[email protected]',
license='AGPL',
packages=['auth_backends'],
install_requires=[
'Django>=1.7',
# PSA package on PyPI hasn't been updated to include a fix for a breaking change in PyJWT.
# For reference on how dependency_links is used here, see http://goo.gl/D5g4Qq.
'python-social-auth<=0.2.2'
],
dependency_links=[
'git+https://github.com/omab/python-social-auth.git@bdf69d67d109acfda1016d4a2a63a1cc0a3aba84#egg=python-social-auth-0.2.2',
]
)
3 changes: 3 additions & 0 deletions test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh

python manage.py test auth_backends --with-coverage --cover-package=auth_backends
Empty file added tests/__init__.py
Empty file.
Loading

0 comments on commit 7e5b2fd

Please sign in to comment.