diff --git a/cas_server/admin.py b/cas_server/admin.py index 520efc6..f9605a2 100644 --- a/cas_server/admin.py +++ b/cas_server/admin.py @@ -19,8 +19,22 @@ class ProxyGrantingInline(admin.TabularInline): class UserAdmin(admin.ModelAdmin): inlines = (ServiceTicketInline, ProxyTicketInline, ProxyGrantingInline) +class UsernamesInline(admin.TabularInline): + model = Usernames + extra = 0 +class ReplaceAttributNameInline(admin.TabularInline): + model = ReplaceAttributName + extra = 0 +class ReplaceAttributValueInline(admin.TabularInline): + model = ReplaceAttributValue + extra = 0 +class FilterAttributValueInline(admin.TabularInline): + model = FilterAttributValue + extra = 0 + class ServicePatternAdmin(admin.ModelAdmin): - list_display = ('pos', 'pattern', 'proxy') + inlines = (UsernamesInline, ReplaceAttributNameInline, ReplaceAttributValueInline, FilterAttributValueInline) + list_display = ('pos', 'name', 'pattern', 'proxy') admin.site.register(User, UserAdmin) diff --git a/cas_server/migrations/0003_auto_20150518_1648.py b/cas_server/migrations/0003_auto_20150518_1648.py new file mode 100644 index 0000000..e4f3dbc --- /dev/null +++ b/cas_server/migrations/0003_auto_20150518_1648.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('cas_server', '0002_auto_20150517_1406'), + ] + + operations = [ + migrations.CreateModel( + name='Attribut', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=255)), + ('replace', models.CharField(default=b'', max_length=255, blank=True)), + ('service_pattern', models.ForeignKey(related_name='attributs', to='cas_server.ServicePattern')), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.RemoveField( + model_name='servicepattern', + name='attributs', + ), + ] diff --git a/cas_server/migrations/0004_auto_20150518_1659.py b/cas_server/migrations/0004_auto_20150518_1659.py new file mode 100644 index 0000000..f6b93de --- /dev/null +++ b/cas_server/migrations/0004_auto_20150518_1659.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('cas_server', '0003_auto_20150518_1648'), + ] + + operations = [ + migrations.CreateModel( + name='FilterAttributValue', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('attribut', models.CharField(max_length=255)), + ('pattern', models.CharField(max_length=255)), + ('service_pattern', models.ForeignKey(related_name='filters', to='cas_server.ServicePattern')), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='ReplaceAttributValue', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('attribut', models.CharField(max_length=255)), + ('pattern', models.CharField(max_length=255)), + ('replace', models.CharField(max_length=255)), + ('service_pattern', models.ForeignKey(related_name='replacements', to='cas_server.ServicePattern')), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.RenameModel( + old_name='Attribut', + new_name='ReplaceAttributName', + ), + migrations.RemoveField( + model_name='servicepattern', + name='filter', + ), + ] diff --git a/cas_server/migrations/0005_auto_20150518_1717.py b/cas_server/migrations/0005_auto_20150518_1717.py new file mode 100644 index 0000000..df7ae40 --- /dev/null +++ b/cas_server/migrations/0005_auto_20150518_1717.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('cas_server', '0004_auto_20150518_1659'), + ] + + operations = [ + migrations.AlterField( + model_name='filterattributvalue', + name='attribut', + field=models.CharField(help_text="Nom de l'attribut devant v\xe9rifier pattern", max_length=255), + preserve_default=True, + ), + migrations.AlterField( + model_name='filterattributvalue', + name='pattern', + field=models.CharField(help_text='Une expression r\xe9guli\xe8re', max_length=255), + preserve_default=True, + ), + migrations.AlterField( + model_name='replaceattributname', + name='name', + field=models.CharField(help_text="nom d'un attributs \xe0 transmettre au service", max_length=255), + preserve_default=True, + ), + migrations.AlterField( + model_name='replaceattributname', + name='replace', + field=models.CharField(help_text="nom sous lequel l'attribut sera pr\xe9sent\xe9 au service. vide = inchang\xe9", max_length=255, blank=True), + preserve_default=True, + ), + migrations.AlterField( + model_name='replaceattributvalue', + name='attribut', + field=models.CharField(help_text="Nom de l'attribut dont la valeur doit \xeatre modifi\xe9", max_length=255), + preserve_default=True, + ), + migrations.AlterField( + model_name='replaceattributvalue', + name='pattern', + field=models.CharField(help_text='Une expression r\xe9guli\xe8re de ce qui doit \xeatre modifi\xe9', max_length=255), + preserve_default=True, + ), + migrations.AlterField( + model_name='replaceattributvalue', + name='replace', + field=models.CharField(help_text='Par quoi le remplacer, les groupes sont captur\xe9 par \\1, \\2 \u2026', max_length=255, blank=True), + preserve_default=True, + ), + ] diff --git a/cas_server/migrations/0006_auto_20150518_1720.py b/cas_server/migrations/0006_auto_20150518_1720.py new file mode 100644 index 0000000..15b503d --- /dev/null +++ b/cas_server/migrations/0006_auto_20150518_1720.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('cas_server', '0005_auto_20150518_1717'), + ] + + operations = [ + migrations.AlterField( + model_name='replaceattributname', + name='name', + field=models.CharField(help_text="nom d'un attributs \xe0 transmettre au service", unique=True, max_length=255), + preserve_default=True, + ), + ] diff --git a/cas_server/migrations/0007_auto_20150518_1727.py b/cas_server/migrations/0007_auto_20150518_1727.py new file mode 100644 index 0000000..4ee050f --- /dev/null +++ b/cas_server/migrations/0007_auto_20150518_1727.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('cas_server', '0006_auto_20150518_1720'), + ] + + operations = [ + migrations.CreateModel( + name='Usernames', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('value', models.CharField(max_length=255)), + ('service_pattern', models.ForeignKey(related_name='usernames', to='cas_server.ServicePattern')), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.RemoveField( + model_name='servicepattern', + name='usernames', + ), + migrations.AddField( + model_name='servicepattern', + name='restrict_users', + field=models.BooleanField(default=False, help_text=b'Limiter les utilisateur autoris\xc3\xa9 a se connect\xc3\xa9 a ce service \xc3\xa0 celle ci-dessous'), + preserve_default=True, + ), + ] diff --git a/cas_server/migrations/0008_servicepattern_name.py b/cas_server/migrations/0008_servicepattern_name.py new file mode 100644 index 0000000..9a30141 --- /dev/null +++ b/cas_server/migrations/0008_servicepattern_name.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('cas_server', '0007_auto_20150518_1727'), + ] + + operations = [ + migrations.AddField( + model_name='servicepattern', + name='name', + field=models.CharField(max_length=255, unique=True, null=True, blank=True), + preserve_default=True, + ), + ] diff --git a/cas_server/migrations/0009_auto_20150518_1740.py b/cas_server/migrations/0009_auto_20150518_1740.py new file mode 100644 index 0000000..e119c3d --- /dev/null +++ b/cas_server/migrations/0009_auto_20150518_1740.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('cas_server', '0008_servicepattern_name'), + ] + + operations = [ + migrations.AlterField( + model_name='servicepattern', + name='name', + field=models.CharField(help_text=b'Un nom pour le service', max_length=255, unique=True, null=True, blank=True), + preserve_default=True, + ), + ] diff --git a/cas_server/models.py b/cas_server/models.py index 12406a4..ace74fb 100644 --- a/cas_server/models.py +++ b/cas_server/models.py @@ -11,7 +11,9 @@ import os import time import random import string -import requests + +from concurrent.futures import ThreadPoolExecutor +from requests_futures.sessions import FuturesSession import utils def _gen_ticket(prefix): @@ -36,23 +38,42 @@ class User(models.Model): return self.username def logout(self, request): + async_list = [] + session = FuturesSession(executor=ThreadPoolExecutor(max_workers=10)) for ticket in ServiceTicket.objects.filter(user=self): - ticket.logout(request) + async_list.append(ticket.logout(request, session)) ticket.delete() for ticket in ProxyTicket.objects.filter(user=self): - ticket.logout(request) + async_list.append(ticket.logout(request, session)) ticket.delete() for ticket in ProxyGrantingTicket.objects.filter(user=self): - ticket.logout(request) + async_list.append(ticket.logout(request, session)) ticket.delete() + for future in async_list: + try: + future.result() + except Exception as e: + messages.add_message(request, messages.WARNING, u'Erreur lors de la déconnexion des services %s' % e) def delete(self): super(User, self).delete() - def get_service_url(self, service, service_pattern, renew): - attributs = [s.strip() for s in service_pattern.attributs.split(',')] - ticket = ServiceTicket.objects.create(user=self, attributs = dict([(k, v) for (k, v) in self.attributs.items() if k in attributs]), service=service, renew=renew, service_pattern=service_pattern) + + def get_ticket(self, TicketClass, service, service_pattern, 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()) + service_attributs = {} + for (k,v) in self.attributs.items(): + if k in attributs: + if k in replacements: + v = re.sub(replacements[k][0], replacements[k][1], v) + service_attributs[attributs[k]] = v + ticket = TicketClass.objects.create(user=self, attributs = service_attributs, service=service, renew=renew, service_pattern=service_pattern) ticket.save() + return ticket + + def get_service_url(self, service, service_pattern, renew): + ticket = self.get_ticket(ServiceTicket, service, service_pattern, renew) url = utils.update_url(service, {'ticket':ticket.value}) return url @@ -67,21 +88,31 @@ class ServicePattern(models.Model): ordering = ("pos", ) pos = models.IntegerField(default=100) + name = models.CharField(max_length=255, unique=True, blank=True, null=True, help_text="Un nom pour le service") pattern = models.CharField(max_length=255, unique=True) user_field = models.CharField(max_length=255, default="", blank=True, help_text="Nom de l'attribut transmit comme username, vide = login") - usernames = models.CharField(max_length=255, default="", blank=True, help_text="Liste d'utilisateurs acceptés séparé par des virgules, vide = tous les utilisateur") - attributs = models.CharField(max_length=255, default="", blank=True, help_text="Liste des nom d'attributs à transmettre au service, séparé par une virgule. vide = aucun") + #usernames = models.CharField(max_length=255, default="", blank=True, help_text="Liste d'utilisateurs acceptés séparé par des virgules, vide = tous les utilisateur") + #attributs = models.CharField(max_length=255, default="", blank=True, help_text="Liste des nom d'attributs à transmettre au service, séparé par une virgule. vide = aucun") + 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") - filter = models.CharField(max_length=255, default="", blank=True, help_text="Une lambda fonction pour filtrer sur les utilisateur où leurs attribut, arg1: username, arg2:attrs_dict. vide = pas de filtre") + #filter = models.CharField(max_length=255, default="", blank=True, help_text="Une lambda fonction pour filtrer sur les utilisateur où leurs attribut, arg1: username, arg2:attrs_dict. vide = pas de filtre") def __unicode__(self): return u"%s: %s" % (self.pos, self.pattern) def check_user(self, user): - if self.usernames and not user.username in self.usernames.split(','): + if self.restrict_users and not self.usernames.filter(value=user.username): raise BadUsername() - if self.filter and self.filter.startswith("lambda") and not eval(str(self.filter))(user.username, user.attributs): - raise BadFilter() + for f in self.filters.all(): + if isinstance(user.attributs[f.attribut], list): + l = user.attributs[f.attribut] + else: + l = [user.attributs[f.attribut]] + for v in l: + if re.match(f.pattern, str(v)): + break + else: + raise BadFilter('%s do not match %s %s' % (f.pattern, f.attribut, user.attributs[f.attribut]) ) if self.user_field and not user.attributs.get(self.user_field): raise UserFieldNotDefined() return True @@ -94,6 +125,36 @@ class ServicePattern(models.Model): return s raise cls.DoesNotExist() +class Usernames(models.Model): + value = models.CharField(max_length=255) + service_pattern = models.ForeignKey(ServicePattern, related_name="usernames") +class ReplaceAttributName(models.Model): + name = models.CharField(max_length=255, unique=True, 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é au service. vide = inchangé") + 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): + 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") + service_pattern = models.ForeignKey(ServicePattern, related_name="filters") + + def __unicode__(self): + return u"%s %s" % (self.attribut, self.pattern) + +class ReplaceAttributValue(models.Model): + 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 …") + service_pattern = models.ForeignKey(ServicePattern, related_name="replacements") + + def __unicode__(self): + return u"%s %s %s" % (self.attribut, self.pattern, self.replace) class Ticket(models.Model): @@ -110,7 +171,7 @@ class Ticket(models.Model): def __unicode__(self): return u"%s: %s %s" % (self.user, self.value, self.service) - def logout(self, request): + def logout(self, request, session): #if self.validate: xml = """ @@ -119,7 +180,7 @@ class Ticket(models.Model): """ % {'id' : os.urandom(20).encode("hex"), 'datetime' : int(time.time()), 'ticket': self.value} headers = {'Content-Type': 'text/xml'} try: - requests.post(self.service.encode('utf-8'), data=xml.encode('utf-8'), headers=headers) + return session.post(self.service.encode('utf-8'), data=xml.encode('utf-8'), headers=headers) except Exception as e: messages.add_message(request, messages.WARNING, u'Erreur lors de la déconnexion du service %s:\n%s' % (self.service, e)) diff --git a/cas_server/templates/cas_server/logged.html b/cas_server/templates/cas_server/logged.html index 0b12d6f..6d26c7f 100644 --- a/cas_server/templates/cas_server/logged.html +++ b/cas_server/templates/cas_server/logged.html @@ -11,7 +11,7 @@
{% bootstrap_messages %} -{% bootstrap_button 'Arrêter' size='lg' button_class="btn-danger btn-block" href="logout" %} +{% bootstrap_button 'Deconnexion' size='lg' button_class="btn-danger btn-block" href="logout" %}
diff --git a/cas_server/templates/cas_server/warn.html b/cas_server/templates/cas_server/warn.html index a238478..20a6f32 100644 --- a/cas_server/templates/cas_server/warn.html +++ b/cas_server/templates/cas_server/warn.html @@ -10,7 +10,7 @@
{% bootstrap_messages %} - + {% bootstrap_button 'Se connecter au service' size='lg' button_class="btn-primary btn-block" href=service_ticket_url %}
diff --git a/cas_server/views.py b/cas_server/views.py index 43be0db..3e66daa 100644 --- a/cas_server/views.py +++ b/cas_server/views.py @@ -69,7 +69,7 @@ def login(request): service_pattern.check_user(user) # if the user has asked to be warned before any login to a service (no transparent SSO) if request.session["warn"] and not warned: - return render(request, settings.CAS_WARN_TEMPLATE, {'service_ticket_url':user.get_service_url(service, service_pattern, renew=renew),'service':service}) + return render(request, settings.CAS_WARN_TEMPLATE, {'service_ticket_url':user.get_service_url(service, service_pattern, renew=renew),'service':service, 'name': service_pattern.name}) else: return redirect(user.get_service_url(service, service_pattern, renew=renew)) # redirect, using method ? except models.ServicePattern.DoesNotExist: @@ -89,13 +89,17 @@ def login(request): return render(request, settings.CAS_LOGGED_TEMPLATE, {}) else: if service: - if gateway: - list(messages.get_messages(request)) # clean messages before leaving the django app - return redirect(service) - if request.session.get("authenticated") and renew: - messages.add_message(request, messages.WARNING, u"Demande de réautentification par le service %s." % service) - else: - messages.add_message(request, messages.WARNING, u"Demande d'autentification par le service %s." % service) + try: + service_pattern = models.ServicePattern.validate(service) + if gateway: + list(messages.get_messages(request)) # clean messages before leaving the django app + return redirect(service) + if request.session.get("authenticated") and renew: + messages.add_message(request, messages.WARNING, u"Demande de réautentification par le service %s (%s)." % (service_pattern.name, service)) + else: + messages.add_message(request, messages.WARNING, u"Demande d'autentification par le service %s (%s)." % (service_pattern.name, service)) + except models.ServicePattern.DoesNotExist: + messages.add_message(request, messages.ERROR, u'Service %s non autorisé.' % service) return render(request, settings.CAS_LOGIN_TEMPLATE, {'form':form}) def logout(request): @@ -198,7 +202,7 @@ def proxy(request): pattern = models.ServicePattern.validate(targetService) ticket = models.ProxyGrantingTicket.objects.get(value=pgt, creation__gt=(datetime.now() - timedelta(seconds=settings.CAS_TICKET_VALIDITY))) pattern.check_user(ticket.user) - pticket = models.ProxyTicket.objects.create(user=ticket.user, service=targetService, service_pattern=ticket.service_pattern) + pticket = ticket.user.get_ticket(models.ProxyTicket, targetService, pattern, False) pticket.proxies.create(url=ticket.service) return render(request, "cas_server/proxy.xml", {'ticket':pticket.value}, content_type="text/xml; charset=utf-8") except (models.ProxyGrantingTicket.DoesNotExist, models.ServicePattern.DoesNotExist, models.BadUsername, models.BadFilter):