Update version to 0.6.3
Bugs fixes ---------- * typos in README.rst * w3c validation Cleaning -------- * Code factorisation (models.py, views.py) * Usage of the documented API for models _meta in auth.DjangoAuthUser Whats new --------- * Add powered by footer * set warn cookie using javascript if possible * Unfold many to many attributes in auth.DjangoAuthUser attributes * Add a github version badge * documents templatetags
This commit is contained in:
commit
07a537b403
17 changed files with 369 additions and 255 deletions
10
README.rst
10
README.rst
|
@ -1,7 +1,7 @@
|
||||||
CAS Server
|
CAS Server
|
||||||
##########
|
##########
|
||||||
|
|
||||||
|travis| |version| |lisence| |codacy| |coverage| |doc|
|
|travis| |coverage| |licence| |github_version| |pypi_version| |codacy| |doc|
|
||||||
|
|
||||||
CAS Server is a Django application implementing the `CAS Protocol 3.0 Specification
|
CAS Server is a Django application implementing the `CAS Protocol 3.0 Specification
|
||||||
<https://apereo.github.io/cas/4.2.x/protocol/CAS-Protocol-Specification.html>`_.
|
<https://apereo.github.io/cas/4.2.x/protocol/CAS-Protocol-Specification.html>`_.
|
||||||
|
@ -206,6 +206,7 @@ Template settings
|
||||||
templates. Set it to ``False`` to disable it.
|
templates. Set it to ``False`` to disable it.
|
||||||
* ``CAS_FAVICON_URL``: URL to the favicon (shortcut icon) used by the default templates.
|
* ``CAS_FAVICON_URL``: URL to the favicon (shortcut icon) used by the default templates.
|
||||||
Default is a key icon. Set it to ``False`` to disable it.
|
Default is a key icon. Set it to ``False`` to disable it.
|
||||||
|
* ``CAS_SHOW_POWERED``: Set it to ``False`` to hide the powered by footer. The default is ``True``.
|
||||||
* ``CAS_COMPONENT_URLS``: URLs to css and javascript external components. It is a dictionnary
|
* ``CAS_COMPONENT_URLS``: URLs to css and javascript external components. It is a dictionnary
|
||||||
and it must have the five following keys: ``"bootstrap3_css"``, ``"bootstrap3_js"``,
|
and it must have the five following keys: ``"bootstrap3_css"``, ``"bootstrap3_js"``,
|
||||||
``"html5shiv"``, ``"respond"``, ``"jquery"``. The default is::
|
``"html5shiv"``, ``"respond"``, ``"jquery"``. The default is::
|
||||||
|
@ -603,10 +604,13 @@ You could for example do as bellow :
|
||||||
.. |travis| image:: https://badges.genua.fr/travis/nitmir/django-cas-server/master.svg
|
.. |travis| image:: https://badges.genua.fr/travis/nitmir/django-cas-server/master.svg
|
||||||
:target: https://travis-ci.org/nitmir/django-cas-server
|
:target: https://travis-ci.org/nitmir/django-cas-server
|
||||||
|
|
||||||
.. |version| image:: https://badges.genua.fr/pypi/v/django-cas-server.svg
|
.. |pypi_version| image:: https://badges.genua.fr/pypi/v/django-cas-server.svg
|
||||||
:target: https://pypi.python.org/pypi/django-cas-server
|
:target: https://pypi.python.org/pypi/django-cas-server
|
||||||
|
|
||||||
.. |lisence| image:: https://badges.genua.fr/pypi/l/django-cas-server.svg
|
.. |github_version| image:: https://badges.genua.fr/github/tag/nitmir/django-cas-server.svg?label=github
|
||||||
|
:target: https://github.com/nitmir/django-cas-server/releases/latest
|
||||||
|
|
||||||
|
.. |licence| image:: https://badges.genua.fr/pypi/l/django-cas-server.svg
|
||||||
:target: https://www.gnu.org/licenses/gpl-3.0.html
|
:target: https://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
.. |codacy| image:: https://badges.genua.fr/codacy/grade/255c21623d6946ef8802fa7995b61366/master.svg
|
.. |codacy| image:: https://badges.genua.fr/codacy/grade/255c21623d6946ef8802fa7995b61366/master.svg
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
"""A django CAS server application"""
|
"""A django CAS server application"""
|
||||||
|
|
||||||
#: version of the application
|
#: version of the application
|
||||||
VERSION = '0.6.2'
|
VERSION = '0.6.3'
|
||||||
|
|
||||||
#: path the the application configuration class
|
#: path the the application configuration class
|
||||||
default_app_config = 'cas_server.apps.CasAppConfig'
|
default_app_config = 'cas_server.apps.CasAppConfig'
|
||||||
|
|
|
@ -369,8 +369,34 @@ class DjangoAuthUser(AuthUser): # pragma: no cover
|
||||||
"""
|
"""
|
||||||
if self.user:
|
if self.user:
|
||||||
attr = {}
|
attr = {}
|
||||||
for field in self.user._meta.fields:
|
# _meta.get_fields() is from the new documented _meta interface in django 1.8
|
||||||
attr[field.attname] = getattr(self.user, field.attname)
|
try:
|
||||||
|
field_names = [
|
||||||
|
field.attname for field in self.user._meta.get_fields()
|
||||||
|
if hasattr(field, "attname")
|
||||||
|
]
|
||||||
|
# backward compatibility with django 1.7
|
||||||
|
except AttributeError: # pragma: no cover (only used by django 1.7)
|
||||||
|
field_names = self.user._meta.get_all_field_names()
|
||||||
|
for name in field_names:
|
||||||
|
attr[name] = getattr(self.user, name)
|
||||||
|
|
||||||
|
# unfold user_permissions many to many relation
|
||||||
|
if 'user_permissions' in attr:
|
||||||
|
attr['user_permissions'] = [
|
||||||
|
(
|
||||||
|
u"%s.%s" % (
|
||||||
|
perm.content_type.model_class().__module__,
|
||||||
|
perm.content_type.model_class().__name__
|
||||||
|
),
|
||||||
|
perm.codename
|
||||||
|
) for perm in attr['user_permissions'].filter()
|
||||||
|
]
|
||||||
|
|
||||||
|
# unfold group many to many relation
|
||||||
|
if 'groups' in attr:
|
||||||
|
attr['groups'] = [group.name for group in attr['groups'].filter()]
|
||||||
|
|
||||||
return attr
|
return attr
|
||||||
else:
|
else:
|
||||||
return {}
|
return {}
|
||||||
|
|
|
@ -20,6 +20,8 @@ from importlib import import_module
|
||||||
CAS_LOGO_URL = static("cas_server/logo.png")
|
CAS_LOGO_URL = static("cas_server/logo.png")
|
||||||
#: URL to the favicon (shortcut icon) used by the default templates. Default is a key icon.
|
#: URL to the favicon (shortcut icon) used by the default templates. Default is a key icon.
|
||||||
CAS_FAVICON_URL = static("cas_server/favicon.ico")
|
CAS_FAVICON_URL = static("cas_server/favicon.ico")
|
||||||
|
#: Show the powered by footer if set to ``True``
|
||||||
|
CAS_SHOW_POWERED = True
|
||||||
#: URLs to css and javascript external components.
|
#: URLs to css and javascript external components.
|
||||||
CAS_COMPONENT_URLS = {
|
CAS_COMPONENT_URLS = {
|
||||||
"bootstrap3_css": "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css",
|
"bootstrap3_css": "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css",
|
||||||
|
|
|
@ -28,13 +28,38 @@ from datetime import timedelta
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from requests_futures.sessions import FuturesSession
|
from requests_futures.sessions import FuturesSession
|
||||||
|
|
||||||
import cas_server.utils as utils
|
from cas_server import utils
|
||||||
from . import VERSION
|
from . import VERSION
|
||||||
|
|
||||||
#: logger facility
|
#: logger facility
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class JsonAttributes(models.Model):
|
||||||
|
"""
|
||||||
|
Bases: :class:`django.db.models.Model`
|
||||||
|
|
||||||
|
A base class for models storing attributes as a json
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
#: The attributes json encoded
|
||||||
|
_attributs = models.TextField(default=None, null=True, blank=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def attributs(self):
|
||||||
|
"""The attributes"""
|
||||||
|
if self._attributs is not None:
|
||||||
|
return utils.json.loads(self._attributs)
|
||||||
|
|
||||||
|
@attributs.setter
|
||||||
|
def attributs(self, value):
|
||||||
|
"""attributs property setter"""
|
||||||
|
self._attributs = utils.json_encode(value)
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class FederatedIendityProvider(models.Model):
|
class FederatedIendityProvider(models.Model):
|
||||||
"""
|
"""
|
||||||
|
@ -130,9 +155,9 @@ class FederatedIendityProvider(models.Model):
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class FederatedUser(models.Model):
|
class FederatedUser(JsonAttributes):
|
||||||
"""
|
"""
|
||||||
Bases: :class:`django.db.models.Model`
|
Bases: :class:`JsonAttributes`
|
||||||
|
|
||||||
A federated user as returner by a CAS provider (username and attributes)
|
A federated user as returner by a CAS provider (username and attributes)
|
||||||
"""
|
"""
|
||||||
|
@ -142,8 +167,6 @@ class FederatedUser(models.Model):
|
||||||
username = models.CharField(max_length=124)
|
username = models.CharField(max_length=124)
|
||||||
#: A foreign key to :class:`FederatedIendityProvider`
|
#: A foreign key to :class:`FederatedIendityProvider`
|
||||||
provider = models.ForeignKey(FederatedIendityProvider, on_delete=models.CASCADE)
|
provider = models.ForeignKey(FederatedIendityProvider, on_delete=models.CASCADE)
|
||||||
#: The user attributes json encoded
|
|
||||||
_attributs = models.TextField(default=None, null=True, blank=True)
|
|
||||||
#: The last ticket used to authenticate :attr:`username` against :attr:`provider`
|
#: The last ticket used to authenticate :attr:`username` against :attr:`provider`
|
||||||
ticket = models.CharField(max_length=255)
|
ticket = models.CharField(max_length=255)
|
||||||
#: Last update timespampt. Usually, the last time :attr:`ticket` has been set.
|
#: Last update timespampt. Usually, the last time :attr:`ticket` has been set.
|
||||||
|
@ -152,17 +175,6 @@ class FederatedUser(models.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.federated_username
|
return self.federated_username
|
||||||
|
|
||||||
@property
|
|
||||||
def attributs(self):
|
|
||||||
"""The user attributes returned by the CAS backend on successful ticket validation"""
|
|
||||||
if self._attributs is not None:
|
|
||||||
return utils.json.loads(self._attributs)
|
|
||||||
|
|
||||||
@attributs.setter
|
|
||||||
def attributs(self, value):
|
|
||||||
"""attributs property setter"""
|
|
||||||
self._attributs = utils.json_encode(value)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def federated_username(self):
|
def federated_username(self):
|
||||||
"""The federated username with a suffix for the current :class:`FederatedUser`."""
|
"""The federated username with a suffix for the current :class:`FederatedUser`."""
|
||||||
|
@ -290,35 +302,23 @@ class User(models.Model):
|
||||||
:param request: The current django HttpRequest to display possible failure to the user.
|
:param request: The current django HttpRequest to display possible failure to the user.
|
||||||
:type request: :class:`django.http.HttpRequest` or :obj:`NoneType<types.NoneType>`
|
:type request: :class:`django.http.HttpRequest` or :obj:`NoneType<types.NoneType>`
|
||||||
"""
|
"""
|
||||||
async_list = []
|
|
||||||
session = FuturesSession(
|
|
||||||
executor=ThreadPoolExecutor(max_workers=settings.CAS_SLO_MAX_PARALLEL_REQUESTS)
|
|
||||||
)
|
|
||||||
# first invalidate all Tickets
|
|
||||||
ticket_classes = [ProxyGrantingTicket, ServiceTicket, ProxyTicket]
|
ticket_classes = [ProxyGrantingTicket, ServiceTicket, ProxyTicket]
|
||||||
for ticket_class in ticket_classes:
|
for error in Ticket.send_slos(
|
||||||
queryset = ticket_class.objects.filter(user=self)
|
[ticket_class.objects.filter(user=self) for ticket_class in ticket_classes]
|
||||||
for ticket in queryset:
|
):
|
||||||
ticket.logout(session, async_list)
|
logger.warning(
|
||||||
queryset.delete()
|
"Error during SLO for user %s: %s" % (
|
||||||
for future in async_list:
|
self.username,
|
||||||
if future: # pragma: no branch (should always be true)
|
error
|
||||||
try:
|
)
|
||||||
future.result()
|
)
|
||||||
except Exception as error:
|
if request is not None:
|
||||||
logger.warning(
|
error = utils.unpack_nested_exception(error)
|
||||||
"Error during SLO for user %s: %s" % (
|
messages.add_message(
|
||||||
self.username,
|
request,
|
||||||
error
|
messages.WARNING,
|
||||||
)
|
_(u'Error during service logout %s') % error
|
||||||
)
|
)
|
||||||
if request is not None:
|
|
||||||
error = utils.unpack_nested_exception(error)
|
|
||||||
messages.add_message(
|
|
||||||
request,
|
|
||||||
messages.WARNING,
|
|
||||||
_(u'Error during service logout %s') % error
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_ticket(self, ticket_class, service, service_pattern, renew):
|
def get_ticket(self, ticket_class, service, service_pattern, renew):
|
||||||
"""
|
"""
|
||||||
|
@ -544,20 +544,13 @@ class ServicePattern(models.Model):
|
||||||
if re.match(filtre.pattern, str(value)):
|
if re.match(filtre.pattern, str(value)):
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
bad_filter = (filtre.pattern, filtre.attribut, user.attributs.get(filtre.attribut))
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"User constraint failed for %s, service %s: %s do not match %s %s." % (
|
"User constraint failed for %s, service %s: %s do not match %s %s." % (
|
||||||
user.username,
|
(user.username, self.name) + bad_filter
|
||||||
self.name,
|
|
||||||
filtre.pattern,
|
|
||||||
filtre.attribut,
|
|
||||||
user.attributs.get(filtre.attribut)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
raise BadFilter('%s do not match %s %s' % (
|
raise BadFilter('%s do not match %s %s' % bad_filter)
|
||||||
filtre.pattern,
|
|
||||||
filtre.attribut,
|
|
||||||
user.attributs.get(filtre.attribut)
|
|
||||||
))
|
|
||||||
if self.user_field and not user.attributs.get(self.user_field):
|
if self.user_field and not user.attributs.get(self.user_field):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Cannot use %s a loggin for user %s on service %s because it is absent" % (
|
"Cannot use %s a loggin for user %s on service %s because it is absent" % (
|
||||||
|
@ -715,9 +708,9 @@ class ReplaceAttributValue(models.Model):
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class Ticket(models.Model):
|
class Ticket(JsonAttributes):
|
||||||
"""
|
"""
|
||||||
Bases: :class:`django.db.models.Model`
|
Bases: :class:`JsonAttributes`
|
||||||
|
|
||||||
Generic class for a Ticket
|
Generic class for a Ticket
|
||||||
"""
|
"""
|
||||||
|
@ -725,8 +718,6 @@ class Ticket(models.Model):
|
||||||
abstract = True
|
abstract = True
|
||||||
#: ForeignKey to a :class:`User`.
|
#: ForeignKey to a :class:`User`.
|
||||||
user = models.ForeignKey(User, related_name="%(class)s")
|
user = models.ForeignKey(User, related_name="%(class)s")
|
||||||
#: The user attributes to transmit to the service json encoded
|
|
||||||
_attributs = models.TextField(default=None, null=True, blank=True)
|
|
||||||
#: A boolean. ``True`` if the ticket has been validated
|
#: A boolean. ``True`` if the ticket has been validated
|
||||||
validate = models.BooleanField(default=False)
|
validate = models.BooleanField(default=False)
|
||||||
#: The service url for the ticket
|
#: The service url for the ticket
|
||||||
|
@ -749,17 +740,6 @@ class Ticket(models.Model):
|
||||||
#: requests.
|
#: requests.
|
||||||
TIMEOUT = settings.CAS_TICKET_TIMEOUT
|
TIMEOUT = settings.CAS_TICKET_TIMEOUT
|
||||||
|
|
||||||
@property
|
|
||||||
def attributs(self):
|
|
||||||
"""The user attributes to be transmited to the service on successful validation"""
|
|
||||||
if self._attributs is not None:
|
|
||||||
return utils.json.loads(self._attributs)
|
|
||||||
|
|
||||||
@attributs.setter
|
|
||||||
def attributs(self, value):
|
|
||||||
"""attributs property setter"""
|
|
||||||
self._attributs = utils.json_encode(value)
|
|
||||||
|
|
||||||
class DoesNotExist(Exception):
|
class DoesNotExist(Exception):
|
||||||
"""raised in :meth:`Ticket.get` then ticket prefix and ticket classes mismatch"""
|
"""raised in :meth:`Ticket.get` then ticket prefix and ticket classes mismatch"""
|
||||||
pass
|
pass
|
||||||
|
@ -767,6 +747,33 @@ class Ticket(models.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return u"Ticket-%s" % self.pk
|
return u"Ticket-%s" % self.pk
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def send_slos(queryset_list):
|
||||||
|
"""
|
||||||
|
Send SLO requests to each ticket of each queryset of ``queryset_list``
|
||||||
|
|
||||||
|
:param list queryset_list: A list a :class:`Ticket` queryset
|
||||||
|
:return: A list of possibly encoutered :class:`Exception`
|
||||||
|
:rtype: list
|
||||||
|
"""
|
||||||
|
# sending SLO to timed-out validated tickets
|
||||||
|
async_list = []
|
||||||
|
session = FuturesSession(
|
||||||
|
executor=ThreadPoolExecutor(max_workers=settings.CAS_SLO_MAX_PARALLEL_REQUESTS)
|
||||||
|
)
|
||||||
|
errors = []
|
||||||
|
for queryset in queryset_list:
|
||||||
|
for ticket in queryset:
|
||||||
|
ticket.logout(session, async_list)
|
||||||
|
queryset.delete()
|
||||||
|
for future in async_list:
|
||||||
|
if future: # pragma: no branch (should always be true)
|
||||||
|
try:
|
||||||
|
future.result()
|
||||||
|
except Exception as error:
|
||||||
|
errors.append(error)
|
||||||
|
return errors
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def clean_old_entries(cls):
|
def clean_old_entries(cls):
|
||||||
"""Remove old ticket and send SLO to timed-out services"""
|
"""Remove old ticket and send SLO to timed-out services"""
|
||||||
|
@ -779,25 +786,12 @@ class Ticket(models.Model):
|
||||||
Q(creation__lt=(timezone.now() - timedelta(seconds=cls.VALIDITY)))
|
Q(creation__lt=(timezone.now() - timedelta(seconds=cls.VALIDITY)))
|
||||||
)
|
)
|
||||||
).delete()
|
).delete()
|
||||||
|
|
||||||
# sending SLO to timed-out validated tickets
|
|
||||||
async_list = []
|
|
||||||
session = FuturesSession(
|
|
||||||
executor=ThreadPoolExecutor(max_workers=settings.CAS_SLO_MAX_PARALLEL_REQUESTS)
|
|
||||||
)
|
|
||||||
queryset = cls.objects.filter(
|
queryset = cls.objects.filter(
|
||||||
creation__lt=(timezone.now() - timedelta(seconds=cls.TIMEOUT))
|
creation__lt=(timezone.now() - timedelta(seconds=cls.TIMEOUT))
|
||||||
)
|
)
|
||||||
for ticket in queryset:
|
for error in cls.send_slos([queryset]):
|
||||||
ticket.logout(session, async_list)
|
logger.warning("Error durring SLO %s" % error)
|
||||||
queryset.delete()
|
sys.stderr.write("%r\n" % error)
|
||||||
for future in async_list:
|
|
||||||
if future: # pragma: no branch (should always be true)
|
|
||||||
try:
|
|
||||||
future.result()
|
|
||||||
except Exception as error:
|
|
||||||
logger.warning("Error durring SLO %s" % error)
|
|
||||||
sys.stderr.write("%r\n" % error)
|
|
||||||
|
|
||||||
def logout(self, session, async_list=None):
|
def logout(self, session, async_list=None):
|
||||||
"""Send a SLO request to the ticket service"""
|
"""Send a SLO request to the ticket service"""
|
||||||
|
@ -811,16 +805,7 @@ class Ticket(models.Model):
|
||||||
self.user.username
|
self.user.username
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
xml = u"""<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
|
xml = utils.logout_request(self.value)
|
||||||
ID="%(id)s" Version="2.0" IssueInstant="%(datetime)s">
|
|
||||||
<saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"></saml:NameID>
|
|
||||||
<samlp:SessionIndex>%(ticket)s</samlp:SessionIndex>
|
|
||||||
</samlp:LogoutRequest>""" % \
|
|
||||||
{
|
|
||||||
'id': utils.gen_saml_id(),
|
|
||||||
'datetime': timezone.now().isoformat(),
|
|
||||||
'ticket': self.value
|
|
||||||
}
|
|
||||||
if self.service_pattern.single_log_out_callback:
|
if self.service_pattern.single_log_out_callback:
|
||||||
url = self.service_pattern.single_log_out_callback
|
url = self.service_pattern.single_log_out_callback
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
function alert_version(last_version){
|
|
||||||
jQuery(function( $ ){
|
|
||||||
$("#alert-version").click(function( e ){
|
|
||||||
e.preventDefault();
|
|
||||||
var date = new Date();
|
|
||||||
date.setTime(date.getTime()+(10*365*24*60*60*1000));
|
|
||||||
var expires = "; expires="+date.toGMTString();
|
|
||||||
document.cookie = "cas-alert-version=" + last_version + expires + "; path=/";
|
|
||||||
});
|
|
||||||
|
|
||||||
var nameEQ="cas-alert-version=";
|
|
||||||
var ca = document.cookie.split(";");
|
|
||||||
var value;
|
|
||||||
for(var i=0;i < ca.length;i++) {
|
|
||||||
var c = ca[i];
|
|
||||||
while(c.charAt(0) === " "){
|
|
||||||
c = c.substring(1,c.length);
|
|
||||||
}
|
|
||||||
if(c.indexOf(nameEQ) === 0){
|
|
||||||
value = c.substring(nameEQ.length,c.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(value === last_version){
|
|
||||||
$("#alert-version").parent().hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
44
cas_server/static/cas_server/functions.js
Normal file
44
cas_server/static/cas_server/functions.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
function createCookie(name, value, days){
|
||||||
|
var expires;
|
||||||
|
var date;
|
||||||
|
if(days){
|
||||||
|
date = new Date();
|
||||||
|
date.setTime(date.getTime()+(days*24*60*60*1000));
|
||||||
|
expires = "; expires="+date.toGMTString();
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
expires = "";
|
||||||
|
}
|
||||||
|
document.cookie = name + "=" + value + expires + "; path=/";
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCookie(name){
|
||||||
|
var nameEQ = name + "=";
|
||||||
|
var ca = document.cookie.split(";");
|
||||||
|
for(var i=0;i < ca.length;i++) {
|
||||||
|
var c = ca[i];
|
||||||
|
while (c.charAt(0) === " "){
|
||||||
|
c = c.substring(1,c.length);
|
||||||
|
}
|
||||||
|
if (c.indexOf(nameEQ) === 0){
|
||||||
|
return c.substring(nameEQ.length,c.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function eraseCookie(name) {
|
||||||
|
createCookie(name,"",-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function alert_version(last_version){
|
||||||
|
jQuery(function( $ ){
|
||||||
|
$("#alert-version").click(function( e ){
|
||||||
|
e.preventDefault();
|
||||||
|
createCookie("cas-alert-version", last_version, 10*365);
|
||||||
|
});
|
||||||
|
if(readCookie("cas-alert-version") === last_version){
|
||||||
|
$("#alert-version").parent().hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,6 +1,9 @@
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
padding-top: 40px;
|
padding-top: 40px;
|
||||||
padding-bottom: 40px;
|
padding-bottom: 0;
|
||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +44,22 @@ body {
|
||||||
width:110px;
|
width:110px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Wrapper for page content to push down footer */
|
||||||
|
#wrap {
|
||||||
|
min-height: 100%;
|
||||||
|
height: auto;
|
||||||
|
height: 100%;
|
||||||
|
/* Negative indent footer by it's height */
|
||||||
|
margin: 0 auto -40px;
|
||||||
|
}
|
||||||
|
#footer {
|
||||||
|
height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#footer p {
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 680px) {
|
@media screen and (max-width: 680px) {
|
||||||
#app-name {
|
#app-name {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
{% load i18n %}
|
{% load i18n %}{% load staticfiles %}<!DOCTYPE html>
|
||||||
{% load staticfiles %}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html{% if request.LANGUAGE_CODE %} lang="{{ request.LANGUAGE_CODE }}"{% endif %}>
|
<html{% if request.LANGUAGE_CODE %} lang="{{ request.LANGUAGE_CODE }}"{% endif %}>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
@ -18,12 +16,13 @@
|
||||||
<link href="{% static "cas_server/styles.css" %}" rel="stylesheet">
|
<link href="{% static "cas_server/styles.css" %}" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div id="wrap">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{% if auto_submit %}<noscript>{% endif %}
|
{% if auto_submit %}<noscript>{% endif %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||||
<h1 id="app-name">
|
<h1 id="app-name">
|
||||||
{% if settings.CAS_LOGO_URL %}<img src="{{settings.CAS_LOGO_URL}}"></img> {% endif %}
|
{% if settings.CAS_LOGO_URL %}<img src="{{settings.CAS_LOGO_URL}}" alt="cas-logo" />{% endif %}
|
||||||
{% trans "Central Authentication Service" %}</h1>
|
{% trans "Central Authentication Service" %}</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -53,7 +52,7 @@
|
||||||
class="alert alert-danger"
|
class="alert alert-danger"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endspaceless %}>
|
{% endspaceless %}>
|
||||||
{{ message }}
|
{{message|safe}}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if auto_submit %}</noscript>{% endif %}
|
{% if auto_submit %}</noscript>{% endif %}
|
||||||
|
@ -62,11 +61,25 @@
|
||||||
<div class="col-lg-3 col-md-3 col-sm-2 col-xs-0"></div>
|
<div class="col-lg-3 col-md-3 col-sm-2 col-xs-0"></div>
|
||||||
</div>
|
</div>
|
||||||
</div> <!-- /container -->
|
</div> <!-- /container -->
|
||||||
<script src="{{settings.CAS_COMPONENT_URLS.jquery}}"></script>
|
</div>
|
||||||
<script src="{{settings.CAS_COMPONENT_URLS.bootstrap3_js}}"></script>
|
<div style="clear: both;"></div>
|
||||||
{% if settings.CAS_NEW_VERSION_HTML_WARNING and upgrade_available %}
|
{% if settings.CAS_SHOW_POWERED %}
|
||||||
<script src="{% static "cas_server/alert-version.js" %}"></script>
|
<div id="footer">
|
||||||
<script>alert_version("{{LAST_VERSION}}")</script>
|
<p><a class="text-muted" href="https://pypi.python.org/pypi/django-cas-server">django-cas-server powered</a></p>
|
||||||
{% endif %}
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<script src="{{settings.CAS_COMPONENT_URLS.jquery}}"></script>
|
||||||
|
<script src="{{settings.CAS_COMPONENT_URLS.bootstrap3_js}}"></script>
|
||||||
|
<script src="{% static "cas_server/functions.js" %}"></script>
|
||||||
|
{% if settings.CAS_NEW_VERSION_HTML_WARNING and upgrade_available %}
|
||||||
|
<script type="text/javascript">alert_version("{{LAST_VERSION}}")</script>
|
||||||
|
{% endif %}
|
||||||
|
{% block javascript %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
<!--
|
||||||
|
Powered by django-cas-server version {{VERSION}}
|
||||||
|
|
||||||
|
Pypi: https://pypi.python.org/pypi/django-cas-server
|
||||||
|
github: https://github.com/nitmir/django-cas-server
|
||||||
|
-->
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
{% endif %}"
|
{% endif %}"
|
||||||
{% endspaceless %}>{% spaceless %}
|
{% endspaceless %}>{% spaceless %}
|
||||||
{% if field|is_checkbox %}
|
{% if field|is_checkbox %}
|
||||||
<div class="checkbox"><label for="{{field.auto_id}}">{{field}}{{field.label}}</label>
|
<div class="checkbox"><label for="{{field.auto_id}}">{{field}}{{field.label}}</label></div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<label class="control-label" for="{{field.auto_id}}">{{field.label}}</label>
|
<label class="control-label" for="{{field.auto_id}}">{{field.label}}</label>
|
||||||
{{field}}
|
{{field}}
|
||||||
|
|
|
@ -14,10 +14,17 @@
|
||||||
<button class="btn btn-primary btn-block btn-lg" type="submit">{% trans "Login" %}</button>
|
<button class="btn btn-primary btn-block btn-lg" type="submit">{% trans "Login" %}</button>
|
||||||
{% if auto_submit %}</noscript>{% endif %}
|
{% if auto_submit %}</noscript>{% endif %}
|
||||||
</form>
|
</form>
|
||||||
{% if auto_submit %}
|
|
||||||
<script type="text/javascript">
|
|
||||||
document.getElementById('login_form').submit(); // SUBMIT FORM
|
|
||||||
</script>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
{% block javascript %}<script type="text/javascript">
|
||||||
|
jQuery(function( $ ){
|
||||||
|
$("#id_warn").click(function(e){
|
||||||
|
if($("#id_warn").is(':checked')){
|
||||||
|
createCookie("warn", "on", 10 * 365);
|
||||||
|
} else {
|
||||||
|
eraseCookie("warn");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});{% if auto_submit %}
|
||||||
|
document.getElementById('login_form').submit(); // SUBMIT FORM{% endif %}
|
||||||
|
</script>{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -261,7 +261,7 @@ class FederateAuthLoginLogoutTestCase(
|
||||||
# SLO for an unkown ticket should do nothing
|
# SLO for an unkown ticket should do nothing
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/federate/%s" % provider.suffix,
|
"/federate/%s" % provider.suffix,
|
||||||
{'logoutRequest': tests_utils.logout_request(utils.gen_st())}
|
{'logoutRequest': utils.logout_request(utils.gen_st())}
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response.content, b"ok")
|
self.assertEqual(response.content, b"ok")
|
||||||
|
@ -288,7 +288,7 @@ class FederateAuthLoginLogoutTestCase(
|
||||||
# 3 or 'CAS_2_SAML_1_0'
|
# 3 or 'CAS_2_SAML_1_0'
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/federate/%s" % provider.suffix,
|
"/federate/%s" % provider.suffix,
|
||||||
{'logoutRequest': tests_utils.logout_request(ticket)}
|
{'logoutRequest': utils.logout_request(ticket)}
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response.content, b"ok")
|
self.assertEqual(response.content, b"ok")
|
||||||
|
|
32
cas_server/tests/test_templatetags.py
Normal file
32
cas_server/tests/test_templatetags.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License version 3 for
|
||||||
|
# more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License version 3
|
||||||
|
# along with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||||
|
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
#
|
||||||
|
# (c) 2016 Valentin Samir
|
||||||
|
"""tests for the customs template tags"""
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from cas_server import forms
|
||||||
|
from cas_server.templatetags import cas_server
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateTagsTestCase(TestCase):
|
||||||
|
"""tests for the customs template tags"""
|
||||||
|
|
||||||
|
def test_is_checkbox(self):
|
||||||
|
"""test for the template filter is_checkbox"""
|
||||||
|
form = forms.UserCredential()
|
||||||
|
self.assertFalse(cas_server.is_checkbox(form["username"]))
|
||||||
|
self.assertTrue(cas_server.is_checkbox(form["warn"]))
|
||||||
|
|
||||||
|
def test_is_hidden(self):
|
||||||
|
"""test for the template filter is_hidden"""
|
||||||
|
form = forms.UserCredential()
|
||||||
|
self.assertFalse(cas_server.is_hidden(form["username"]))
|
||||||
|
self.assertTrue(cas_server.is_hidden(form["lt"]))
|
|
@ -115,8 +115,8 @@ def get_validated_ticket(service):
|
||||||
|
|
||||||
client = Client()
|
client = Client()
|
||||||
response = client.get('/validate', {'ticket': ticket.value, 'service': service})
|
response = client.get('/validate', {'ticket': ticket.value, 'service': service})
|
||||||
assert (response.status_code == 200)
|
assert response.status_code == 200
|
||||||
assert (response.content == b'yes\ntest\n')
|
assert response.content == b'yes\ntest\n'
|
||||||
|
|
||||||
ticket = models.ServiceTicket.objects.get(value=ticket.value)
|
ticket = models.ServiceTicket.objects.get(value=ticket.value)
|
||||||
return (auth_client, ticket)
|
return (auth_client, ticket)
|
||||||
|
@ -222,6 +222,10 @@ class Http404Handler(HttpParamsHandler):
|
||||||
|
|
||||||
class DummyCAS(BaseHTTPServer.BaseHTTPRequestHandler):
|
class DummyCAS(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||||
"""A dummy CAS that validate for only one (service, ticket) used in federated mode tests"""
|
"""A dummy CAS that validate for only one (service, ticket) used in federated mode tests"""
|
||||||
|
|
||||||
|
#: dict of the last receive GET parameters
|
||||||
|
params = None
|
||||||
|
|
||||||
def test_params(self):
|
def test_params(self):
|
||||||
"""check that internal and provided (service, ticket) matches"""
|
"""check that internal and provided (service, ticket) matches"""
|
||||||
if (
|
if (
|
||||||
|
@ -340,17 +344,3 @@ class DummyCAS(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||||
httpd_thread.daemon = True
|
httpd_thread.daemon = True
|
||||||
httpd_thread.start()
|
httpd_thread.start()
|
||||||
return (httpd, host, port)
|
return (httpd, host, port)
|
||||||
|
|
||||||
|
|
||||||
def logout_request(ticket):
|
|
||||||
"""build a SLO request XML, ready to be send"""
|
|
||||||
return u"""<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
|
|
||||||
ID="%(id)s" Version="2.0" IssueInstant="%(datetime)s">
|
|
||||||
<saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"></saml:NameID>
|
|
||||||
<samlp:SessionIndex>%(ticket)s</samlp:SessionIndex>
|
|
||||||
</samlp:LogoutRequest>""" % \
|
|
||||||
{
|
|
||||||
'id': utils.gen_saml_id(),
|
|
||||||
'datetime': timezone.now().isoformat(),
|
|
||||||
'ticket': ticket
|
|
||||||
}
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ from django.http import HttpResponseRedirect, HttpResponse
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.messages import constants as DEFAULT_MESSAGE_LEVELS
|
from django.contrib.messages import constants as DEFAULT_MESSAGE_LEVELS
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
|
@ -680,3 +681,22 @@ def dictfetchall(cursor):
|
||||||
dict(zip(columns, row))
|
dict(zip(columns, row))
|
||||||
for row in cursor.fetchall()
|
for row in cursor.fetchall()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def logout_request(ticket):
|
||||||
|
"""
|
||||||
|
Forge a SLO logout request
|
||||||
|
|
||||||
|
:param unicode ticket: A ticket value
|
||||||
|
:return: A SLO XML body request
|
||||||
|
:rtype: unicode
|
||||||
|
"""
|
||||||
|
return u"""<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
|
||||||
|
ID="%(id)s" Version="2.0" IssueInstant="%(datetime)s">
|
||||||
|
<saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"></saml:NameID>
|
||||||
|
<samlp:SessionIndex>%(ticket)s</samlp:SessionIndex>
|
||||||
|
</samlp:LogoutRequest>""" % {
|
||||||
|
'id': gen_saml_id(),
|
||||||
|
'datetime': timezone.now().isoformat(),
|
||||||
|
'ticket': ticket
|
||||||
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class LogoutMixin(object):
|
class LogoutMixin(object):
|
||||||
"""destroy CAS session utils"""
|
"""destroy CAS session utils"""
|
||||||
|
|
||||||
def logout(self, all_session=False):
|
def logout(self, all_session=False):
|
||||||
"""
|
"""
|
||||||
effectively destroy a CAS session
|
effectively destroy a CAS session
|
||||||
|
@ -63,43 +64,59 @@ class LogoutMixin(object):
|
||||||
logger.info("Logging out user %s from all of they sessions." % username)
|
logger.info("Logging out user %s from all of they sessions." % username)
|
||||||
else:
|
else:
|
||||||
logger.info("Logging out user %s." % username)
|
logger.info("Logging out user %s." % username)
|
||||||
# logout the user from the current session
|
users = []
|
||||||
|
# try to get the user from the current session
|
||||||
try:
|
try:
|
||||||
user = models.User.objects.get(
|
users.append(
|
||||||
username=username,
|
models.User.objects.get(
|
||||||
session_key=self.request.session.session_key
|
username=username,
|
||||||
|
session_key=self.request.session.session_key
|
||||||
|
)
|
||||||
)
|
)
|
||||||
# flush the session
|
except models.User.DoesNotExist:
|
||||||
|
# if user not found in database, flush the session anyway
|
||||||
self.request.session.flush()
|
self.request.session.flush()
|
||||||
|
|
||||||
|
# If all_session is set, search all of the user sessions
|
||||||
|
if all_session:
|
||||||
|
users.extend(
|
||||||
|
models.User.objects.filter(
|
||||||
|
username=username
|
||||||
|
).exclude(
|
||||||
|
session_key=self.request.session.session_key
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Iterate over all user sessions that have to be logged out
|
||||||
|
for user in users:
|
||||||
|
# get the user session
|
||||||
|
session = SessionStore(session_key=user.session_key)
|
||||||
|
# flush the session
|
||||||
|
session.flush()
|
||||||
# send SLO requests
|
# send SLO requests
|
||||||
user.logout(self.request)
|
user.logout(self.request)
|
||||||
# delete the user
|
# delete the user
|
||||||
user.delete()
|
user.delete()
|
||||||
# increment the destroyed session counter
|
# increment the destroyed session counter
|
||||||
session_nb += 1
|
session_nb += 1
|
||||||
except models.User.DoesNotExist:
|
|
||||||
# if user not found in database, flush the session anyway
|
|
||||||
self.request.session.flush()
|
|
||||||
|
|
||||||
# If all_session is set logout user from alternative sessions
|
|
||||||
if all_session:
|
|
||||||
# Iterate over all user sessions
|
|
||||||
for user in models.User.objects.filter(username=username):
|
|
||||||
# get the user session
|
|
||||||
session = SessionStore(session_key=user.session_key)
|
|
||||||
# flush the session
|
|
||||||
session.flush()
|
|
||||||
# send SLO requests
|
|
||||||
user.logout(self.request)
|
|
||||||
# delete the user
|
|
||||||
user.delete()
|
|
||||||
# increment the destroyed session counter
|
|
||||||
session_nb += 1
|
|
||||||
if username:
|
if username:
|
||||||
logger.info("User %s logged out" % username)
|
logger.info("User %s logged out" % username)
|
||||||
return session_nb
|
return session_nb
|
||||||
|
|
||||||
|
|
||||||
|
class CsrfExemptView(View):
|
||||||
|
"""base class for csrf exempt class views"""
|
||||||
|
|
||||||
|
@method_decorator(csrf_exempt) # csrf is disabled for allowing SLO requests reception
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
dispatch different http request to the methods of the same name
|
||||||
|
|
||||||
|
:param django.http.HttpRequest request: The current request object
|
||||||
|
"""
|
||||||
|
return super(CsrfExemptView, self).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class LogoutView(View, LogoutMixin):
|
class LogoutView(View, LogoutMixin):
|
||||||
"""destroy CAS session (logout) view"""
|
"""destroy CAS session (logout) view"""
|
||||||
|
|
||||||
|
@ -210,17 +227,15 @@ class LogoutView(View, LogoutMixin):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class FederateAuth(View):
|
class FederateAuth(CsrfExemptView):
|
||||||
"""view to authenticated user agains a backend CAS then CAS_FEDERATE is True"""
|
"""
|
||||||
|
view to authenticated user agains a backend CAS then CAS_FEDERATE is True
|
||||||
|
|
||||||
@method_decorator(csrf_exempt) # csrf is disabled for allowing SLO requests reception
|
csrf is disabled for allowing SLO requests reception.
|
||||||
def dispatch(self, request, *args, **kwargs):
|
"""
|
||||||
"""
|
|
||||||
dispatch different http request to the methods of the same name
|
|
||||||
|
|
||||||
:param django.http.HttpRequest request: The current request object
|
#: current URL used as service URL by the CAS client
|
||||||
"""
|
service_url = None
|
||||||
return super(FederateAuth, self).dispatch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def get_cas_client(self, request, provider, renew=False):
|
def get_cas_client(self, request, provider, renew=False):
|
||||||
"""
|
"""
|
||||||
|
@ -285,7 +300,7 @@ class FederateAuth(View):
|
||||||
"""
|
"""
|
||||||
method called on GET request
|
method called on GET request
|
||||||
|
|
||||||
:param django.http.HttpRequest request: The current request object
|
:param django.http.HttpRequestself. request: The current request object
|
||||||
:param unicode provider: Optional parameter. The user provider suffix.
|
:param unicode provider: Optional parameter. The user provider suffix.
|
||||||
"""
|
"""
|
||||||
# if settings.CAS_FEDERATE is not True redirect to the login page
|
# if settings.CAS_FEDERATE is not True redirect to the login page
|
||||||
|
@ -923,18 +938,13 @@ class LoginView(View, LogoutMixin):
|
||||||
return self.not_authenticated()
|
return self.not_authenticated()
|
||||||
|
|
||||||
|
|
||||||
class Auth(View):
|
class Auth(CsrfExemptView):
|
||||||
"""A simple view to validate username/password/service tuple"""
|
"""
|
||||||
# csrf is disable as it is intended to be used by programs. Security is assured by a shared
|
A simple view to validate username/password/service tuple
|
||||||
# secret between the programs dans django-cas-server.
|
|
||||||
@method_decorator(csrf_exempt)
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
dispatch requests based on method GET, POST, ...
|
|
||||||
|
|
||||||
:param django.http.HttpRequest request: The current request object
|
csrf is disable as it is intended to be used by programs. Security is assured by a shared
|
||||||
"""
|
secret between the programs dans django-cas-server.
|
||||||
return super(Auth, self).dispatch(request, *args, **kwargs)
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def post(request):
|
def post(request):
|
||||||
|
@ -1041,8 +1051,9 @@ class Validate(View):
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class ValidateError(Exception):
|
class ValidationBaseError(Exception):
|
||||||
"""handle service validation error"""
|
"""Base class for both saml and cas validation error"""
|
||||||
|
|
||||||
#: The error code
|
#: The error code
|
||||||
code = None
|
code = None
|
||||||
#: The error message
|
#: The error message
|
||||||
|
@ -1051,7 +1062,7 @@ class ValidateError(Exception):
|
||||||
def __init__(self, code, msg=""):
|
def __init__(self, code, msg=""):
|
||||||
self.code = code
|
self.code = code
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
super(ValidateError, self).__init__(code)
|
super(ValidationBaseError, self).__init__(code)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return u"%s" % self.msg
|
return u"%s" % self.msg
|
||||||
|
@ -1066,12 +1077,27 @@ class ValidateError(Exception):
|
||||||
"""
|
"""
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"cas_server/serviceValidateError.xml",
|
self.template,
|
||||||
{'code': self.code, 'msg': self.msg},
|
self.context(), content_type="text/xml; charset=utf-8"
|
||||||
content_type="text/xml; charset=utf-8"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ValidateError(ValidationBaseError):
|
||||||
|
"""handle service validation error"""
|
||||||
|
|
||||||
|
#: template to be render for the error
|
||||||
|
template = "cas_server/serviceValidateError.xml"
|
||||||
|
|
||||||
|
def context(self):
|
||||||
|
"""
|
||||||
|
content to use to render :attr:`template`
|
||||||
|
|
||||||
|
:return: A dictionary to contextualize :attr:`template`
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
return {'code': self.code, 'msg': self.msg}
|
||||||
|
|
||||||
|
|
||||||
class ValidateService(View):
|
class ValidateService(View):
|
||||||
"""service ticket validation [CAS 2.0] and [CAS 3.0]"""
|
"""service ticket validation [CAS 2.0] and [CAS 3.0]"""
|
||||||
#: Current :class:`django.http.HttpRequest` object
|
#: Current :class:`django.http.HttpRequest` object
|
||||||
|
@ -1333,59 +1359,32 @@ class Proxy(View):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
class SamlValidateError(ValidationBaseError):
|
||||||
class SamlValidateError(Exception):
|
|
||||||
"""handle saml validation error"""
|
"""handle saml validation error"""
|
||||||
#: The error code
|
|
||||||
code = None
|
|
||||||
#: The error message
|
|
||||||
msg = None
|
|
||||||
|
|
||||||
def __init__(self, code, msg=""):
|
#: template to be render for the error
|
||||||
self.code = code
|
template = "cas_server/samlValidateError.xml"
|
||||||
self.msg = msg
|
|
||||||
super(SamlValidateError, self).__init__(code)
|
|
||||||
|
|
||||||
def __str__(self):
|
def context(self):
|
||||||
return u"%s" % self.msg
|
|
||||||
|
|
||||||
def render(self, request):
|
|
||||||
"""
|
"""
|
||||||
render the error template for the exception
|
:return: A dictionary to contextualize :attr:`template`
|
||||||
|
:rtype: dict
|
||||||
:param django.http.HttpRequest request: The current request object:
|
|
||||||
:return: the rendered ``cas_server/samlValidateError.xml`` template
|
|
||||||
:rtype: django.http.HttpResponse
|
|
||||||
"""
|
"""
|
||||||
return render(
|
return {
|
||||||
request,
|
'code': self.code,
|
||||||
"cas_server/samlValidateError.xml",
|
'msg': self.msg,
|
||||||
{
|
'IssueInstant': timezone.now().isoformat(),
|
||||||
'code': self.code,
|
'ResponseID': utils.gen_saml_id()
|
||||||
'msg': self.msg,
|
}
|
||||||
'IssueInstant': timezone.now().isoformat(),
|
|
||||||
'ResponseID': utils.gen_saml_id()
|
|
||||||
},
|
|
||||||
content_type="text/xml; charset=utf-8"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SamlValidate(View):
|
class SamlValidate(CsrfExemptView):
|
||||||
"""SAML ticket validation"""
|
"""SAML ticket validation"""
|
||||||
request = None
|
request = None
|
||||||
target = None
|
target = None
|
||||||
ticket = None
|
ticket = None
|
||||||
root = None
|
root = None
|
||||||
|
|
||||||
@method_decorator(csrf_exempt)
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
dispatch requests based on method GET, POST, ...
|
|
||||||
|
|
||||||
:param django.http.HttpRequest request: The current request object
|
|
||||||
"""
|
|
||||||
return super(SamlValidate, self).dispatch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""
|
"""
|
||||||
methode called on POST request on this view
|
methode called on POST request on this view
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -62,7 +62,7 @@ if __name__ == '__main__':
|
||||||
'lxml >= 3.4', 'six >= 1'
|
'lxml >= 3.4', 'six >= 1'
|
||||||
],
|
],
|
||||||
url="https://github.com/nitmir/django-cas-server",
|
url="https://github.com/nitmir/django-cas-server",
|
||||||
download_url="https://github.com/nitmir/django-cas-server/releases",
|
download_url="https://github.com/nitmir/django-cas-server/releases/latest",
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
setup_requires=['pytest-runner'],
|
setup_requires=['pytest-runner'],
|
||||||
tests_require=['pytest', 'pytest-django', 'pytest-pythonpath', 'pytest-warnings', 'mock>=1'],
|
tests_require=['pytest', 'pytest-django', 'pytest-pythonpath', 'pytest-warnings', 'mock>=1'],
|
||||||
|
|
Loading…
Reference in a new issue