diff --git a/cas_server/__init__.py b/cas_server/__init__.py index 794c759..e69de29 100644 --- a/cas_server/__init__.py +++ b/cas_server/__init__.py @@ -1 +0,0 @@ -import default_settings diff --git a/cas_server/auth.py b/cas_server/auth.py new file mode 100644 index 0000000..4d88b66 --- /dev/null +++ b/cas_server/auth.py @@ -0,0 +1,94 @@ +# ⁻*- coding: utf-8 -*- +from django.conf import settings +from django.contrib.auth.models import User +try: + import MySQLdb + import MySQLdb.cursors + import crypt +except ImportError: + MySQLdb = None +class DummyAuthUser(object): + def __init__(self, username): + self.username = username + + def test_password(self, password): + return False + + def attributs(self): + return {} + + +class TestAuthUser(DummyAuthUser): + def __init__(self, username): + self.username = username + + def test_password(self, password): + return self.username == "test" and password == "test" + + def attributs(self): + return {'nom':'Nymous', 'prenom':'Ano', 'email':'anonymous@example.net'} + + +class MysqlAuthUser(DummyAuthUser): + user = None + def __init__(self, username): + mysql_config = { + "user": settings.CAS_SQL_USERNAME, + "passwd": settings.CAS_SQL_PASSWORD, + "db": settings.CAS_SQL_DBNAME, + "host": settings.CAS_SQL_HOST, + "charset":settings.CAS_SQL_DBCHARSET, + "cursorclass":MySQLdb.cursors.DictCursor + } + if not MySQLdb: + raise RuntimeError("Please install MySQLdb before using the MysqlAuthUser backend") + conn = MySQLdb.connect(**mysql_config) + curs = conn.cursor() + if curs.execute(settings.CAS_SQL_USER_QUERY, (username,)) == 1: + self.user = curs.fetchone() + super(MysqlAuthUser, self).__init__(username) + + def test_password(self, password): + if not self.user: + return False + else: + if settings.CAS_SQL_PASSWORD_CHECK == "plain": + return password == self.user["password"] + elif settings.CAS_SQL_PASSWORD_CHECK == "crypt": + if self.user["password"].startswith('$'): + salt = '$'.join(self.user["password"].split('$', 3)[:-1]) + return crypt.crypt(password, salt) == self.user["password"] + else: + return crypt.crypt(password, self.user["password"][:2]) == self.user["password"] + + def attributs(self): + if not self.user: + return {} + else: + return self.user + + +class DjangoAuthUser(DummyAuthUser): + user = None + def __init__(self, username): + try: + self.user = User.objects.get(username=username) + except User.DoesNotExist: + pass + super(DjangoAuthUser, self).__init__(username) + + + def test_password(self, password): + if not self.user: + return False + else: + return self.user.check_password(password) + + def attributs(self): + if not self.user: + return {} + else: + attr = {} + for field in self.user._meta.fields: + attr[field.attname]=getattr(self.user, field.attname) + return attr diff --git a/cas_server/default_settings.py b/cas_server/default_settings.py index ec17441..7315d37 100644 --- a/cas_server/default_settings.py +++ b/cas_server/default_settings.py @@ -1,24 +1,23 @@ from django.conf import settings +import auth def setting_default(name, default_value): value = getattr(settings, name, default_value) setattr(settings, name, value) -class AuthUser(object): - def __init__(self, username): - self.username = username - - def test_password(self, password): - return self.username == "test" and password == "test" - - def attributs(self): - return {'nom':'Nymous', 'prenom':'Ano', 'email':'anonymous@example.net'} - - setting_default('CAS_LOGIN_TEMPLATE', 'cas_server/login.html') setting_default('CAS_WARN_TEMPLATE', 'cas_server/warn.html') setting_default('CAS_LOGGED_TEMPLATE', 'cas_server/logged.html') -setting_default('CAS_AUTH_CLASS', AuthUser) +setting_default('CAS_AUTH_CLASS', auth.DjangoAuthUser) setting_default('CAS_ST_LEN', 30) setting_default('CAS_TICKET_VALIDITY', 300) setting_default('CAS_PROXY_CA_CERTIFICATE_PATH', True) + +setting_default('CAS_SQL_HOST', 'localhost') +setting_default('CAS_SQL_USERNAME', '') +setting_default('CAS_SQL_PASSWORD', '') +setting_default('CAS_SQL_DBNAME', '') +setting_default('CAS_SQL_DBCHARSET', 'utf8') +setting_default('CAS_SQL_USER_QUERY', 'SELECT user AS usersame, pass AS password, users.* FROM users WHERE user = %s') +setting_default('CAS_SQL_PASSWORD_CHECK', 'crypt') # crypt or plain + diff --git a/cas_server/forms.py b/cas_server/forms.py index 9eecd70..192b26b 100644 --- a/cas_server/forms.py +++ b/cas_server/forms.py @@ -1,3 +1,5 @@ +import default_settings + from django import forms from django.conf import settings diff --git a/cas_server/migrations/0002_auto_20150517_1406.py b/cas_server/migrations/0002_auto_20150517_1406.py new file mode 100644 index 0000000..0876d86 --- /dev/null +++ b/cas_server/migrations/0002_auto_20150517_1406.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('cas_server', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='proxygrantingticket', + name='service_pattern', + field=models.ForeignKey(related_name='proxygrantingticket', default=1, to='cas_server.ServicePattern'), + preserve_default=False, + ), + migrations.AddField( + model_name='proxyticket', + name='service_pattern', + field=models.ForeignKey(related_name='proxyticket', default=1, to='cas_server.ServicePattern'), + preserve_default=False, + ), + migrations.AddField( + model_name='serviceticket', + name='service_pattern', + field=models.ForeignKey(related_name='serviceticket', default=1, to='cas_server.ServicePattern'), + preserve_default=False, + ), + ] diff --git a/cas_server/models.py b/cas_server/models.py index 4e4376f..8eae817 100644 --- a/cas_server/models.py +++ b/cas_server/models.py @@ -1,4 +1,5 @@ # ⁻*- coding: utf-8 -*- +import default_settings from django.conf import settings from django.db import models @@ -50,11 +51,53 @@ class User(models.Model): 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) + 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) ticket.save() url = utils.update_url(service, {'ticket':ticket.value}) return url +class BadUsername(Exception): + pass +class BadFilter(Exception): + pass +class UserFieldNotDefined(Exception): + pass +class ServicePattern(models.Model): + class Meta: + ordering = ("pos", ) + + pos = models.IntegerField(default=100) + 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") + 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") + + 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(','): + raise BadUsername() + if self.filter and self.filter.startswith("lambda") and not eval(str(self.filter))(user.username, user.attributs): + raise BadFilter() + print self.user_field + print user.attributs.get(self.user_field) + if self.user_field and not user.attributs.get(self.user_field): + raise UserFieldNotDefined() + return True + + + @classmethod + def validate(cls, service): + for s in cls.objects.all().order_by('pos'): + if re.match(s.pattern, service): + return s + raise cls.DoesNotExist() + + + class Ticket(models.Model): class Meta: abstract = True @@ -62,6 +105,7 @@ class Ticket(models.Model): attributs = PickledObjectField() validate = models.BooleanField(default=False) service = models.TextField() + service_pattern = models.ForeignKey(ServicePattern, related_name="%(class)s") creation = models.DateTimeField(auto_now_add=True) renew = models.BooleanField(default=False) @@ -96,36 +140,3 @@ class Proxy(models.Model): url = models.CharField(max_length=255) proxy_ticket = models.ForeignKey(ProxyTicket, related_name="proxies") -class BadUsername(Exception): - pass -class BadFilter(Exception): - pass -class ServicePattern(models.Model): - class Meta: - ordering = ("pos", ) - - pos = models.IntegerField(default=100) - 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") - 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") - - 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(','): - raise BadUsername() - if self.filter and self.filter.startswith("lambda") and not eval(str(self.filter))(user.username, user.attributs): - raise BadFilter() - return True - - - @classmethod - def validate(cls, service): - for s in cls.objects.all(): - if re.match(s.pattern, service): - return s - raise cls.DoesNotExist() diff --git a/cas_server/views.py b/cas_server/views.py index 65b397c..43be0db 100644 --- a/cas_server/views.py +++ b/cas_server/views.py @@ -1,4 +1,6 @@ # ⁻*- coding: utf-8 -*- +import default_settings + from django.shortcuts import render, redirect from django.http import HttpResponse, StreamingHttpResponse from django.conf import settings @@ -76,6 +78,8 @@ def login(request): messages.add_message(request, messages.ERROR, u"Nom d'utilisateur non autorisé") except models.BadFilter: messages.add_message(request, messages.ERROR, u"Caractéristique utilisateur non autorisé") + except models.UserFieldNotDefined: + messages.add_message(request, messages.ERROR, u"L'attribut %s est nécessaire pour utiliser ce service" % service_pattern.user_field) # if gateway is set and auth failed redirect to the service without authentication if gateway: @@ -155,11 +159,13 @@ def psValidate(request, typ=['ST']): else: attributes.append((key, value)) params = {'username':ticket.user.username, 'attributes':attributes, 'proxies':proxies} + if ticket.service_pattern.user_field and ticket.user.attributs.get(ticket.service_pattern.user_field): + params['username'] = ticket.user.attributs.get(ticket.service_pattern.user_field) if pgtUrl and pgtUrl.startswith("https://"): - pattern = modele.ServicePattern(pgtUrl) + pattern = models.ServicePattern.validate(pgtUrl) if pattern.proxy: proxyid = models._gen_ticket('PGTIOU') - pticket = models.ProxyGrantingTicket.objects.create(user=ticket.user, service=pgtUrl) + pticket = models.ProxyGrantingTicket.objects.create(user=ticket.user, service=pgtUrl, service_pattern=pattern) url = utils.update_url(pgtUrl, {'pgtIou':proxyid, 'pgtId':pticket.value}) try: r = requests.get(url, verify=settings.CAS_PROXY_CA_CERTIFICATE_PATH) @@ -174,7 +180,7 @@ def psValidate(request, typ=['ST']): return render(request, "cas_server/serviceValidateError.xml", {'code':'INVALID_PROXY_CALLBACK'}, content_type="text/xml; charset=utf-8") else: return render(request, "cas_server/serviceValidate.xml", params, content_type="text/xml; charset=utf-8") - except (models.ServiceTicket.DoesNotExist, models.ProxyTicket.DoesNotExist): + except (models.ServiceTicket.DoesNotExist, models.ProxyTicket.DoesNotExist, models.ServicePattern.DoesNotExist): return render(request, "cas_server/serviceValidateError.xml", {'code':'INVALID_TICKET'}, content_type="text/xml; charset=utf-8") else: return render(request, "cas_server/serviceValidateError.xml", {'code':'INVALID_REQUEST'}, content_type="text/xml; charset=utf-8") @@ -189,11 +195,13 @@ def proxy(request): targetService = request.GET.get('targetService') if pgt and targetService: try: + pattern = models.ServicePattern.validate(targetService) ticket = models.ProxyGrantingTicket.objects.get(value=pgt, creation__gt=(datetime.now() - timedelta(seconds=settings.CAS_TICKET_VALIDITY))) - pticket = models.ProxyTicket.objects.create(user=ticket.user, service=targetService) + pattern.check_user(ticket.user) + pticket = models.ProxyTicket.objects.create(user=ticket.user, service=targetService, service_pattern=ticket.service_pattern) 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: + except (models.ProxyGrantingTicket.DoesNotExist, models.ServicePattern.DoesNotExist, models.BadUsername, models.BadFilter): return render(request, "cas_server/serviceValidateError.xml", {'code':'INVALID_TICKET'}, content_type="text/xml; charset=utf-8") else: return render(request, "cas_server/serviceValidateError.xml", {'code':'INVALID_REQUEST'}, content_type="text/xml; charset=utf-8")