This repository has been archived by the owner on May 29, 2024. It is now read-only.
forked from recalapp/recal
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
977 additions
and
6 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
"""Django CAS 1.0/2.0 authentication backend""" | ||
|
||
from django.conf import settings | ||
|
||
__all__ = [] | ||
|
||
_DEFAULTS = { | ||
'CAS_ADMIN_PREFIX': None, | ||
'CAS_EXTRA_LOGIN_PARAMS': None, | ||
'CAS_IGNORE_REFERER': False, | ||
'CAS_LOGOUT_COMPLETELY': True, | ||
'CAS_REDIRECT_URL': '/', | ||
'CAS_RETRY_LOGIN': False, | ||
'CAS_SERVER_URL': None, | ||
'CAS_VERSION': '2', | ||
'CAS_GATEWAY': False, | ||
'CAS_PROXY_CALLBACK': None, | ||
'CAS_RESPONSE_CALLBACKS': None, | ||
'CAS_CUSTOM_FORBIDDEN':None | ||
} | ||
|
||
for key, value in _DEFAULTS.iteritems(): | ||
try: | ||
getattr(settings, key) | ||
except AttributeError: | ||
setattr(settings, key, value) | ||
# Suppress errors from DJANGO_SETTINGS_MODULE not being set | ||
except ImportError: | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
"""CAS authentication backend""" | ||
|
||
from urllib import urlencode, urlopen | ||
from urlparse import urljoin | ||
|
||
from django.conf import settings | ||
from django.core.exceptions import ObjectDoesNotExist | ||
from cas.models import User, Tgt, PgtIOU | ||
from cas.utils import cas_response_callbacks | ||
|
||
__all__ = ['CASBackend'] | ||
|
||
def _verify_cas1(ticket, service): | ||
"""Verifies CAS 1.0 authentication ticket. | ||
Returns username on success and None on failure. | ||
""" | ||
|
||
params = {'ticket': ticket, 'service': service} | ||
url = (urljoin(settings.CAS_SERVER_URL, 'validate') + '?' + | ||
urlencode(params)) | ||
page = urlopen(url) | ||
try: | ||
verified = page.readline().strip() | ||
if verified == 'yes': | ||
return page.readline().strip() | ||
else: | ||
return None | ||
finally: | ||
page.close() | ||
|
||
|
||
def _verify_cas2(ticket, service): | ||
"""Verifies CAS 2.0+ XML-based authentication ticket. | ||
Returns username on success and None on failure. | ||
""" | ||
|
||
try: | ||
from xml.etree import ElementTree | ||
except ImportError: | ||
from elementtree import ElementTree | ||
|
||
if settings.CAS_PROXY_CALLBACK: | ||
params = {'ticket': ticket, 'service': service, 'pgtUrl': settings.CAS_PROXY_CALLBACK} | ||
else: | ||
params = {'ticket': ticket, 'service': service} | ||
|
||
url = (urljoin(settings.CAS_SERVER_URL, 'proxyValidate') + '?' + | ||
urlencode(params)) | ||
|
||
page = urlopen(url) | ||
try: | ||
response = page.read() | ||
tree = ElementTree.fromstring(response) | ||
|
||
#Useful for debugging | ||
#from xml.dom.minidom import parseString | ||
#from xml.etree import ElementTree | ||
#txt = ElementTree.tostring(tree) | ||
#print parseString(txt).toprettyxml() | ||
|
||
if tree[0].tag.endswith('authenticationSuccess'): | ||
if settings.CAS_RESPONSE_CALLBACKS: | ||
cas_response_callbacks(tree) | ||
return tree[0][0].text | ||
else: | ||
return None | ||
finally: | ||
page.close() | ||
|
||
|
||
def verify_proxy_ticket(ticket, service): | ||
"""Verifies CAS 2.0+ XML-based proxy ticket. | ||
Returns username on success and None on failure. | ||
""" | ||
|
||
try: | ||
from xml.etree import ElementTree | ||
except ImportError: | ||
from elementtree import ElementTree | ||
|
||
params = {'ticket': ticket, 'service': service} | ||
|
||
url = (urljoin(settings.CAS_SERVER_URL, 'proxyValidate') + '?' + | ||
urlencode(params)) | ||
|
||
page = urlopen(url) | ||
|
||
try: | ||
response = page.read() | ||
tree = ElementTree.fromstring(response) | ||
if tree[0].tag.endswith('authenticationSuccess'): | ||
username = tree[0][0].text | ||
proxies = [] | ||
if len(tree[0]) > 1: | ||
for element in tree[0][1]: | ||
proxies.append(element.text) | ||
return {"username": username, "proxies": proxies} | ||
else: | ||
return None | ||
finally: | ||
page.close() | ||
|
||
_PROTOCOLS = {'1': _verify_cas1, '2': _verify_cas2} | ||
|
||
if settings.CAS_VERSION not in _PROTOCOLS: | ||
raise ValueError('Unsupported CAS_VERSION %r' % settings.CAS_VERSION) | ||
|
||
_verify = _PROTOCOLS[settings.CAS_VERSION] | ||
|
||
|
||
class CASBackend(object): | ||
"""CAS authentication backend""" | ||
|
||
supports_object_permissions = False | ||
supports_inactive_user = False | ||
|
||
def authenticate(self, ticket, service): | ||
"""Verifies CAS ticket and gets or creates User object | ||
NB: Use of PT to identify proxy | ||
""" | ||
|
||
username = _verify(ticket, service) | ||
if not username: | ||
return None | ||
try: | ||
user = User.objects.get(username__iexact=username) | ||
except User.DoesNotExist: | ||
# user will have an "unusable" password | ||
user = User.objects.create_user(username, '') | ||
user.save() | ||
return user | ||
|
||
def get_user(self, user_id): | ||
"""Retrieve the user's entry in the User model if it exists""" | ||
|
||
try: | ||
return User.objects.get(pk=user_id) | ||
except User.DoesNotExist: | ||
return None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
"""Replacement authentication decorators that work around redirection loops""" | ||
|
||
try: | ||
from functools import wraps | ||
except ImportError: | ||
from django.utils.functional import wraps | ||
|
||
from urllib import urlencode | ||
from django.contrib.auth import REDIRECT_FIELD_NAME | ||
from django.contrib.auth.decorators import login_required | ||
from django.http import HttpResponseForbidden, HttpResponseRedirect | ||
from django.utils.http import urlquote | ||
|
||
__all__ = ['login_required', 'permission_required', 'user_passes_test'] | ||
|
||
def user_passes_test(test_func, login_url=None, | ||
redirect_field_name=REDIRECT_FIELD_NAME): | ||
"""Replacement for django.contrib.auth.decorators.user_passes_test that | ||
returns 403 Forbidden if the user is already logged in. | ||
""" | ||
|
||
if not login_url: | ||
from django.conf import settings | ||
login_url = settings.LOGIN_URL | ||
|
||
def decorator(view_func): | ||
@wraps(view_func) | ||
def wrapper(request, *args, **kwargs): | ||
if test_func(request.user): | ||
return view_func(request, *args, **kwargs) | ||
elif request.user.is_authenticated(): | ||
return HttpResponseForbidden('<h1>Permission denied</h1>') | ||
else: | ||
path = '%s?%s=%s' % (login_url, redirect_field_name, | ||
urlquote(request.get_full_path())) | ||
return HttpResponseRedirect(path) | ||
return wrapper | ||
return decorator | ||
|
||
|
||
def permission_required(perm, login_url=None): | ||
"""Replacement for django.contrib.auth.decorators.permission_required that | ||
returns 403 Forbidden if the user is already logged in. | ||
""" | ||
|
||
return user_passes_test(lambda u: u.has_perm(perm), login_url=login_url) | ||
|
||
|
||
from django.conf import settings | ||
from django.core.exceptions import ImproperlyConfigured | ||
|
||
def gateway(): | ||
"""Authenticates single sign on session if ticket is available, | ||
but doesn't redirect to sign in url otherwise. | ||
""" | ||
if settings.CAS_GATEWAY == False: | ||
raise ImproperlyConfigured('CAS_GATEWAY must be set to True') | ||
def wrap(func): | ||
def wrapped_f(*args): | ||
|
||
from cas.views import login | ||
request = args[0] | ||
|
||
if request.user.is_authenticated(): | ||
#Is Authed, fine | ||
pass | ||
else: | ||
path_with_params = request.path + '?' + urlencode(request.GET.copy()) | ||
if request.GET.get('ticket'): | ||
#Not Authed, but have a ticket! | ||
#Try to authenticate | ||
return login(request, path_with_params, False, True) | ||
else: | ||
#Not Authed, but no ticket | ||
gatewayed = request.GET.get('gatewayed') | ||
if gatewayed == 'true': | ||
pass | ||
else: | ||
#Not Authed, try to authenticate | ||
return login(request, path_with_params, False, True) | ||
|
||
return func(*args) | ||
return wrapped_f | ||
return wrap |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
"CasTicketException, CasConfigException" | ||
from django.core.exceptions import ValidationError | ||
|
||
class CasTicketException(ValidationError): | ||
"""The ticket fails to validate""" | ||
|
||
class CasConfigException(ValidationError): | ||
"""The config is wrong""" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
"""CAS authentication middleware""" | ||
|
||
from urllib import urlencode | ||
|
||
from django.conf import settings | ||
from django.contrib.auth import REDIRECT_FIELD_NAME | ||
from django.contrib.auth import logout as do_logout | ||
from django.contrib.auth.views import login, logout | ||
from django.core.urlresolvers import reverse | ||
from django.http import HttpResponseRedirect, HttpResponseForbidden | ||
from django.core.exceptions import ImproperlyConfigured | ||
|
||
from cas.exceptions import CasTicketException | ||
from cas.views import login as cas_login, logout as cas_logout | ||
|
||
__all__ = ['CASMiddleware'] | ||
|
||
class CASMiddleware(object): | ||
"""Middleware that allows CAS authentication on admin pages""" | ||
|
||
def process_request(self, request): | ||
"""Checks that the authentication middleware is installed""" | ||
|
||
error = ("The Django CAS middleware requires authentication " | ||
"middleware to be installed. Edit your MIDDLEWARE_CLASSES " | ||
"setting to insert 'django.contrib.auth.middleware." | ||
"AuthenticationMiddleware'.") | ||
assert hasattr(request, 'user'), error | ||
|
||
def process_view(self, request, view_func, view_args, view_kwargs): | ||
"""Forwards unauthenticated requests to the admin page to the CAS | ||
login URL, as well as calls to django.contrib.auth.views.login and | ||
logout. | ||
""" | ||
|
||
#if view_func == login: | ||
# return cas_login(request, *view_args, **view_kwargs) | ||
#elif view_func == logout: | ||
# return cas_logout(request, *view_args, **view_kwargs) | ||
|
||
if settings.CAS_ADMIN_PREFIX: | ||
if not request.path.startswith(settings.CAS_ADMIN_PREFIX): | ||
return None | ||
elif not view_func.__module__.startswith('django.contrib.admin.'): | ||
return None | ||
|
||
if request.user.is_authenticated(): | ||
if request.user.is_staff: | ||
return None | ||
else: | ||
error = ('<h1>Forbidden</h1><p>You do not have staff ' | ||
'privileges.</p>') | ||
return HttpResponseForbidden(error) | ||
params = urlencode({REDIRECT_FIELD_NAME: request.get_full_path()}) | ||
return HttpResponseRedirect(reverse(cas_login) + '?' + params) | ||
|
||
def process_exception(self, request, exception): | ||
"""When we get a CasTicketException, that is probably caused by the ticket timing out. | ||
So logout/login and get the same page again.""" | ||
if isinstance(exception, CasTicketException): | ||
do_logout(request) | ||
# This assumes that request.path requires authentication. | ||
return HttpResponseRedirect(request.path) | ||
else: | ||
return None | ||
|
||
class ProxyMiddleware(object): | ||
|
||
# Middleware used to "fake" the django app that it lives at the Proxy Domain | ||
def process_request(self, request): | ||
proxy = getattr(settings, 'PROXY_DOMAIN', None) | ||
if not proxy: | ||
raise ImproperlyConfigured('To use Proxy Middleware you must set a PROXY_DOMAIN setting.') | ||
else: | ||
request.META['HTTP_HOST'] = proxy |
Oops, something went wrong.