django-cas-server/cas_server/models.py

344 lines
12 KiB
Python
Raw Normal View History

2015-05-16 21:43:46 +00:00
# ⁻*- coding: utf-8 -*-
2015-05-27 20:10:06 +00:00
# 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) 2015 Valentin Samir
2015-05-27 19:56:39 +00:00
"""models for the app"""
2015-05-27 20:18:01 +00:00
from . import default_settings
2015-05-16 21:43:46 +00:00
from django.conf import settings
from django.db import models
from django.contrib import messages
from picklefield.fields import PickledObjectField
2015-05-22 15:55:00 +00:00
from django.utils.translation import ugettext as _
2015-05-16 21:43:46 +00:00
import re
import os
import time
import random
import string
2015-05-18 18:30:00 +00:00
from concurrent.futures import ThreadPoolExecutor
from requests_futures.sessions import FuturesSession
2015-05-16 21:43:46 +00:00
2015-05-27 19:56:39 +00:00
from . import utils
2015-05-16 21:43:46 +00:00
def _gen_ticket(prefix):
2015-05-27 19:56:39 +00:00
"""Generate a ticket with prefix `prefix`"""
return '%s-%s' % (
prefix,
''.join(
random.choice(
string.ascii_letters + string.digits
) for _ in range(settings.CAS_ST_LEN)
)
)
2015-05-16 21:43:46 +00:00
def _gen_st():
2015-05-27 19:56:39 +00:00
"""Generate a Service Ticket"""
2015-05-16 21:43:46 +00:00
return _gen_ticket('ST')
def _gen_pt():
2015-05-27 19:56:39 +00:00
"""Generate a Proxy Ticket"""
2015-05-16 21:43:46 +00:00
return _gen_ticket('PT')
def _gen_pgt():
2015-05-27 19:56:39 +00:00
"""Generate a Proxy Granting Ticket"""
2015-05-16 21:43:46 +00:00
return _gen_ticket('PGT')
2015-05-27 19:56:39 +00:00
def gen_pgtiou():
"""Generate a Proxy Granting Ticket IOU"""
return _gen_ticket('PGTIOU')
2015-05-16 21:43:46 +00:00
class User(models.Model):
2015-05-27 19:56:39 +00:00
"""A user logged into the CAS"""
2015-05-16 21:43:46 +00:00
username = models.CharField(max_length=30, unique=True)
attributs = PickledObjectField()
date = models.DateTimeField(auto_now_add=True, auto_now=True)
def __unicode__(self):
return self.username
def logout(self, request):
2015-05-27 19:56:39 +00:00
"""Sending SSO request to all services the user logged in"""
2015-05-18 18:30:00 +00:00
async_list = []
session = FuturesSession(executor=ThreadPoolExecutor(max_workers=10))
2015-05-23 19:12:42 +00:00
for ticket in ServiceTicket.objects.filter(user=self, validate=True):
2015-05-18 18:30:00 +00:00
async_list.append(ticket.logout(request, session))
2015-05-16 21:43:46 +00:00
ticket.delete()
2015-05-23 19:12:42 +00:00
for ticket in ProxyTicket.objects.filter(user=self, validate=True):
2015-05-18 18:30:00 +00:00
async_list.append(ticket.logout(request, session))
2015-05-16 21:43:46 +00:00
ticket.delete()
2015-05-23 19:12:42 +00:00
for ticket in ProxyGrantingTicket.objects.filter(user=self, validate=True):
2015-05-18 18:30:00 +00:00
async_list.append(ticket.logout(request, session))
2015-05-16 21:43:46 +00:00
ticket.delete()
2015-05-18 18:30:00 +00:00
for future in async_list:
if future:
try:
future.result()
except Exception as error:
messages.add_message(
request,
messages.WARNING,
_(u'Error during service logout %r') % error
)
2015-05-27 19:56:39 +00:00
def get_ticket(self, ticket_class, service, service_pattern, renew):
"""
Generate a ticket using `ticket_class` for the service
`service` matching `service_pattern` and asking or not for
authentication renewal with `renew
"""
attributs = dict(
(a.name, a.replace if a.replace else a.name) for a in service_pattern.attributs.all()
)
replacements = dict(
(a.name, (a.pattern, a.replace)) for a in service_pattern.replacements.all()
)
2015-05-18 18:30:00 +00:00
service_attributs = {}
2015-05-27 19:56:39 +00:00
for (key, value) in self.attributs.items():
if key in attributs:
if key in replacements:
value = re.sub(replacements[key][0], replacements[key][1], value)
service_attributs[attributs[key]] = value
ticket = ticket_class.objects.create(
user=self,
attributs=service_attributs,
service=service,
renew=renew,
service_pattern=service_pattern
)
2015-05-16 21:43:46 +00:00
ticket.save()
2015-05-18 18:30:00 +00:00
return ticket
def get_service_url(self, service, service_pattern, renew):
2015-05-27 19:56:39 +00:00
"""Return the url to which the user must be redirected to
after a Service Ticket has been generated"""
2015-05-18 18:30:00 +00:00
ticket = self.get_ticket(ServiceTicket, service, service_pattern, renew)
2015-05-16 21:43:46 +00:00
url = utils.update_url(service, {'ticket':ticket.value})
return url
2015-05-17 21:24:41 +00:00
class BadUsername(Exception):
2015-05-27 19:56:39 +00:00
"""Exception raised then an non allowed username
try to get a ticket for a service"""
2015-05-17 21:24:41 +00:00
pass
class BadFilter(Exception):
2015-05-27 19:56:39 +00:00
""""Exception raised then a user try
to get a ticket for a service and do not reach a condition"""
2015-05-17 21:24:41 +00:00
pass
2015-05-27 19:56:39 +00:00
2015-05-17 21:24:41 +00:00
class UserFieldNotDefined(Exception):
2015-05-27 19:56:39 +00:00
"""Exception raised then a user try to get a ticket for a service
using as username an attribut not present on this user"""
2015-05-17 21:24:41 +00:00
pass
class ServicePattern(models.Model):
2015-05-27 19:56:39 +00:00
"""Allowed services pattern agains services are tested to"""
2015-05-17 21:24:41 +00:00
class Meta:
ordering = ("pos", )
pos = models.IntegerField(default=100)
2015-05-27 19:56:39 +00:00
name = models.CharField(
max_length=255,
unique=True,
blank=True,
null=True,
help_text="Un nom pour le service"
)
2015-05-17 21:24:41 +00:00
pattern = models.CharField(max_length=255, unique=True)
2015-05-27 19:56:39 +00:00
user_field = models.CharField(
max_length=255,
default="",
blank=True,
help_text="Nom de l'attribut transmit comme username, vide = login"
)
restrict_users = models.BooleanField(
default=False,
help_text="Limiter les utilisateur autorisé a se connecté a ce service à celle ci-dessous"
)
proxy = models.BooleanField(
default=False,
help_text="Un ProxyGrantingTicket peut être délivré au service pour " \
"s'authentifier en temps que l'utilisateur sur d'autres services"
)
2015-05-27 20:23:16 +00:00
single_sign_out = models.BooleanField(
default=False,
help_text="Activer le SSO sur le service"
)
2015-05-17 21:24:41 +00:00
def __unicode__(self):
return u"%s: %s" % (self.pos, self.pattern)
def check_user(self, user):
2015-05-27 19:56:39 +00:00
"""Check if `user` if allowed to use theses services"""
2015-05-18 18:30:00 +00:00
if self.restrict_users and not self.usernames.filter(value=user.username):
2015-05-17 21:24:41 +00:00
raise BadUsername()
2015-05-27 19:56:39 +00:00
for filtre in self.filters.all():
if isinstance(user.attributs[filtre.attribut], list):
attrs = user.attributs[filtre.attribut]
2015-05-18 18:30:00 +00:00
else:
2015-05-27 19:56:39 +00:00
attrs = [user.attributs[filtre.attribut]]
for value in attrs:
if re.match(filtre.pattern, str(value)):
2015-05-18 18:30:00 +00:00
break
else:
2015-05-27 19:56:39 +00:00
raise BadFilter('%s do not match %s %s' % (
filtre.pattern,
filtre.attribut,
user.attributs[filtre.attribut]
))
2015-05-17 21:24:41 +00:00
if self.user_field and not user.attributs.get(self.user_field):
raise UserFieldNotDefined()
return True
@classmethod
def validate(cls, service):
2015-05-27 19:56:39 +00:00
"""Check if a Service Patern match `service` and
return it, else raise `ServicePattern.DoesNotExist`"""
for service_pattern in cls.objects.all().order_by('pos'):
if re.match(service_pattern.pattern, service):
return service_pattern
2015-05-17 21:24:41 +00:00
raise cls.DoesNotExist()
2015-05-27 19:56:39 +00:00
class Username(models.Model):
"""A list of allowed usernames on a service pattern"""
2015-05-18 18:30:00 +00:00
value = models.CharField(max_length=255)
service_pattern = models.ForeignKey(ServicePattern, related_name="usernames")
2015-05-18 21:38:28 +00:00
2015-05-27 19:56:39 +00:00
def __unicode__(self):
return self.value
2015-05-18 18:30:00 +00:00
class ReplaceAttributName(models.Model):
2015-05-27 19:56:39 +00:00
"""A list of replacement of attributs name for a service pattern"""
2015-05-18 21:38:28 +00:00
class Meta:
2015-05-23 17:32:02 +00:00
unique_together = ('name', 'replace', 'service_pattern')
2015-05-27 19:56:39 +00:00
name = models.CharField(
max_length=255,
help_text=u"nom d'un attributs à transmettre au service"
)
replace = models.CharField(
max_length=255,
blank=True,
help_text=u"nom sous lequel l'attribut sera présenté " \
u"au service. vide = inchangé"
)
2015-05-18 18:30:00 +00:00
service_pattern = models.ForeignKey(ServicePattern, related_name="attributs")
def __unicode__(self):
if not self.replace:
return self.name
else:
return u"%s%s" % (self.name, self.replace)
class FilterAttributValue(models.Model):
2015-05-27 19:56:39 +00:00
"""A list of filter on attributs for a service pattern"""
attribut = models.CharField(
max_length=255,
help_text=u"Nom de l'attribut devant vérifier pattern"
)
pattern = models.CharField(
max_length=255,
help_text=u"Une expression régulière"
)
2015-05-18 18:30:00 +00:00
service_pattern = models.ForeignKey(ServicePattern, related_name="filters")
def __unicode__(self):
return u"%s %s" % (self.attribut, self.pattern)
class ReplaceAttributValue(models.Model):
2015-05-27 19:56:39 +00:00
"""Replacement to apply on attributs values for a service pattern"""
attribut = models.CharField(
max_length=255,
help_text=u"Nom de l'attribut dont la valeur doit être modifié"
)
pattern = models.CharField(
max_length=255,
help_text=u"Une expression régulière de ce qui doit être modifié"
)
replace = models.CharField(
max_length=255,
blank=True,
help_text=u"Par quoi le remplacer, les groupes sont capturé par \\1, \\2 …"
)
2015-05-18 18:30:00 +00:00
service_pattern = models.ForeignKey(ServicePattern, related_name="replacements")
def __unicode__(self):
return u"%s %s %s" % (self.attribut, self.pattern, self.replace)
2015-05-17 21:24:41 +00:00
2015-05-16 21:43:46 +00:00
class Ticket(models.Model):
2015-05-27 19:56:39 +00:00
"""Generic class for a Ticket"""
2015-05-16 21:43:46 +00:00
class Meta:
abstract = True
user = models.ForeignKey(User, related_name="%(class)s")
attributs = PickledObjectField()
validate = models.BooleanField(default=False)
service = models.TextField()
2015-05-27 19:56:39 +00:00
service_pattern = models.ForeignKey(ServicePattern, related_name="%(class)s")
2015-05-16 21:43:46 +00:00
creation = models.DateTimeField(auto_now_add=True)
renew = models.BooleanField(default=False)
def __unicode__(self):
2015-05-27 19:56:39 +00:00
return u"Ticket(%s, %s)" % (self.user, self.service)
2015-05-16 21:43:46 +00:00
2015-05-18 18:30:00 +00:00
def logout(self, request, session):
2015-05-27 19:56:39 +00:00
"""Send a SSO request to the ticket service"""
2015-05-27 20:23:16 +00:00
if self.validate and self.service_pattern.single_sign_out:
2015-05-16 21:43:46 +00:00
xml = """<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>
2015-05-27 19:56:39 +00:00
</samlp:LogoutRequest>""" % \
{
'id' : os.urandom(20).encode("hex"),
'datetime' : int(time.time()),
'ticket': self.value
}
2015-05-16 21:43:46 +00:00
headers = {'Content-Type': 'text/xml'}
try:
2015-05-27 19:56:39 +00:00
return session.post(
self.service.encode('utf-8'),
data=xml.encode('utf-8'),
headers=headers
)
except Exception as error:
messages.add_message(
request,
messages.WARNING,
_(u'Error during service logout %(service)s:\n%(error)s') %
{'service': self.service, 'error':error}
)
2015-05-16 21:43:46 +00:00
class ServiceTicket(Ticket):
2015-05-27 19:56:39 +00:00
"""A Service Ticket"""
2015-05-16 21:43:46 +00:00
value = models.CharField(max_length=255, default=_gen_st, unique=True)
2015-05-27 19:56:39 +00:00
def __unicode__(self):
return u"ServiceTicket(%s, %s, %s)" % (self.user, self.value, self.service)
2015-05-16 21:43:46 +00:00
class ProxyTicket(Ticket):
2015-05-27 19:56:39 +00:00
"""A Proxy Ticket"""
2015-05-16 21:43:46 +00:00
value = models.CharField(max_length=255, default=_gen_pt, unique=True)
2015-05-27 19:56:39 +00:00
def __unicode__(self):
return u"ProxyTicket(%s, %s, %s)" % (self.user, self.value, self.service)
2015-05-16 21:43:46 +00:00
class ProxyGrantingTicket(Ticket):
2015-05-27 19:56:39 +00:00
"""A Proxy Granting Ticket"""
2015-05-16 21:43:46 +00:00
value = models.CharField(max_length=255, default=_gen_pgt, unique=True)
2015-05-27 19:56:39 +00:00
def __unicode__(self):
return u"ProxyGrantingTicket(%s, %s, %s)" % (self.user, self.value, self.service)
2015-05-16 21:43:46 +00:00
class Proxy(models.Model):
2015-05-27 19:56:39 +00:00
"""A list of proxies on `ProxyTicket`"""
2015-05-16 21:43:46 +00:00
class Meta:
ordering = ("-pk", )
url = models.CharField(max_length=255)
proxy_ticket = models.ForeignKey(ProxyTicket, related_name="proxies")
2015-05-27 19:56:39 +00:00
def __unicode__(self):
return self.url