Commit 37bb7662 authored by Valentin Samir's avatar Valentin Samir Committed by GitHub

Merge pull request #6 from nitmir/federate

Add a federated mode.

When the settings CAS_FEDERATE is True, django-cas-server will offer to the user to choose its CAS backend to authenticate. Hence the login page do not display anymore a username/password form but a select form with configured CASs backend.

This allow to give access to CAS supported applications to users from multiple organization seamlessly.

It was originally developped to mach the need of https://myares.fr (Federated CAS at https://cas.myares.fr, example of an application using it as https://chat.myares.fr)
parents 097edfd9 6b3b280d
......@@ -5,12 +5,14 @@ omit =
cas_server/migrations*
cas_server/management/*
cas_server/tests/*
cas_server/cas.py
[report]
exclude_lines =
pragma: no cover
def __repr__
def __unicode__
def __str__
raise AssertionError
raise NotImplementedError
if six.PY3:
......@@ -15,3 +15,5 @@ coverage.xml
test_venv
.coverage
htmlcov/
tox_logs/
.cache/
......@@ -40,6 +40,7 @@ Features
* Fine control on which user's attributes are passed to which service
* Possibility to rename/rewrite attributes per service
* Possibility to require some attribute values per service
* Federated mode between multiple CAS
* Supports Django 1.7, 1.8 and 1.9
* Supports Python 2.7, 3.x
......@@ -158,6 +159,17 @@ Authentication settings
If more requests need to be send, there are queued. The default is ``10``.
* ``CAS_SLO_TIMEOUT``: Timeout for a single SLO request in seconds. The default is ``5``.
Federation settings
-------------------
* ``CAS_FEDERATE``: A boolean for activating the federated mode (see the federate section below).
The default is ``False``.
* ``CAS_FEDERATE_REMEMBER_TIMEOUT``: Time after witch the cookie use for "remember my identity
provider" expire. The default is ``604800``, one week. The cookie is called
``_remember_provider``.
Tickets validity settings
-------------------------
......@@ -245,6 +257,8 @@ Authentication backend
This is the default backend. The returned attributes are the fields available on the user model.
* mysql backend ``cas_server.auth.MysqlAuthUser``: see the 'Mysql backend settings' section.
The returned attributes are those return by sql query ``CAS_SQL_USER_QUERY``.
* federated backend ``cas_server.auth.CASFederateAuth``: It is automatically used then ``CAS_FEDERATE`` is ``True``.
You should not set it manually without setting ``CAS_FEDERATE`` to ``True``.
Logs
====
......@@ -313,3 +327,51 @@ Or to log to a file:
},
},
}
Federation mode
===============
``django-cas-server`` comes with a federation mode. Then ``CAS_FEDERATE`` is ``True``,
user are invited to choose an identity provider on the login page, then, they are redirected
to the provider CAS to authenticate. This provider transmit to ``django-cas-server`` the user
username and attributes. The user is now logged in on ``django-cas-server`` and can use
services using ``django-cas-server`` as CAS.
The list of allowed identity providers is defined using the django admin application.
With the development server started, visit http://127.0.0.1:8000/admin/ to add identity providers.
An identity provider comes with 5 fields:
* `Position`: an integer used to tweak the order in which identity providers are displayed on
the login page. Identity providers are sorted using position first, then, on equal position,
using `verbose name` and then, on equal `verbose name`, using `suffix`.
* `Suffix`: the suffix that will be append to the username returned by the identity provider.
It must be unique.
* `Server url`: the url to the identity provider CAS. For instance, if you are using
`https://cas.example.org/login` to authenticate on the CAS, the `server url` is
`https://cas.example.org`
* `CAS protocol version`: the version of the CAS protocol to use to contact the identity provider.
The default is version 3.
* `Verbose name`: the name used on the login page to display the identity provider.
* `Display`: a boolean controlling the display of the identity provider on the login page.
Beware that this do not disable the identity provider, it just hide it on the login page.
User will always be able to log in using this provider by fetching `/federate/provider_suffix`.
In federation mode, ``django-cas-server`` build user's username as follow:
``provider_returned_username@provider_suffix``.
Choose the provider returned username for ``django-cas-server`` and the provider suffix
in order to make sense, as this built username is likely to be displayed to end users in
applications.
Then using federate mode, you should add one command to a daily crontab: ``cas_clean_federate``.
This command clean the local cache of federated user from old unused users.
You could for example do as bellow :
.. code-block::
10 0 * * * cas-user /path/to/project/manage.py cas_clean_federate
......@@ -12,6 +12,7 @@
from django.contrib import admin
from .models import ServiceTicket, ProxyTicket, ProxyGrantingTicket, User, ServicePattern
from .models import Username, ReplaceAttributName, ReplaceAttributValue, FilterAttributValue
from .models import FederatedIendityProvider
from .forms import TicketForm
TICKETS_READONLY_FIELDS = ('validate', 'service', 'service_pattern',
......@@ -91,5 +92,12 @@ class ServicePatternAdmin(admin.ModelAdmin):
'single_log_out', 'proxy_callback', 'restrict_users')
class FederatedIendityProviderAdmin(admin.ModelAdmin):
"""`FederatedIendityProvider` in admin interface"""
fields = ('pos', 'suffix', 'server_url', 'cas_protocol_version', 'verbose_name', 'display')
list_display = ('verbose_name', 'suffix', 'display')
admin.site.register(User, UserAdmin)
admin.site.register(ServicePattern, ServicePatternAdmin)
admin.site.register(FederatedIendityProvider, FederatedIendityProviderAdmin)
# *- coding: utf-8 -*-
# -*- 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
......@@ -12,6 +12,9 @@
"""Some authentication classes for the CAS"""
from django.conf import settings
from django.contrib.auth import get_user_model
from django.utils import timezone
from datetime import timedelta
try: # pragma: no cover
import MySQLdb
import MySQLdb.cursors
......@@ -19,6 +22,8 @@ try: # pragma: no cover
except ImportError:
MySQLdb = None
from .models import FederatedUser
class AuthUser(object):
"""Authentication base class"""
......@@ -136,3 +141,35 @@ class DjangoAuthUser(AuthUser): # pragma: no cover
return attr
else:
return {}
class CASFederateAuth(AuthUser):
"""Authentication class used then CAS_FEDERATE is True"""
user = None
def __init__(self, username):
try:
self.user = FederatedUser.get_from_federated_username(username)
super(CASFederateAuth, self).__init__(
self.user.federated_username
)
except FederatedUser.DoesNotExist:
super(CASFederateAuth, self).__init__(username)
def test_password(self, ticket):
"""test `password` agains the user"""
if not self.user or not self.user.ticket:
return False
else:
return (
ticket == self.user.ticket and
self.user.last_update >
(timezone.now() - timedelta(seconds=settings.CAS_TICKET_VALIDITY))
)
def attributs(self):
"""return a dict of user attributes"""
if not self.user: # pragma: no cover (should not happen)
return {}
else:
return self.user.attributs
This diff is collapsed.
# -*- 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
......@@ -7,7 +8,7 @@
# 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
# (c) 2015-2016 Valentin Samir
"""Default values for the app's settings"""
from django.conf import settings
from django.contrib.staticfiles.templatetags.staticfiles import static
......@@ -87,3 +88,9 @@ setting_default(
)
setting_default('CAS_ENABLE_AJAX_AUTH', False)
setting_default('CAS_FEDERATE', False)
setting_default('CAS_FEDERATE_REMEMBER_TIMEOUT', 604800) # one week
if settings.CAS_FEDERATE:
settings.CAS_AUTH_CLASS = "cas_server.auth.CASFederateAuth"
# -*- 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
"""federated mode helper classes"""
from .default_settings import settings
from django.db import IntegrityError
from .cas import CASClient
from .models import FederatedUser, FederateSLO, User
import logging
from importlib import import_module
from six.moves import urllib
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
logger = logging.getLogger(__name__)
class CASFederateValidateUser(object):
"""Class CAS client used to authenticate the user again a CAS provider"""
username = None
attributs = {}
client = None
def __init__(self, provider, service_url):
self.provider = provider
self.client = CASClient(
service_url=service_url,
version=provider.cas_protocol_version,
server_url=provider.server_url,
renew=False,
)
def get_login_url(self):
"""return the CAS provider login url"""
return self.client.get_login_url()
def get_logout_url(self, redirect_url=None):
"""return the CAS provider logout url"""
return self.client.get_logout_url(redirect_url)
def verify_ticket(self, ticket):
"""test `ticket` agains the CAS provider, if valid, create the local federated user"""
try:
username, attributs = self.client.verify_ticket(ticket)[:2]
except urllib.error.URLError:
return False
if username is not None:
if attributs is None:
attributs = {}
attributs["provider"] = self.provider
self.username = username
self.attributs = attributs
user = FederatedUser.objects.update_or_create(
username=username,
provider=self.provider,
defaults=dict(attributs=attributs, ticket=ticket)
)[0]
user.save()
self.federated_username = user.federated_username
return True
else:
return False
@staticmethod
def register_slo(username, session_key, ticket):
"""association a ticket with a (username, session) for processing later SLO request"""
try:
FederateSLO.objects.create(
username=username,
session_key=session_key,
ticket=ticket
)
except IntegrityError: # pragma: no cover (ignore if the FederateSLO already exists)
pass
def clean_sessions(self, logout_request):
"""process a SLO request"""
try:
slos = self.client.get_saml_slos(logout_request) or []
except NameError: # pragma: no cover (should not happen)
slos = []
for slo in slos:
for federate_slo in FederateSLO.objects.filter(ticket=slo.text):
logger.info(
"Got an SLO requests for ticket %s, logging out user %s" % (
federate_slo.username,
federate_slo.ticket
)
)
session = SessionStore(session_key=federate_slo.session_key)
session.flush()
try:
user = User.objects.get(
username=federate_slo.username,
session_key=federate_slo.session_key
)
user.logout()
user.delete()
except User.DoesNotExist: # pragma: no cover (should not happen)
pass
federate_slo.delete()
......@@ -28,6 +28,27 @@ class WarnForm(forms.Form):
lt = forms.CharField(widget=forms.HiddenInput(), required=False)
class FederateSelect(forms.Form):
"""
Form used on the login page when CAS_FEDERATE is True
allowing the user to choose a identity provider.
"""
provider = forms.ModelChoiceField(
queryset=models.FederatedIendityProvider.objects.filter(display=True).order_by(
"pos",
"verbose_name",
"suffix"
),
to_field_name="suffix",
label=_('Identity provider'),
)
service = forms.CharField(label=_('service'), widget=forms.HiddenInput(), required=False)
method = forms.CharField(widget=forms.HiddenInput(), required=False)
remember = forms.BooleanField(label=_('Remember the identity provider'), required=False)
warn = forms.BooleanField(label=_('warn'), required=False)
renew = forms.BooleanField(widget=forms.HiddenInput(), required=False)
class UserCredential(forms.Form):
"""Form used on the login page to retrive user credentials"""
username = forms.CharField(label=_('login'))
......@@ -51,6 +72,32 @@ class UserCredential(forms.Form):
return cleaned_data
class FederateUserCredential(UserCredential):
"""Form used on the login page to retrive user credentials"""
username = forms.CharField(widget=forms.HiddenInput())
service = forms.CharField(widget=forms.HiddenInput(), required=False)
password = forms.CharField(widget=forms.HiddenInput())
ticket = forms.CharField(widget=forms.HiddenInput())
lt = forms.CharField(widget=forms.HiddenInput(), required=False)
method = forms.CharField(widget=forms.HiddenInput(), required=False)
warn = forms.BooleanField(widget=forms.HiddenInput(), required=False)
renew = forms.BooleanField(widget=forms.HiddenInput(), required=False)
def clean(self):
cleaned_data = super(FederateUserCredential, self).clean()
try:
user = models.FederatedUser.get_from_federated_username(cleaned_data["username"])
user.ticket = ""
user.save()
# should not happed as if the FederatedUser do not exists, super should
# raise before a ValidationError("bad user")
except models.FederatedUser.DoesNotExist: # pragma: no cover (should not happend)
raise forms.ValidationError(
_(u"User not found in the temporary database, please try to reconnect")
)
return cleaned_data
class TicketForm(forms.ModelForm):
"""Form for Tickets in the admin interface"""
class Meta:
......
This diff is collapsed.
This diff is collapsed.
# -*- 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
from django.core.management.base import BaseCommand
from django.utils.translation import ugettext_lazy as _
from ... import models
class Command(BaseCommand):
args = ''
help = _(u"Clean old federated users")
def handle(self, *args, **options):
models.FederatedUser.clean_old_entries()
models.FederateSLO.clean_deleted_sessions()
# -*- 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
"""Clean deleted sessions management command"""
from django.core.management.base import BaseCommand
from django.utils.translation import ugettext_lazy as _
......
# -*- 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
"""Clean old trickets management command"""
from django.core.management.base import BaseCommand
from django.utils.translation import ugettext_lazy as _
......
# -*- coding: utf-8 -*-
# Generated by Django 1.9.6 on 2016-06-16 10:18
from __future__ import unicode_literals
from django.db import migrations, models
import picklefield.fields
class Migration(migrations.Migration):
dependencies = [
('cas_server', '0004_auto_20151218_1032'),
]
operations = [
migrations.CreateModel(
name='FederatedUser',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('username', models.CharField(max_length=124)),
('provider', models.CharField(max_length=124)),
('attributs', picklefield.fields.PickledObjectField(editable=False)),
('ticket', models.CharField(max_length=255)),
('last_update', models.DateTimeField(auto_now=True)),
],
),
migrations.AlterUniqueTogether(
name='federateduser',
unique_together=set([('username', 'provider')]),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-06-23 15:16
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cas_server', '0005_auto_20160616_1018'),
]
operations = [
migrations.CreateModel(
name='FederateSLO',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('username', models.CharField(max_length=30)),
('session_key', models.CharField(blank=True, max_length=40, null=True)),
('ticket', models.CharField(max_length=255)),
],
),
migrations.AlterUniqueTogether(
name='federateslo',
unique_together=set([('username', 'session_key')]),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-04 15:10
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cas_server', '0006_auto_20160623_1516'),
]
operations = [
migrations.CreateModel(
name='FederatedIendityProvider',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('suffix', models.CharField(help_text='Suffix append to backend CAS returner username: `returned_username`@`suffix`', max_length=30, unique=True, verbose_name='suffix')),
('server_url', models.CharField(max_length=255, verbose_name='server url')),
('cas_protocol_version', models.CharField(choices=[(b'1', b'CAS 1.0'), (b'2', b'CAS 2.0'), (b'3', b'CAS 3.0'), (b'CAS_2_SAML_1_0', b'SAML 1.1')], default=b'3', help_text='Version of the CAS protocol to use when sending requests the the backend CAS', max_length=30, verbose_name='CAS protocol version')),
('verbose_name', models.CharField(help_text='Name for this identity provider displayed on the login page', max_length=255, verbose_name='verbose name')),
('pos', models.IntegerField(default=100, help_text='Identity provider are sorted using the (position, verbose name, suffix) attributes', verbose_name='position')),
],
options={
'verbose_name': 'identity provider',
'verbose_name_plural': 'identity providers',
},
),
migrations.AlterField(
model_name='federateduser',
name='provider',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cas_server.FederatedIendityProvider'),
),
migrations.AlterField(
model_name='federateslo',
name='ticket',
field=models.CharField(db_index=True, max_length=255),
),
migrations.AlterField(
model_name='servicepattern',
name='pos',
field=models.IntegerField(default=100, help_text='service patterns are sorted using the position attribute', verbose_name='position'),
),
migrations.AlterUniqueTogether(
name='federateslo',
unique_together=set([('username', 'session_key', 'ticket')]),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-04 15:33
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cas_server', '0007_auto_20160704_1510'),
]
operations = [
migrations.AddField(
model_name='federatediendityprovider',
name='display',
field=models.BooleanField(default=True, help_text='Display the provider on the login page', verbose_name='display'),
),
]
This diff is collapsed.
......@@ -12,6 +12,7 @@
{% block bootstrap3_content %}
<div class="container">
{% if auto_submit %}<noscript>{% endif %}
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<h1 id="app-name">
......@@ -19,10 +20,13 @@
{% trans "Central Authentication Service" %}</h1>
</div>
</div>
{% if auto_submit %}</noscript>{% endif %}
<div class="row">
<div class="col-lg-3 col-md-3 col-sm-2 col-xs-12"></div>
<div class="col-lg-6 col-md-6 col-sm-8 col-xs-12">
{% if auto_submit %}<noscript>{% endif %}
{% bootstrap_messages %}
{% if auto_submit %}</noscript>{% endif %}
{% block content %}
{% endblock %}
</div>
......
......@@ -3,11 +3,20 @@
{% load staticfiles %}
{% load i18n %}
{% block content %}
<form class="form-signin" method="post">
<form class="form-signin" method="post" id="login_form"{% if post_url %} action="{{post_url}}"{% endif %}>
{% if auto_submit %}<noscript>{% endif %}
<h2 class="form-signin-heading">{% trans "Please log in" %}</h2>
{% if auto_submit %}</noscript>{% endif %}
{% csrf_token %}
{% bootstrap_form form %}
{% if auto_submit %}<noscript>{% endif %}
{% bootstrap_button _('Login') size='lg' button_type="submit" button_class="btn-primary btn-block"%}
{% if auto_submit %}</noscript>{% endif %}
</form>
{% if auto_submit %}
<script type="text/javascript">
document.getElementById('login_form').submit(); // SUBMIT FORM
</script>
{% endif %}
{% endblock %}
# *- coding: utf-8 -*-
# -*- 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
......@@ -23,27 +23,28 @@ from cas_server.tests.utils import get_auth_client
class BaseServicePattern(object):
"""Mixing for setting up service pattern for testing"""
def setup_service_patterns(self, proxy=False):
@classmethod
def setup_service_patterns(cls, proxy=False):
"""setting up service pattern"""
# For general purpose testing
self.service = "https://www.example.com"
self.service_pattern = models.ServicePattern.objects.create(
cls.service = "https://www.example.com"
cls.service_pattern = models.ServicePattern.objects.create(
name="example",
pattern="^https://www\.example\.com(/.*)?$",
proxy=proxy,
)
models.ReplaceAttributName.objects.create(name="*", service_pattern=self.service_pattern)
models.ReplaceAttributName.objects.create(name="*", service_pattern=cls.service_pattern)
# For testing the restrict_users attributes
self.service_restrict_user_fail = "https://restrict_user_fail.example.com"
self.service_pattern_restrict_user_fail = models.ServicePattern.objects.create(
cls.service_restrict_user_fail = "https://restrict_user_fail.example.com"
cls.service_pattern_restrict_user_fail = models.ServicePattern.objects.create(
name="restrict_user_fail",
pattern="^https://restrict_user_fail\.example\.com(/.*)?$",
restrict_users=True,
proxy=proxy,
)
self.service_restrict_user_success = "https://restrict_user_success.example.com"
self.service_pattern_restrict_user_success = models.ServicePattern.objects.create(
cls.service_restrict_user_success = "https://restrict_user_success.example.com"
cls.service_pattern_restrict_user_success = models.ServicePattern.objects.create(
name="restrict_user_success",
pattern="^https://restrict_user_success\.example\.com(/.*)?$",
restrict_users=True,
......@@ -51,12 +52,12 @@ class BaseServicePattern(object):
)
models.Username.objects.create(
value=settings.CAS_TEST_USER,
service_pattern=self.service_pattern_restrict_user_success
service_pattern=cls.service_pattern_restrict_user_success
)
# For testing the user attributes filtering conditions
self.service_filter_fail = "https://filter_fail.example.com"
self.service_pattern_filter_fail = models.ServicePattern.objects.create(
cls.service_filter_fail = "https://filter_fail.example.com"
cls.service_pattern_filter_fail = models.ServicePattern.objects.create(
name="filter_fail",
pattern="^https://filter_fail\.example\.com(/.*)?$",
proxy=proxy,
......@@ -64,10 +65,10 @@ class BaseServicePattern(object):
models.FilterAttributValue.objects.create(
attribut="right",
pattern="^admin$",
service_pattern=self.service_pattern_filter_fail
service_pattern=cls.service_pattern_filter_fail
)
self.service_filter_fail_alt = "https://filter_fail_alt.example.com"
self.service_pattern_filter_fail_alt = models.ServicePattern.objects.create(
cls.service_filter_fail_alt = "https://filter_fail_alt.example.com"
cls.service_pattern_filter_fail_alt = models.ServicePattern.objects.create(
name="filter_fail_alt",
pattern="^https://filter_fail_alt\.example\.com(/.*)?$",
proxy=proxy,
......@@ -75,10 +76,10 @@ class BaseServicePattern(object):
models.FilterAttributValue.objects.create(
attribut="nom",
pattern="^toto$",
service_pattern=self.service_pattern_filter_fail_alt
service_pattern=cls.service_pattern_filter_fail_alt
)
self.service_filter_success = "https://filter_success.example.com"
self.service_pattern_filter_success = models.ServicePattern.objects.create(
cls.service_filter_success = "https://filter_success.example.com"
cls.service_pattern_filter_success = models.ServicePattern.objects.create(
name="filter_success",
pattern="^https://filter_success\.example\.com(/.*)?$",
proxy=proxy,
......@@ -86,26 +87,26 @@ class BaseServicePattern(object):
models.FilterAttributValue.objects.create(
attribut="email",
pattern="^%s$" % re.escape(settings.CAS_TEST_ATTRIBUTES['email']),
service_pattern=self.service_pattern_filter_success
service_pattern=cls.service_pattern_filter_success
)
# For testing the user_field attributes
self.service_field_needed_fail = "https://field_needed_fail.example.com"
self.service_pattern_field_needed_fail = models.ServicePattern.objects.create(