Beta release (with docs)
This commit is contained in:
parent
e3f75a0c9f
commit
5340ef0d1a
26 changed files with 1048 additions and 205 deletions
5
CHANGELOG.rst
Normal file
5
CHANGELOG.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
******************
|
||||
1.0.0 (unreleased)
|
||||
******************
|
||||
|
||||
- First official release.
|
45
README.rst
45
README.rst
|
@ -1,6 +1,6 @@
|
|||
==================
|
||||
##################
|
||||
django-allauth-cas
|
||||
==================
|
||||
##################
|
||||
|
||||
.. image:: https://travis-ci.org/aureplop/django-allauth-cas.svg?branch=master
|
||||
:target: https://travis-ci.org/aureplop/django-allauth-cas
|
||||
|
@ -9,14 +9,43 @@ django-allauth-cas
|
|||
:target: https://coveralls.io/github/aureplop/django-allauth-cas?branch=master
|
||||
|
||||
|
||||
**Warning:** Still under development.
|
||||
|
||||
CAS support for django-allauth_.
|
||||
|
||||
Supports:
|
||||
Requirements
|
||||
* Django 1.8 → 2.0
|
||||
|
||||
- Django 1.8-10 - Python 2.7, 3.4-5
|
||||
- Django 1.11 - Python 2.7, 3.4-6
|
||||
Dependencies
|
||||
* django-allauth_
|
||||
* python-cas_: CAS client library
|
||||
|
||||
.. note::
|
||||
|
||||
Tests only target the latest allauth version compatible for each Django version
|
||||
supported:
|
||||
|
||||
* Django 1.9 with django-allauth 0.32.0;
|
||||
* Django 1.8, 1.10, 1.11, 2.0 with the latest django-allauth.
|
||||
|
||||
If you have any problems at use or think docs can be clearer, take a little
|
||||
time to open an issue and/or a PR would be welcomed ;-)
|
||||
|
||||
Acknowledgments
|
||||
* This work is strongly inspired by the `OAuth2 support of django-allauth`_.
|
||||
|
||||
|
||||
.. _django-allauth: https://www.intenct.nl/projects/django-allauth/
|
||||
************
|
||||
Installation
|
||||
************
|
||||
|
||||
Install the python package ``django-allauth-cas``. For example, using pip:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ pip install django-allauth-cas
|
||||
|
||||
Add ``'allauth_cas'`` to ``INSTALLED_APPS``.
|
||||
|
||||
|
||||
.. _django-allauth: https://github.com/pennersr/django-allauth
|
||||
.. _OAuth2 support of django-allauth: https://github.com/pennersr/django-allauth/tree/master/allauth/socialaccount/providers/oauth2
|
||||
.. _python-cas: https://github.com/python-cas/python-cas
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
__version__ = '0.0.1.dev5'
|
||||
__version__ = '1.0.0b1'
|
||||
|
||||
default_app_config = 'allauth_cas.apps.CASAccountConfig'
|
||||
|
||||
|
|
|
@ -3,7 +3,9 @@ from six.moves.urllib.parse import parse_qsl
|
|||
|
||||
import django
|
||||
from django.contrib import messages
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.http import urlencode
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from allauth.socialaccount.providers.base import Provider
|
||||
|
||||
|
@ -15,41 +17,197 @@ else:
|
|||
|
||||
class CASProvider(Provider):
|
||||
|
||||
def get_auth_params(self, request, action):
|
||||
settings = self.get_settings()
|
||||
ret = dict(settings.get('AUTH_PARAMS', {}))
|
||||
dynamic_auth_params = request.GET.get('auth_params')
|
||||
if dynamic_auth_params:
|
||||
ret.update(dict(parse_qsl(dynamic_auth_params)))
|
||||
return ret
|
||||
|
||||
##
|
||||
# Data extraction from CAS responses.
|
||||
##
|
||||
|
||||
def extract_uid(self, data):
|
||||
"""Extract the user uid.
|
||||
|
||||
Notes:
|
||||
Each pair ``(provider_id, uid)`` is unique and related to a single
|
||||
user.
|
||||
|
||||
Args:
|
||||
data (uid (str), extra (dict)): CAS response. Example:
|
||||
``('alice', {'name': 'Alice'})``
|
||||
|
||||
Returns:
|
||||
str: Default to ``data[0]``, user identifier for the CAS server.
|
||||
|
||||
"""
|
||||
uid, _ = data
|
||||
return uid
|
||||
|
||||
def extract_common_fields(self, data):
|
||||
"""Extract the data to pass to `SOCIALACCOUNT_ADAPTER.populate_user()`.
|
||||
|
||||
Args:
|
||||
data (uid (str), extra (dict)): CAS response. Example:
|
||||
``('alice', {'name': 'Alice'})``
|
||||
|
||||
Returns:
|
||||
dict: Default::
|
||||
|
||||
{
|
||||
'username': extra.get('username', uid),
|
||||
'email': extra.get('email'),
|
||||
'first_name': extra.get('first_name'),
|
||||
'last_name': extra.get('last_name'),
|
||||
'name': extra.get('name'),
|
||||
}
|
||||
|
||||
"""
|
||||
uid, extra = data
|
||||
return {
|
||||
'username': extra.get('username', uid),
|
||||
'email': extra.get('email'),
|
||||
'first_name': extra.get('first_name'),
|
||||
'last_name': extra.get('last_name'),
|
||||
'name': extra.get('name'),
|
||||
}
|
||||
|
||||
def extract_email_addresses(self, data):
|
||||
"""Extract the email addresses.
|
||||
|
||||
Args:
|
||||
data (uid (str), extra (dict)): CAS response. Example:
|
||||
``('alice', {'name': 'Alice'})``
|
||||
|
||||
Returns:
|
||||
`list` of `EmailAddress`: By default, ``[]``.
|
||||
|
||||
Example::
|
||||
|
||||
[
|
||||
EmailAddress(
|
||||
email='user@domain.net',
|
||||
verified=True, primary=True,
|
||||
),
|
||||
EmailAddress(
|
||||
email='alias@domain.net',
|
||||
verified=True, primary=False,
|
||||
),
|
||||
]
|
||||
|
||||
"""
|
||||
return super(CASProvider, self).extract_email_addresses(data)
|
||||
|
||||
def extract_extra_data(self, data):
|
||||
"""Extract the data to save to `SocialAccount.extra_data`.
|
||||
|
||||
Args:
|
||||
data (uid (str), extra (dict)): CAS response. Example:
|
||||
``('alice', {'name': 'Alice'})``
|
||||
|
||||
Returns:
|
||||
dict: By default, ``data``.
|
||||
"""
|
||||
uid, extra = data
|
||||
return dict(extra, uid=uid)
|
||||
|
||||
##
|
||||
# Message to suggest users to logout of the CAS server.
|
||||
##
|
||||
|
||||
def add_message_suggest_caslogout(
|
||||
self, request, next_page=None, level=None,
|
||||
):
|
||||
"""Add a message with a link for the user to logout of the CAS server.
|
||||
|
||||
It uses the template ``socialaccount/messages/suggest_caslogout.html``,
|
||||
with the ``provider`` and the ``logout_url`` as context.
|
||||
|
||||
Args:
|
||||
request: The request to which the message is added.
|
||||
next_page (optional): Added to the logout link for the CAS server
|
||||
to redirect the user to this url.
|
||||
Default: ``request.get_full_path()``
|
||||
level: The message level. Default: ``messages.INFO``
|
||||
|
||||
"""
|
||||
if next_page is None:
|
||||
next_page = request.get_full_path()
|
||||
if level is None:
|
||||
level = messages.INFO
|
||||
|
||||
logout_url = self.get_logout_url(request, next=next_page)
|
||||
|
||||
# DefaultAccountAdapter.add_message is unusable because it always
|
||||
# escape the message content.
|
||||
|
||||
template = 'socialaccount/messages/suggest_caslogout.html'
|
||||
context = {
|
||||
'provider': self,
|
||||
'logout_url': logout_url,
|
||||
}
|
||||
|
||||
messages.add_message(
|
||||
request, level,
|
||||
mark_safe(render_to_string(template, context).strip()),
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
def message_suggest_caslogout_on_logout(self, request):
|
||||
"""Indicates whether the logout message should be sent on user logout.
|
||||
|
||||
By default, it returns
|
||||
``settings.SOCIALACCOUNT_PROVIDERS[self.id]['MESSAGE_SUGGEST_CASLOGOUT_ON_LOGOUT']``
|
||||
or ``False``.
|
||||
|
||||
Notes:
|
||||
The ``request`` argument is the one trigerring the emission of the
|
||||
signal ``user_logged_out``.
|
||||
|
||||
"""
|
||||
return (
|
||||
self.get_settings()
|
||||
.get('MESSAGE_SUGGEST_CASLOGOUT_ON_LOGOUT', False)
|
||||
)
|
||||
|
||||
def message_suggest_caslogout_on_logout_level(self, request):
|
||||
"""Level of the logout message issued on user logout.
|
||||
|
||||
By default, it returns
|
||||
``settings.SOCIALACCOUNT_PROVIDERS[self.id]['MESSAGE_SUGGEST_CASLOGOUT_ON_LOGOUT_LEVEL']``
|
||||
or ``messages.INFO``.
|
||||
|
||||
Notes:
|
||||
The ``request`` argument is the one trigerring the emission of the
|
||||
signal ``user_logged_out``.
|
||||
|
||||
"""
|
||||
return (
|
||||
self.get_settings()
|
||||
.get('MESSAGE_SUGGEST_CASLOGOUT_ON_LOGOUT_LEVEL', messages.INFO)
|
||||
)
|
||||
|
||||
##
|
||||
# Shortcuts functions.
|
||||
##
|
||||
|
||||
def get_login_url(self, request, **kwargs):
|
||||
url = reverse(self.id + '_login')
|
||||
if kwargs:
|
||||
url += '?' + urlencode(kwargs)
|
||||
return url
|
||||
|
||||
def get_callback_url(self, request, **kwargs):
|
||||
url = reverse(self.id + '_callback')
|
||||
if kwargs:
|
||||
url += '?' + urlencode(kwargs)
|
||||
return url
|
||||
|
||||
def get_logout_url(self, request, **kwargs):
|
||||
url = reverse(self.id + '_logout')
|
||||
if kwargs:
|
||||
url += '?' + urlencode(kwargs)
|
||||
return url
|
||||
|
||||
def get_auth_params(self, request, action):
|
||||
settings = self.get_settings()
|
||||
ret = dict(settings.get('AUTH_PARAMS', {}))
|
||||
dynamic_auth_params = request.GET.get('auth_params')
|
||||
if dynamic_auth_params:
|
||||
ret.update(dict(parse_qsl(dynamic_auth_params)))
|
||||
return ret
|
||||
|
||||
def message_on_logout(self, request):
|
||||
return self.get_settings().get('MESSAGE_ON_LOGOUT', True)
|
||||
|
||||
def message_on_logout_level(self, request):
|
||||
return self.get_settings().get('MESSAGE_ON_LOGOUT_LEVEL',
|
||||
messages.INFO)
|
||||
|
||||
def extract_uid(self, data):
|
||||
username, _, _ = data
|
||||
return username
|
||||
|
||||
def extract_common_fields(self, data):
|
||||
username, _, _ = data
|
||||
return {'username': username}
|
||||
|
||||
def extract_extra_data(self, data):
|
||||
_, extra_data, _ = data
|
||||
return extra_data
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.signals import user_logged_out
|
||||
from django.dispatch import receiver
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from allauth.account.adapter import get_adapter
|
||||
from allauth.account.utils import get_next_redirect_url
|
||||
from allauth.socialaccount import providers
|
||||
|
||||
|
@ -16,35 +13,20 @@ from . import CAS_PROVIDER_SESSION_KEY
|
|||
def cas_account_logout(sender, request, **kwargs):
|
||||
provider_id = request.session.get(CAS_PROVIDER_SESSION_KEY)
|
||||
|
||||
if (not provider_id or
|
||||
'django.contrib.messages' not in settings.INSTALLED_APPS):
|
||||
if not provider_id:
|
||||
return
|
||||
|
||||
provider = providers.registry.by_id(provider_id, request)
|
||||
|
||||
if not provider.message_on_logout(request):
|
||||
if not provider.message_suggest_caslogout_on_logout(request):
|
||||
return
|
||||
|
||||
redirect_url = (
|
||||
next_page = (
|
||||
get_next_redirect_url(request) or
|
||||
request.get_full_path()
|
||||
get_adapter(request).get_logout_redirect_url(request)
|
||||
)
|
||||
|
||||
logout_kwargs = {'next': redirect_url} if redirect_url else {}
|
||||
logout_url = provider.get_logout_url(request, **logout_kwargs)
|
||||
logout_link = mark_safe('<a href="{}">link</a>'.format(logout_url))
|
||||
|
||||
level = provider.message_on_logout_level(request)
|
||||
|
||||
# DefaultAccountAdapter.add_message from allauth.account.adapter is
|
||||
# unusable because HTML in message content is always escaped.
|
||||
|
||||
template = 'cas_account/messages/logged_out.txt'
|
||||
context = {
|
||||
'logout_url': logout_url,
|
||||
'logout_link': logout_link,
|
||||
}
|
||||
|
||||
message = mark_safe(render_to_string(template, context).strip())
|
||||
|
||||
messages.add_message(request, level, message)
|
||||
provider.add_message_suggest_caslogout(
|
||||
request, next_page=next_page,
|
||||
level=provider.message_suggest_caslogout_on_logout_level(request),
|
||||
)
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
{% load i18n %}
|
||||
{% blocktrans %}
|
||||
To logout of CAS, please close your browser, or visit this {{ logout_link }}.
|
||||
{% endblocktrans %}
|
|
@ -0,0 +1,5 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% blocktrans with provider_name=provider.name %}
|
||||
To logout of {{ provider_name }}, please close your browser, or visit this <a href="{{ logout_url }}">link</a>.
|
||||
{% endblocktrans %}
|
|
@ -1,23 +1,51 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from django.conf.urls import include, url
|
||||
|
||||
from allauth.utils import import_attribute
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
|
||||
def default_urlpatterns(provider):
|
||||
package = provider.get_package()
|
||||
|
||||
login_view = import_attribute(package + '.views.login')
|
||||
callback_view = import_attribute(package + '.views.callback')
|
||||
logout_view = import_attribute(package + '.views.logout')
|
||||
try:
|
||||
login_view = import_string(package + '.views.login')
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"The login view for the '{id}' provider is lacking from the "
|
||||
"'views' module of its app.\n"
|
||||
"You may want to add:\n"
|
||||
"from allauth_cas.views import CASLoginView\n\n"
|
||||
"login = CASLoginView.adapter_view(<LocalCASAdapter>)"
|
||||
.format(id=provider.id)
|
||||
)
|
||||
|
||||
try:
|
||||
callback_view = import_string(package + '.views.callback')
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"The callback view for the '{id}' provider is lacking from the "
|
||||
"'views' module of its app.\n"
|
||||
"You may want to add:\n"
|
||||
"from allauth_cas.views import CASCallbackView\n\n"
|
||||
"callback = CASCallbackView.adapter_view(<LocalCASAdapter>)"
|
||||
.format(id=provider.id)
|
||||
)
|
||||
|
||||
try:
|
||||
logout_view = import_string(package + '.views.logout')
|
||||
except ImportError:
|
||||
logout_view = None
|
||||
|
||||
urlpatterns = [
|
||||
url('^login/$',
|
||||
login_view, name=provider.id + '_login'),
|
||||
url('^login/callback/$',
|
||||
callback_view, name=provider.id + '_callback'),
|
||||
url('^logout/$',
|
||||
logout_view, name=provider.id + '_logout'),
|
||||
url('^login/$', login_view,
|
||||
name=provider.id + '_login'),
|
||||
url('^login/callback/$', callback_view,
|
||||
name=provider.id + '_callback'),
|
||||
]
|
||||
|
||||
if logout_view is not None:
|
||||
urlpatterns += [
|
||||
url('^logout/$', logout_view,
|
||||
name=provider.id + '_logout'),
|
||||
]
|
||||
|
||||
return [url('^' + provider.get_slug() + '/', include(urlpatterns))]
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import django
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.utils.http import urlencode
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
from allauth.account.adapter import get_adapter
|
||||
from allauth.account.utils import get_next_redirect_url
|
||||
|
@ -16,11 +15,6 @@ import cas
|
|||
from . import CAS_PROVIDER_SESSION_KEY
|
||||
from .exceptions import CASAuthenticationError
|
||||
|
||||
if django.VERSION >= (1, 10):
|
||||
from django.urls import reverse
|
||||
else:
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
|
||||
class AuthAction(object):
|
||||
AUTHENTICATE = 'authenticate'
|
||||
|
@ -29,23 +23,38 @@ class AuthAction(object):
|
|||
|
||||
|
||||
class CASAdapter(object):
|
||||
#: CAS server url.
|
||||
url = None
|
||||
#: CAS server version.
|
||||
#: Choices: ``1`` or ``'1'``, ``2`` or ``'2'``, ``3`` or ``'3'``,
|
||||
#: ``'CAS_2_SAML_1_0'``
|
||||
version = None
|
||||
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def renew(self):
|
||||
"""
|
||||
If user is already authenticated on Django, he may already been
|
||||
connected to CAS, but still may want to use another CAS account.
|
||||
We set renew to True in this case, as the CAS server won't use the
|
||||
single sign-on.
|
||||
To specifically check, if the current user has used a CAS server,
|
||||
we check if the CAS session key is set.
|
||||
"""Controls presence of ``renew`` in requests to the CAS server.
|
||||
|
||||
If ``True``, opt out single sign-on (SSO) functionality of the CAS
|
||||
server. So that, user is always prompted for his username and password.
|
||||
|
||||
If ``False``, the CAS server does not prompt users for their
|
||||
credentials if a SSO exists.
|
||||
|
||||
The default allows user to connect via an already used CAS server
|
||||
with other credentials.
|
||||
|
||||
Returns:
|
||||
``True`` if logged in user has already connected to Django using
|
||||
**any** CAS provider in the current session, ``False`` otherwise.
|
||||
|
||||
"""
|
||||
return CAS_PROVIDER_SESSION_KEY in self.request.session
|
||||
|
||||
def get_provider(self):
|
||||
@cached_property
|
||||
def provider(self):
|
||||
"""
|
||||
Returns a provider instance for the current request.
|
||||
"""
|
||||
|
@ -53,75 +62,66 @@ class CASAdapter(object):
|
|||
|
||||
def complete_login(self, request, response):
|
||||
"""
|
||||
Executed by the callback view after successful authentication on CAS
|
||||
server.
|
||||
Executed by the callback view after successful authentication on the
|
||||
CAS server.
|
||||
|
||||
Args:
|
||||
request
|
||||
response (`dict`): Data returned by the CAS server.
|
||||
``response[username]`` contains the user identifier for the
|
||||
server, and may contain extra user-attributes.
|
||||
|
||||
Returns:
|
||||
`SocialLogin()` object: State of the login-session.
|
||||
|
||||
Returns the SocialLogin object which represents the state of the
|
||||
current login-session.
|
||||
"""
|
||||
login = (self.get_provider()
|
||||
.sociallogin_from_response(request, response))
|
||||
login = self.provider.sociallogin_from_response(request, response)
|
||||
return login
|
||||
|
||||
def get_service_url(self, request):
|
||||
"""
|
||||
Returns the service url to for a CAS client.
|
||||
"""The service url, used by the CAS client.
|
||||
|
||||
From CAS specification, the service url is used in order to redirect
|
||||
user after a successful login on CAS server. Also, service_url sent
|
||||
when ticket is verified must be the one for which ticket was issued.
|
||||
According to the CAS spec, the service url is passed by the CAS client
|
||||
at several times. It must be the same for all interactions with the CAS
|
||||
server.
|
||||
|
||||
To conform this, the service url is always the callback url.
|
||||
It is used as redirection from the CAS server after a succssful
|
||||
authentication. So, the callback url is used as service url.
|
||||
|
||||
A redirect url is found from the current request and appended as
|
||||
parameter to the service url and is latter used by the callback view to
|
||||
redirect user.
|
||||
If present, the GET param ``next`` is added to the service url.
|
||||
"""
|
||||
redirect_to = get_next_redirect_url(request)
|
||||
|
||||
callback_kwargs = {'next': redirect_to} if redirect_to else {}
|
||||
callback_url = self.get_callback_url(request, **callback_kwargs)
|
||||
callback_url = (
|
||||
self.provider.get_callback_url(request, **callback_kwargs))
|
||||
|
||||
service_url = request.build_absolute_uri(callback_url)
|
||||
|
||||
return service_url
|
||||
|
||||
def get_callback_url(self, request, **kwargs):
|
||||
"""
|
||||
Returns the callback url of the provider.
|
||||
|
||||
Keyword arguments are set as query string.
|
||||
"""
|
||||
url = reverse(self.provider_id + '_callback')
|
||||
if kwargs:
|
||||
url += '?' + urlencode(kwargs)
|
||||
return url
|
||||
|
||||
|
||||
class CASView(object):
|
||||
|
||||
@classmethod
|
||||
def adapter_view(cls, adapter, **kwargs):
|
||||
"""
|
||||
Similar to the Django as_view() method.
|
||||
Base class for CAS views.
|
||||
"""
|
||||
@classmethod
|
||||
def adapter_view(cls, adapter):
|
||||
"""Transform the view class into a view function.
|
||||
|
||||
It also setups a few things:
|
||||
- given adapter argument will be used in views internals.
|
||||
- if the view execution raises a CASAuthenticationError, the view
|
||||
renders an authentication error page.
|
||||
Similar to the Django ``as_view()`` method.
|
||||
|
||||
To use this:
|
||||
Notes:
|
||||
An (human) error page is rendered if any ``CASAuthenticationError``
|
||||
is catched.
|
||||
|
||||
- subclass CAS adapter as wanted:
|
||||
Args:
|
||||
adapter (:class:`CASAdapter`): Provide specifics of a CAS server.
|
||||
|
||||
class MyAdapter(CASAdapter):
|
||||
url = 'https://my.cas.url'
|
||||
Returns:
|
||||
A view function. The given adapter and related provider are
|
||||
accessible as attributes from the view class.
|
||||
|
||||
- define views:
|
||||
|
||||
login = views.CASLoginView.adapter_view(MyAdapter)
|
||||
callback = views.CASCallbackView.adapter_view(MyAdapter)
|
||||
logout = views.CASLogoutView.adapter_view(MyAdapter)
|
||||
|
||||
"""
|
||||
def view(request, *args, **kwargs):
|
||||
|
@ -134,7 +134,7 @@ class CASView(object):
|
|||
|
||||
# Setup and store adapter as view attribute.
|
||||
self.adapter = adapter(request)
|
||||
self.provider = self.adapter.get_provider()
|
||||
self.provider = self.adapter.provider
|
||||
|
||||
try:
|
||||
return self.dispatch(request, *args, **kwargs)
|
||||
|
@ -207,17 +207,20 @@ class CASCallbackView(CASView):
|
|||
# - error: None, {}, None
|
||||
response = client.verify_ticket(ticket)
|
||||
|
||||
if not response[0]:
|
||||
uid, extra, _ = response
|
||||
|
||||
if not uid:
|
||||
raise CASAuthenticationError(
|
||||
"CAS server doesn't validate the ticket."
|
||||
)
|
||||
|
||||
# The CAS provider in use is stored to propose to the user to
|
||||
# disconnect from the latter when he logouts.
|
||||
# Keep tracks of the last used CAS provider.
|
||||
request.session[CAS_PROVIDER_SESSION_KEY] = self.provider.id
|
||||
|
||||
# Finish the login flow
|
||||
login = self.adapter.complete_login(request, response)
|
||||
data = (uid, extra)
|
||||
|
||||
# Finish the login flow.
|
||||
login = self.adapter.complete_login(request, data)
|
||||
login.state = SocialLogin.unstash_state(request)
|
||||
return complete_social_login(request, login)
|
||||
|
||||
|
@ -242,7 +245,7 @@ class CASLogoutView(CASView):
|
|||
|
||||
def get_redirect_url(self):
|
||||
"""
|
||||
Returns the url to redirect after logout from current request.
|
||||
Returns the url to redirect after logout.
|
||||
"""
|
||||
request = self.request
|
||||
return (
|
||||
|
|
1
docs/.gitignore
vendored
Normal file
1
docs/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
_build/
|
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SPHINXPROJ = django-allauth-cas
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
14
docs/README
Normal file
14
docs/README
Normal file
|
@ -0,0 +1,14 @@
|
|||
#############
|
||||
Documentation
|
||||
#############
|
||||
|
||||
|
||||
The documentation is compiled from reStructuredText using `Sphinx`_.
|
||||
|
||||
To compile your own html version in ``_build/html/``::
|
||||
|
||||
# First time only.
|
||||
pip install sphinx
|
||||
|
||||
# Build html.
|
||||
make html
|
40
docs/advanced/cas_client.txt
Normal file
40
docs/advanced/cas_client.txt
Normal file
|
@ -0,0 +1,40 @@
|
|||
########################
|
||||
Configure the CAS client
|
||||
########################
|
||||
|
||||
.. seealso::
|
||||
|
||||
`CAS Protocol Specification`_
|
||||
|
||||
The CAS client parameters can be set on the ``CASAdapter`` subclasses.
|
||||
|
||||
|
||||
******************
|
||||
Server information
|
||||
******************
|
||||
|
||||
You must at least fill these attributes on an adapter class.
|
||||
|
||||
.. autoattribute:: allauth_cas.views.CASAdapter.url
|
||||
|
||||
.. autoattribute:: allauth_cas.views.CASAdapter.version
|
||||
|
||||
|
||||
*****************
|
||||
Client parameters
|
||||
*****************
|
||||
|
||||
.. autoattribute:: allauth_cas.views.CASAdapter.renew
|
||||
|
||||
.. note::
|
||||
|
||||
A SSO session is created when user successfully authenticates against the
|
||||
server, which let an HTTP cookie in the browser current session. If SSO is
|
||||
enabled (``renew = False``), server checks this cookie, if any, to bypass the
|
||||
request of user credentials. Depending on the server configuration and user
|
||||
input at login time, CAS server replies to login page requests with a warning
|
||||
page, or transparently redirects to the callback url (path to come back to
|
||||
your web service).
|
||||
|
||||
|
||||
.. _`CAS Protocol Specification`: https://apereo.github.io/cas/5.0.x/protocol/CAS-Protocol-Specification.html
|
21
docs/advanced/extract_data.txt
Normal file
21
docs/advanced/extract_data.txt
Normal file
|
@ -0,0 +1,21 @@
|
|||
#################################
|
||||
Use data returned by a CAS server
|
||||
#################################
|
||||
|
||||
.. seealso::
|
||||
|
||||
`Creating and Populating User instances`_
|
||||
|
||||
The following methods of ``CASProvider`` are used to extract data from the CAS
|
||||
responses.
|
||||
|
||||
.. automethod:: allauth_cas.providers.CASProvider.extract_uid
|
||||
|
||||
.. automethod:: allauth_cas.providers.CASProvider.extract_common_fields
|
||||
|
||||
.. automethod:: allauth_cas.providers.CASProvider.extract_email_addresses
|
||||
|
||||
.. automethod:: allauth_cas.providers.CASProvider.extract_extra_data
|
||||
|
||||
|
||||
.. _`Creating and Populating User instances`: http://django-allauth.readthedocs.io/en/latest/advanced.html#creating-and-populating-user-instances
|
8
docs/advanced/index.txt
Normal file
8
docs/advanced/index.txt
Normal file
|
@ -0,0 +1,8 @@
|
|||
##############
|
||||
Advanced Usage
|
||||
##############
|
||||
|
||||
.. toctree::
|
||||
cas_client
|
||||
extract_data
|
||||
signout
|
87
docs/advanced/signout.txt
Normal file
87
docs/advanced/signout.txt
Normal file
|
@ -0,0 +1,87 @@
|
|||
################
|
||||
Sign out helpers
|
||||
################
|
||||
|
||||
To use features described on this page, you must also add a logout view for
|
||||
your provider:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from allauth_cas.views import CASLogoutView
|
||||
|
||||
logout = CASLogoutView.adapter_view(MyCASAdapter)
|
||||
|
||||
|
||||
**************
|
||||
Suggest logout
|
||||
**************
|
||||
|
||||
Sending message
|
||||
===============
|
||||
|
||||
Using the method below, you can emit a message to suggest users to logout of
|
||||
the CAS server.
|
||||
|
||||
.. automethod:: allauth_cas.providers.CASProvider.add_message_suggest_caslogout
|
||||
|
||||
Sending message at user logout
|
||||
==============================
|
||||
|
||||
When the user signs out your application, this message can be sent
|
||||
automatically using the following settings.
|
||||
|
||||
The message contains a logout link for **the last used** CAS server during the
|
||||
session.
|
||||
|
||||
In your settings:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
SOCIALACCOUNT_PROVIDERS = {
|
||||
# …
|
||||
'<provider id>': {
|
||||
# …
|
||||
|
||||
'MESSAGE_SUGGEST_CASLOGOUT_ON_LOGOUT': True,
|
||||
|
||||
# Optional. By default, messages.INFO
|
||||
'MESSAGE_SUGGEST_CASLOGOUT_ON_LOGOUT_LEVEL': messages.WARNING,
|
||||
},
|
||||
}
|
||||
|
||||
If you need more control over the sending of the message, you can use the
|
||||
methods below of the provider class.
|
||||
|
||||
.. automethod:: allauth_cas.providers.CASProvider.message_suggest_caslogout_on_logout
|
||||
|
||||
.. automethod:: allauth_cas.providers.CASProvider.message_suggest_caslogout_on_logout_level
|
||||
|
||||
|
||||
****************************
|
||||
Redirection after CAS logout
|
||||
****************************
|
||||
|
||||
An url is always given for the CAS server to redirect the user to your
|
||||
application.
|
||||
|
||||
The target of this redirection is:
|
||||
|
||||
* If the link is created on user logout (using above configuration):
|
||||
|
||||
* if present, the url pointed by the GET parameter ``next``, which should
|
||||
be the url the user has just landed after being logged out;
|
||||
* otherwise, the value returned by
|
||||
``ACCOUNT_ADAPTER.get_logout_redirect_url()``.
|
||||
|
||||
* If the link is created using
|
||||
:meth:`~allauth_cas.providers.CASProvider.add_message_suggest_caslogout`:
|
||||
|
||||
* if present, the value of the parameter ``next_page``;
|
||||
* otherwise, the url of the current page.
|
||||
|
||||
* Otherwise, ``ACCOUNT_ADAPTER.get_logout_redirect_url()``.
|
||||
|
||||
.. note::
|
||||
|
||||
If no redirection happens, you should check the version declared by the
|
||||
``CASAdapter`` class corresponds to the CAS server one.
|
153
docs/basic_setup.txt
Normal file
153
docs/basic_setup.txt
Normal file
|
@ -0,0 +1,153 @@
|
|||
###########
|
||||
Basic setup
|
||||
###########
|
||||
|
||||
Following the instructions on this page, your will create a provider for
|
||||
allauth, which allows users to connect through a CAS server.
|
||||
|
||||
|
||||
****************
|
||||
1. Create an app
|
||||
****************
|
||||
|
||||
``allauth`` determines available providers by scanning ``INSTALLED_APPS``.
|
||||
Let's begin by creating an app for the CAS provider:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ python manage.py startapp mycas
|
||||
|
||||
And add it to the ``INSTALLED_APPS``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
INSTALLED_APPS = [
|
||||
# …
|
||||
'allauth',
|
||||
'allauth.account',
|
||||
'allauth.socialaccount',
|
||||
|
||||
'allauth_cas',
|
||||
|
||||
'mycas',
|
||||
]
|
||||
|
||||
|
||||
**********************
|
||||
2. Create the provider
|
||||
**********************
|
||||
|
||||
In ``mycas/provider.py``, create subclasses of ``ProviderAccount`` and
|
||||
``CASProvider``.
|
||||
|
||||
The ``CASProvider`` subclass defines how to process data returned by the CAS
|
||||
server.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from allauth.socialaccount.providers.base import ProviderAccount
|
||||
from allauth_cas.providers import CASProvider
|
||||
|
||||
|
||||
class MyCASAccount(ProviderAccount):
|
||||
pass
|
||||
|
||||
|
||||
class MyCASProvider(CASProvider):
|
||||
id = 'mycas' # Choose an identifier for your provider
|
||||
name = 'My CAS' # Verbose name of your provider
|
||||
account_class = MyCASAccount
|
||||
|
||||
|
||||
provider_classes = [ClipperProvider]
|
||||
|
||||
.. seealso::
|
||||
|
||||
:doc:`advanced/extract_data`
|
||||
|
||||
|
||||
*******************
|
||||
3. Create the views
|
||||
*******************
|
||||
|
||||
Subclass ``CASAdapter`` to give your configuration as a CAS client.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from allauth_cas.views import CASAdapter
|
||||
|
||||
from .providers import MyCASProvider
|
||||
|
||||
|
||||
class MyCASAdapter(CASAdapter):
|
||||
provider_id = MyCASProvider.id
|
||||
url = 'https://mycas.mydomain.net' # The CAS server url
|
||||
version = 3 # Select the CAS protocol version used by the CAS server: 1, 2, 3…
|
||||
|
||||
Then, you can simply create the login and callback views.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from allauth_cas.views import CASCallbackView, CASLoginView
|
||||
|
||||
login = CASLoginView.adapter_view(MyCASAdapter)
|
||||
callback = CASLogoutView.adapter_view(MyCASAdapter)
|
||||
|
||||
.. seealso::
|
||||
|
||||
:doc:`advanced/cas_client`
|
||||
|
||||
|
||||
******************
|
||||
4. Create the urls
|
||||
******************
|
||||
|
||||
Finally, add the urls in ``mycas/urls.py``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from allauth_cas.urls import default_urlpatterns
|
||||
|
||||
from .provider import MyCASProvider
|
||||
|
||||
urlpatterns = default_urlpatterns(MyCasProvider)
|
||||
|
||||
There is no need to do more, as ``allauth`` is responsible for including these
|
||||
urls.
|
||||
|
||||
|
||||
*******************************************
|
||||
5. Allow your application at the CAS server
|
||||
*******************************************
|
||||
|
||||
.. note::
|
||||
|
||||
This step is only required if the CAS server restricts access to known
|
||||
applications.
|
||||
|
||||
CAS servers may restrict their usage to a list of known clients. To do so,
|
||||
the service url must be known by the CAS server. For our case, the service
|
||||
url is the callback url of a CAS provider.
|
||||
|
||||
The service url is formatted as:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
<url of your application>/<path to allauth urls>/<provider id>/login/callback/
|
||||
|
||||
Assuming a site is served at ``https://mydomain.net``, that the allauth urls
|
||||
are included under ``accounts/``, and the provider id is ``mycas``, the service url
|
||||
is:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
https://mydomain.net/accounts/mycas/login/callback
|
||||
|
||||
While in local development, it can be:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
http://127.0.0.1:8000/accounts/mycas/login/callback
|
||||
|
||||
This url should be added to the authorized services within the CAS server
|
||||
configuration (by yourself or someone in charge of the server).
|
5
docs/changelog.txt
Normal file
5
docs/changelog.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
#########
|
||||
Changelog
|
||||
#########
|
||||
|
||||
.. include:: ../CHANGELOG.rst
|
181
docs/conf.py
Normal file
181
docs/conf.py
Normal file
|
@ -0,0 +1,181 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# django-allauth-cas documentation build configuration file.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import django
|
||||
|
||||
from allauth_cas import __version__
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
|
||||
sys.path.insert(0, os.path.abspath('../'))
|
||||
|
||||
# Setup django to avoid issues with autodoc.
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.settings')
|
||||
|
||||
django.setup()
|
||||
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.napoleon',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.viewcode',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.txt'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'django-allauth-cas'
|
||||
copyright = '2017, Aurélien Delobelle'
|
||||
author = 'Aurélien Delobelle'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = __version__
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = __version__
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This patterns also effect to html_static_path and html_extra_path
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = True
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Custom sidebar templates, must be a dictionary that maps document names
|
||||
# to template names.
|
||||
#
|
||||
# This is required for the alabaster theme
|
||||
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
|
||||
html_sidebars = {
|
||||
'**': [
|
||||
'relations.html', # needs 'show_related': True theme option to display
|
||||
'searchbox.html',
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'django-allauth-casdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'django-allauth-cas.tex', 'django-allauth-cas Documentation',
|
||||
'Aurélien Delobelle', 'manual'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'django-allauth-cas', 'django-allauth-cas Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'django-allauth-cas', 'django-allauth-cas Documentation',
|
||||
author, 'django-allauth-cas', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
16
docs/index.txt
Normal file
16
docs/index.txt
Normal file
|
@ -0,0 +1,16 @@
|
|||
.. django-allauth-cas documentation master file.
|
||||
|
||||
.. include:: ../README.rst
|
||||
|
||||
|
||||
********
|
||||
Contents
|
||||
********
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
basic_setup
|
||||
advanced/index
|
||||
|
||||
changelog
|
36
docs/make.bat
Normal file
36
docs/make.bat
Normal file
|
@ -0,0 +1,36 @@
|
|||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
set SPHINXPROJ=django-allauth-cas
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
|
||||
:end
|
||||
popd
|
4
setup.py
4
setup.py
|
@ -20,13 +20,14 @@ setup(
|
|||
long_description=README,
|
||||
url='https://github.com/aureplop/django-allauth-cas',
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: Web Environment',
|
||||
'Framework :: Django',
|
||||
'Framework :: Django :: 1.8',
|
||||
'Framework :: Django :: 1.9',
|
||||
'Framework :: Django :: 1.10',
|
||||
'Framework :: Django :: 1.11',
|
||||
'Framework :: Django :: 2.0',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: OS Independent',
|
||||
|
@ -48,6 +49,7 @@ setup(
|
|||
'six',
|
||||
],
|
||||
extras_require={
|
||||
'docs': ['sphinx'],
|
||||
'tests': ['tox'],
|
||||
},
|
||||
)
|
||||
|
|
|
@ -12,8 +12,8 @@ User = get_user_model()
|
|||
|
||||
class LogoutFlowTests(CASTestCase):
|
||||
expected_msg_str = (
|
||||
"To logout of CAS, please close your browser, or visit this "
|
||||
"<a href=\"/accounts/theid/logout/?next=%2Faccounts%2Flogout%2F\">"
|
||||
"To logout of The Provider, please close your browser, or visit this "
|
||||
"<a href=\"/accounts/theid/logout/?next=%2Fredir%2F\">"
|
||||
"link</a>."
|
||||
)
|
||||
|
||||
|
@ -28,43 +28,36 @@ class LogoutFlowTests(CASTestCase):
|
|||
)
|
||||
self.assertTemplateNotUsed(
|
||||
response,
|
||||
'cas_account/messages/logged_out.txt',
|
||||
'socialaccount/messages/suggest_caslogout.html',
|
||||
)
|
||||
|
||||
@override_settings(SOCIALACCOUNT_PROVIDERS={
|
||||
'theid': {
|
||||
'MESSAGE_ON_LOGOUT': True,
|
||||
'MESSAGE_ON_LOGOUT_LEVEL': messages.WARNING,
|
||||
'MESSAGE_SUGGEST_CASLOGOUT_ON_LOGOUT': True,
|
||||
'MESSAGE_SUGGEST_CASLOGOUT_ON_LOGOUT_LEVEL': messages.WARNING,
|
||||
},
|
||||
})
|
||||
def test_message_on_logout(self):
|
||||
"""
|
||||
Message is sent to propose user to logout of CAS.
|
||||
"""
|
||||
r = self.client.post('/accounts/logout/')
|
||||
r = self.client.post('/accounts/logout/?next=/redir/')
|
||||
r_messages = get_messages(r.wsgi_request)
|
||||
|
||||
expected_msg = Message(messages.WARNING, self.expected_msg_str)
|
||||
|
||||
self.assertIn(expected_msg, r_messages)
|
||||
self.assertTemplateUsed(r, 'cas_account/messages/logged_out.txt')
|
||||
self.assertTemplateUsed(
|
||||
r, 'socialaccount/messages/suggest_caslogout.html')
|
||||
|
||||
@override_settings(SOCIALACCOUNT_PROVIDERS={
|
||||
'theid': {
|
||||
'MESSAGE_ON_LOGOUT': False,
|
||||
},
|
||||
})
|
||||
def test_message_on_logout_disabled(self):
|
||||
"""
|
||||
The logout message can be disabled in settings.
|
||||
"""
|
||||
r = self.client.post('/accounts/logout/')
|
||||
self.assertCASLogoutNotInMessages(r)
|
||||
|
||||
@override_settings(SOCIALACCOUNT_PROVIDERS={
|
||||
'theid': {'MESSAGE_ON_LOGOUT': True},
|
||||
'theid': {'MESSAGE_SUGGEST_CASLOGOUT_ON_LOGOUT': True},
|
||||
})
|
||||
def test_default_logout(self):
|
||||
def test_other_logout(self):
|
||||
"""
|
||||
The CAS logout message doesn't appear with other login methods.
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from six.moves.urllib.parse import urlencode
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.messages.api import get_messages
|
||||
from django.contrib.messages.middleware import MessageMiddleware
|
||||
from django.contrib.messages.storage.base import Message
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.test import RequestFactory, TestCase, override_settings
|
||||
|
||||
from allauth.socialaccount.providers import registry
|
||||
|
@ -12,12 +18,14 @@ from .example.provider import ExampleCASProvider
|
|||
class CASProviderTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
factory = RequestFactory()
|
||||
request = factory.get('/test/')
|
||||
request.session = {}
|
||||
self.request = request
|
||||
self.request = self._get_request()
|
||||
self.provider = ExampleCASProvider(self.request)
|
||||
|
||||
self.provider = ExampleCASProvider(request)
|
||||
def _get_request(self):
|
||||
request = RequestFactory().get('/test/')
|
||||
SessionMiddleware().process_request(request)
|
||||
MessageMiddleware().process_request(request)
|
||||
return request
|
||||
|
||||
def test_register(self):
|
||||
"""
|
||||
|
@ -26,10 +34,6 @@ class CASProviderTests(TestCase):
|
|||
self.assertIsInstance(registry.by_id('theid'), ExampleCASProvider)
|
||||
|
||||
def test_get_login_url(self):
|
||||
"""
|
||||
get_login_url returns the url to logout of the provider.
|
||||
Keyword arguments are set as query string.
|
||||
"""
|
||||
url = self.provider.get_login_url(self.request)
|
||||
self.assertEqual('/accounts/theid/login/', url)
|
||||
|
||||
|
@ -43,11 +47,21 @@ class CASProviderTests(TestCase):
|
|||
'Dwhoam%25C3%25AF'
|
||||
)
|
||||
|
||||
def test_get_callback_url(self):
|
||||
url = self.provider.get_callback_url(self.request)
|
||||
self.assertEqual('/accounts/theid/login/callback/', url)
|
||||
|
||||
url_with_qs = self.provider.get_callback_url(
|
||||
self.request,
|
||||
next='/path?quéry=string&two=whoam%C3%AF',
|
||||
)
|
||||
self.assertEqual(
|
||||
url_with_qs,
|
||||
'/accounts/theid/login/callback/?next=%2Fpath%3Fqu%C3%A9ry%3Dstrin'
|
||||
'g%26two%3Dwhoam%25C3%25AF'
|
||||
)
|
||||
|
||||
def test_get_logout_url(self):
|
||||
"""
|
||||
get_logout_url returns the url to logout of the provider.
|
||||
Keyword arguments are set as query string.
|
||||
"""
|
||||
url = self.provider.get_logout_url(self.request)
|
||||
self.assertEqual('/accounts/theid/logout/', url)
|
||||
|
||||
|
@ -97,35 +111,90 @@ class CASProviderTests(TestCase):
|
|||
'next': 'two=whoam%C3%AF&qu%C3%A9ry=string',
|
||||
})
|
||||
|
||||
def test_add_message_suggest_caslogout(self):
|
||||
expected_msg_base_str = (
|
||||
"To logout of The Provider, please close your browser, or visit "
|
||||
"this <a href=\"/accounts/theid/logout/?{}\">link</a>."
|
||||
)
|
||||
|
||||
# Defaults.
|
||||
req1 = self.request
|
||||
|
||||
self.provider.add_message_suggest_caslogout(req1)
|
||||
|
||||
expected_msg1 = Message(
|
||||
messages.INFO,
|
||||
expected_msg_base_str.format(urlencode({'next': '/test/'})),
|
||||
)
|
||||
self.assertIn(expected_msg1, get_messages(req1))
|
||||
|
||||
# Custom arguments.
|
||||
req2 = self._get_request()
|
||||
|
||||
self.provider.add_message_suggest_caslogout(
|
||||
req2, next_page='/redir/', level=messages.WARNING)
|
||||
|
||||
expected_msg2 = Message(
|
||||
messages.WARNING,
|
||||
expected_msg_base_str.format(urlencode({'next': '/redir/'})),
|
||||
)
|
||||
self.assertIn(expected_msg2, get_messages(req2))
|
||||
|
||||
def test_message_suggest_caslogout_on_logout(self):
|
||||
self.assertFalse(
|
||||
self.provider.message_suggest_caslogout_on_logout(self.request))
|
||||
|
||||
with override_settings(SOCIALACCOUNT_PROVIDERS={
|
||||
'theid': {'MESSAGE_SUGGEST_CASLOGOUT_ON_LOGOUT': True},
|
||||
}):
|
||||
self.assertTrue(
|
||||
self.provider
|
||||
.message_suggest_caslogout_on_logout(self.request)
|
||||
)
|
||||
|
||||
@override_settings(SOCIALACCOUNT_PROVIDERS={
|
||||
'theid': {
|
||||
'MESSAGE_ON_LOGOUT_LEVEL': messages.WARNING,
|
||||
'MESSAGE_SUGGEST_CASLOGOUT_ON_LOGOUT_LEVEL': messages.WARNING,
|
||||
},
|
||||
})
|
||||
def test_message_on_logout(self):
|
||||
message_on_logout = self.provider.message_on_logout(self.request)
|
||||
self.assertTrue(message_on_logout)
|
||||
|
||||
message_level = self.provider.message_on_logout_level(self.request)
|
||||
self.assertEqual(messages.WARNING, message_level)
|
||||
def test_message_suggest_caslogout_on_logout_level(self):
|
||||
self.assertEqual(messages.WARNING, (
|
||||
self.provider
|
||||
.message_suggest_caslogout_on_logout_level(self.request)
|
||||
))
|
||||
|
||||
def test_extract_uid(self):
|
||||
response = 'useRName', {}, None
|
||||
response = 'useRName', {}
|
||||
uid = self.provider.extract_uid(response)
|
||||
self.assertEqual('useRName', uid)
|
||||
|
||||
def test_extract_common_fields(self):
|
||||
response = 'useRName', {}, None
|
||||
response = 'useRName', {}
|
||||
common_fields = self.provider.extract_common_fields(response)
|
||||
self.assertDictEqual(common_fields, {
|
||||
'username': 'useRName',
|
||||
'first_name': None,
|
||||
'last_name': None,
|
||||
'name': None,
|
||||
'email': None,
|
||||
})
|
||||
|
||||
def test_extract_common_fields_with_extra(self):
|
||||
response = 'useRName', {'username': 'user', 'email': 'user@mail.net'}
|
||||
common_fields = self.provider.extract_common_fields(response)
|
||||
self.assertDictEqual(common_fields, {
|
||||
'username': 'user',
|
||||
'first_name': None,
|
||||
'last_name': None,
|
||||
'name': None,
|
||||
'email': 'user@mail.net',
|
||||
})
|
||||
|
||||
def test_extract_extra_data(self):
|
||||
attributes = {'user_attr': 'thevalue', 'another': 'value'}
|
||||
response = 'useRName', attributes, None
|
||||
response = 'useRName', {'user_attr': 'thevalue', 'another': 'value'}
|
||||
extra_data = self.provider.extract_extra_data(response)
|
||||
self.assertDictEqual(extra_data, {
|
||||
'user_attr': 'thevalue',
|
||||
'another': 'value',
|
||||
'uid': 'useRName',
|
||||
})
|
||||
|
|
|
@ -48,20 +48,6 @@ class CASAdapterTests(CASTestCase):
|
|||
service_url = adapter.get_service_url(request)
|
||||
self.assertEqual(expected, service_url)
|
||||
|
||||
def test_get_callback_url(self):
|
||||
expected = '/accounts/theid/login/callback/'
|
||||
callback_url = self.adapter.get_callback_url(self.request)
|
||||
self.assertEqual(expected, callback_url)
|
||||
|
||||
def test_get_callback_url_with_kwargs(self):
|
||||
expected = (
|
||||
'/accounts/theid/login/callback/?next=%2Fpath%2F'
|
||||
)
|
||||
callback_url = self.adapter.get_callback_url(self.request, **{
|
||||
'next': '/path/',
|
||||
})
|
||||
self.assertEqual(expected, callback_url)
|
||||
|
||||
def test_renew(self):
|
||||
"""
|
||||
From an anonymous request, renew is False to let using the single
|
||||
|
|
9
tox.ini
9
tox.ini
|
@ -1,24 +1,29 @@
|
|||
[tox]
|
||||
envlist =
|
||||
django{18,19,110}-py{27,34,35},
|
||||
django18-py{27,34,35},
|
||||
django19-py{27,34,35}-allauth32,
|
||||
django110-py{27,34,35},
|
||||
django111-py{27,34,35,36},
|
||||
django20-py{34,35,36},
|
||||
|
||||
cov_combine,
|
||||
flake8,
|
||||
isort
|
||||
|
||||
[testenv]
|
||||
deps =
|
||||
allauth32: django-allauth>=0.32.0,<0.33.0
|
||||
|
||||
django18: django>=1.8,<1.9
|
||||
django19: django>=1.9,<1.10
|
||||
django110: django>=1.10,<1.11
|
||||
django111: django>=1.11,<2.0
|
||||
django20: django>=2.0,<2.1
|
||||
|
||||
coverage
|
||||
mock ; python_version < "3.0"
|
||||
usedevelop = True
|
||||
commands =
|
||||
python -V
|
||||
coverage run \
|
||||
--branch \
|
||||
--source=allauth_cas --omit=*migrations* \
|
||||
|
|
Loading…
Reference in a new issue