Use django admin application to add/modif identty providers when CAS_FEDERATE is True

This commit is contained in:
Valentin Samir 2016-07-04 17:23:11 +02:00
parent 40b4f07001
commit aa433d3c58
18 changed files with 600 additions and 388 deletions

View file

@ -12,6 +12,7 @@ exclude_lines =
pragma: no cover pragma: no cover
def __repr__ def __repr__
def __unicode__ def __unicode__
def __str__
raise AssertionError raise AssertionError
raise NotImplementedError raise NotImplementedError
if six.PY3: if six.PY3:

2
.gitignore vendored
View file

@ -15,3 +15,5 @@ coverage.xml
test_venv test_venv
.coverage .coverage
htmlcov/ htmlcov/
tox_logs/
.cache/

View file

@ -165,12 +165,6 @@ Federation settings
* ``CAS_FEDERATE``: A boolean for activating the federated mode (see the federate section below). * ``CAS_FEDERATE``: A boolean for activating the federated mode (see the federate section below).
The default is ``False``. The default is ``False``.
* ``CAS_FEDERATE_PROVIDERS``: A dictionnary for the allowed identity providers (see the federate
section below). The default is ``{}``.
* ``CAS_FEDERATE_PROVIDERS_LIST``: A list in with the keys of ``CAS_FEDERATE_PROVIDERS`` are ordened
for beeing displayed on the login page. The default is the list of all the keys of
``CAS_FEDERATE_PROVIDERS`` sorted in natural order (0 < 2 < 10 < 20 < a = A < … < z = Z and
lexicographical)
* ``CAS_FEDERATE_REMEMBER_TIMEOUT``: Time after witch the cookie use for "remember my identity * ``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 provider" expire. The default is ``604800``, one week. The cookie is called
``_remember_provider``. ``_remember_provider``.
@ -344,26 +338,29 @@ to the provider CAS to authenticate. This provider transmit to ``django-cas-serv
username and attributes. The user is now logged in on ``django-cas-server`` and can use username and attributes. The user is now logged in on ``django-cas-server`` and can use
services using ``django-cas-server`` as CAS. services using ``django-cas-server`` as CAS.
The list of allowed identity providers is defined using the ``CAS_FEDERATE_PROVIDERS`` parameter. The list of allowed identity providers is defined using the django admin application.
For instance: With the development server started, visit http://127.0.0.1:8000/admin/ to add identity providers.
.. code-block:: python An identity provider comes with 5 fields:
CAS_FEDERATE_PROVIDERS = { * `Position`: an integer used to tweak the order in which identity providers are displayed on
"example.com": ("https://cas.example.com", 3, "Example dot com"), the login page. Identity providers are sorted using position first, then, on equal position,
"exemple.fr": ("https://cas.exemple.fr", 3, "Exemple point fr"), 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.
``CAS_FEDERATE_PROVIDERS`` is a dictionnary using provider names as key and a tuple
(cas address, cas version protocol, provider verbose name) as value.
In federation mode, ``django-cas-server`` build user's username as follow: In federation mode, ``django-cas-server`` build user's username as follow:
``provider_returned_username@provider_name``. ``provider_returned_username@provider_suffix``.
You can choose the provider returned username for ``django-cas-server`` and the provider name Choose the provider returned username for ``django-cas-server`` and the provider suffix
in order to make sense. in order to make sense, as this built username is likely to be displayed to end users in
applications.
The "provider verbose name" is showed on the select menu of the login page.
Then using federate mode, you should add one command to a daily crontab: ``cas_clean_federate``. Then using federate mode, you should add one command to a daily crontab: ``cas_clean_federate``.

View file

@ -12,6 +12,7 @@
from django.contrib import admin from django.contrib import admin
from .models import ServiceTicket, ProxyTicket, ProxyGrantingTicket, User, ServicePattern from .models import ServiceTicket, ProxyTicket, ProxyGrantingTicket, User, ServicePattern
from .models import Username, ReplaceAttributName, ReplaceAttributValue, FilterAttributValue from .models import Username, ReplaceAttributName, ReplaceAttributValue, FilterAttributValue
from .models import FederatedIendityProvider
from .forms import TicketForm from .forms import TicketForm
TICKETS_READONLY_FIELDS = ('validate', 'service', 'service_pattern', TICKETS_READONLY_FIELDS = ('validate', 'service', 'service_pattern',
@ -91,5 +92,10 @@ class ServicePatternAdmin(admin.ModelAdmin):
'single_log_out', 'proxy_callback', 'restrict_users') 'single_log_out', 'proxy_callback', 'restrict_users')
class FederatedIendityProviderAdmin(admin.ModelAdmin):
fields = ('pos', 'suffix', 'server_url', 'cas_protocol_version', 'verbose_name')
admin.site.register(User, UserAdmin) admin.site.register(User, UserAdmin)
admin.site.register(ServicePattern, ServicePatternAdmin) admin.site.register(ServicePattern, ServicePatternAdmin)
admin.site.register(FederatedIendityProvider, FederatedIendityProviderAdmin)

View file

@ -148,16 +148,13 @@ class CASFederateAuth(AuthUser):
user = None user = None
def __init__(self, username): def __init__(self, username):
component = username.split('@')
username = '@'.join(component[:-1])
provider = component[-1]
try: try:
self.user = FederatedUser.objects.get(username=username, provider=provider) self.user = FederatedUser.get_from_federated_username(username)
super(CASFederateAuth, self).__init__( super(CASFederateAuth, self).__init__(
"%s@%s" % (self.user.username, self.user.provider) self.user.federated_username
) )
except FederatedUser.DoesNotExist: except FederatedUser.DoesNotExist:
super(CASFederateAuth, self).__init__("%s@%s" % (username, provider)) super(CASFederateAuth, self).__init__(username)
def test_password(self, ticket): def test_password(self, ticket):
"""test `password` agains the user""" """test `password` agains the user"""

View file

@ -13,8 +13,6 @@
from django.conf import settings from django.conf import settings
from django.contrib.staticfiles.templatetags.staticfiles import static from django.contrib.staticfiles.templatetags.staticfiles import static
import re
def setting_default(name, default_value): def setting_default(name, default_value):
"""if the config `name` is not set, set it the `default_value`""" """if the config `name` is not set, set it the `default_value`"""
@ -92,30 +90,7 @@ setting_default(
setting_default('CAS_ENABLE_AJAX_AUTH', False) setting_default('CAS_ENABLE_AJAX_AUTH', False)
setting_default('CAS_FEDERATE', False) setting_default('CAS_FEDERATE', False)
# A dict of "provider suffix" -> (provider CAS server url, CAS version, verbose name)
setting_default('CAS_FEDERATE_PROVIDERS', {})
setting_default('CAS_FEDERATE_REMEMBER_TIMEOUT', 604800) # one week setting_default('CAS_FEDERATE_REMEMBER_TIMEOUT', 604800) # one week
if settings.CAS_FEDERATE: if settings.CAS_FEDERATE:
settings.CAS_AUTH_CLASS = "cas_server.auth.CASFederateAuth" settings.CAS_AUTH_CLASS = "cas_server.auth.CASFederateAuth"
# create CAS_FEDERATE_PROVIDERS_LIST default value if not set: list of
# the keys of CAS_FEDERATE_PROVIDERS in natural order: 2 < 10 < 20 < a = A < … < z = Z
try:
getattr(settings, 'CAS_FEDERATE_PROVIDERS_LIST')
except AttributeError:
__CAS_FEDERATE_PROVIDERS_LIST = list(settings.CAS_FEDERATE_PROVIDERS.keys())
def __cas_federate_providers_list_sort(key):
if len(settings.CAS_FEDERATE_PROVIDERS[key]) > 2:
key = settings.CAS_FEDERATE_PROVIDERS[key][2].lower()
else:
key = key.lower()
return tuple(
int(num) if num else alpha
for num, alpha in __cas_federate_providers_list_sort.tokenize(key)
)
__cas_federate_providers_list_sort.tokenize = re.compile(r'(\d+)|(\D+)').findall
__CAS_FEDERATE_PROVIDERS_LIST.sort(key=__cas_federate_providers_list_sort)
setting_default('CAS_FEDERATE_PROVIDERS_LIST', __CAS_FEDERATE_PROVIDERS_LIST)

View file

@ -11,6 +11,7 @@
# (c) 2016 Valentin Samir # (c) 2016 Valentin Samir
"""federated mode helper classes""" """federated mode helper classes"""
from .default_settings import settings from .default_settings import settings
from django.db import IntegrityError
from .cas import CASClient from .cas import CASClient
from .models import FederatedUser, FederateSLO, User from .models import FederatedUser, FederateSLO, User
@ -29,28 +30,23 @@ class CASFederateValidateUser(object):
def __init__(self, provider, service_url): def __init__(self, provider, service_url):
self.provider = provider self.provider = provider
if provider in settings.CAS_FEDERATE_PROVIDERS: # pragma: no branch (should always be True)
(server_url, version) = settings.CAS_FEDERATE_PROVIDERS[provider][:2]
self.client = CASClient( self.client = CASClient(
service_url=service_url, service_url=service_url,
version=version, version=provider.cas_protocol_version,
server_url=server_url, server_url=provider.server_url,
renew=False, renew=False,
) )
def get_login_url(self): def get_login_url(self):
"""return the CAS provider login url""" """return the CAS provider login url"""
return self.client.get_login_url() if self.client is not None else False return self.client.get_login_url()
def get_logout_url(self, redirect_url=None): def get_logout_url(self, redirect_url=None):
"""return the CAS provider logout url""" """return the CAS provider logout url"""
return self.client.get_logout_url(redirect_url) if self.client is not None else False return self.client.get_logout_url(redirect_url)
def verify_ticket(self, ticket): def verify_ticket(self, ticket):
"""test `ticket` agains the CAS provider, if valid, create the local federated user""" """test `ticket` agains the CAS provider, if valid, create the local federated user"""
if self.client is None: # pragma: no cover (should not happen)
return False
try: try:
username, attributs = self.client.verify_ticket(ticket)[:2] username, attributs = self.client.verify_ticket(ticket)[:2]
except urllib.error.URLError: except urllib.error.URLError:
@ -61,22 +57,13 @@ class CASFederateValidateUser(object):
attributs["provider"] = self.provider attributs["provider"] = self.provider
self.username = username self.username = username
self.attributs = attributs self.attributs = attributs
try: user = FederatedUser.objects.update_or_create(
user = FederatedUser.objects.get(
username=username,
provider=self.provider
)
user.attributs = attributs
user.ticket = ticket
user.save()
except FederatedUser.DoesNotExist:
user = FederatedUser.objects.create(
username=username, username=username,
provider=self.provider, provider=self.provider,
attributs=attributs, defaults=dict(attributs=attributs, ticket=ticket)
ticket=ticket )[0]
)
user.save() user.save()
self.federated_username = user.federated_username
return True return True
else: else:
return False return False
@ -84,11 +71,14 @@ class CASFederateValidateUser(object):
@staticmethod @staticmethod
def register_slo(username, session_key, ticket): def register_slo(username, session_key, ticket):
"""association a ticket with a (username, session) for processing later SLO request""" """association a ticket with a (username, session) for processing later SLO request"""
try:
FederateSLO.objects.create( FederateSLO.objects.create(
username=username, username=username,
session_key=session_key, session_key=session_key,
ticket=ticket ticket=ticket
) )
except IntegrityError: # pragma: no cover (ignore if the FederateSLO already exists)
pass
def clean_sessions(self, logout_request): def clean_sessions(self, logout_request):
"""process a SLO request""" """process a SLO request"""

View file

@ -33,16 +33,14 @@ class FederateSelect(forms.Form):
Form used on the login page when CAS_FEDERATE is True Form used on the login page when CAS_FEDERATE is True
allowing the user to choose a identity provider. allowing the user to choose a identity provider.
""" """
provider = forms.ChoiceField( provider = forms.ModelChoiceField(
queryset=models.FederatedIendityProvider.objects.all().order_by(
"pos",
"verbose_name",
"suffix"
),
to_field_name="suffix",
label=_('Identity provider'), label=_('Identity provider'),
# with use a lambda abstraction to delay the access to settings.CAS_FEDERATE_PROVIDERS
# this is usefull to use the override_settings decorator in tests
choices=[
(
p,
utils.get_tuple(settings.CAS_FEDERATE_PROVIDERS[p], 2, p)
) for p in settings.CAS_FEDERATE_PROVIDERS_LIST
]
) )
service = forms.CharField(label=_('service'), widget=forms.HiddenInput(), required=False) service = forms.CharField(label=_('service'), widget=forms.HiddenInput(), required=False)
method = forms.CharField(widget=forms.HiddenInput(), required=False) method = forms.CharField(widget=forms.HiddenInput(), required=False)
@ -88,13 +86,10 @@ class FederateUserCredential(UserCredential):
def clean(self): def clean(self):
cleaned_data = super(FederateUserCredential, self).clean() cleaned_data = super(FederateUserCredential, self).clean()
try: try:
component = cleaned_data["username"].split('@') user = models.FederatedUser.get_from_federated_username(cleaned_data["username"])
username = '@'.join(component[:-1])
provider = component[-1]
user = models.FederatedUser.objects.get(username=username, provider=provider)
user.ticket = "" user.ticket = ""
user.save() user.save()
# should not happed as is the FederatedUser do not exists, super should # should not happed as if the FederatedUser do not exists, super should
# raise before a ValidationError("bad user") # raise before a ValidationError("bad user")
except models.FederatedUser.DoesNotExist: # pragma: no cover (should not happend) except models.FederatedUser.DoesNotExist: # pragma: no cover (should not happend)
raise forms.ValidationError( raise forms.ValidationError(

View file

@ -7,8 +7,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: cas_server\n" "Project-Id-Version: cas_server\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-06-21 00:14+0200\n" "POT-Creation-Date: 2016-07-04 17:15+0200\n"
"PO-Revision-Date: 2016-06-21 00:16+0200\n" "PO-Revision-Date: 2016-07-04 17:15+0200\n"
"Last-Translator: Valentin Samir <valentin.samir@crans.org>\n" "Last-Translator: Valentin Samir <valentin.samir@crans.org>\n"
"Language-Team: django <LL@li.org>\n" "Language-Team: django <LL@li.org>\n"
"Language: en\n" "Language: en\n"
@ -17,88 +17,135 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.8.8\n" "X-Generator: Poedit 1.8.8\n"
#: apps.py:7 templates/cas_server/base.html:3 templates/cas_server/base.html:21 #: apps.py:19 templates/cas_server/base.html:3
#: templates/cas_server/base.html:20
msgid "Central Authentication Service" msgid "Central Authentication Service"
msgstr "Central Authentication Service" msgstr "Central Authentication Service"
#: forms.py:32 #: forms.py:43
msgid "Identity provider" msgid "Identity provider"
msgstr "Identity provider" msgstr "Identity provider"
#: forms.py:35 forms.py:44 forms.py:92 #: forms.py:45 forms.py:55 forms.py:106
msgid "service" msgid "service"
msgstr "" msgstr ""
#: forms.py:37 #: forms.py:47
msgid "Remember the identity provider" msgid "Remember the identity provider"
msgstr "Remember the identity provider" msgstr "Remember the identity provider"
#: forms.py:38 forms.py:48 #: forms.py:48 forms.py:59
msgid "warn" msgid "warn"
msgstr " Warn me before logging me into other sites." msgstr " Warn me before logging me into other sites."
#: forms.py:43 #: forms.py:54
msgid "login" msgid "login"
msgstr "username" msgstr "username"
#: forms.py:45 #: forms.py:56
msgid "password" msgid "password"
msgstr "password" msgstr "password"
#: forms.py:59 #: forms.py:71
msgid "Bad user" msgid "Bad user"
msgstr "The credentials you provided cannot be determined to be authentic." msgstr "The credentials you provided cannot be determined to be authentic."
#: management/commands/cas_clean_federate.py:13 #: forms.py:96
msgid "User not found in the temporary database, please try to reconnect"
msgstr ""
#: management/commands/cas_clean_federate.py:20
msgid "Clean old federated users" msgid "Clean old federated users"
msgstr "Clean old federated users" msgstr "Clean old federated users"
#: management/commands/cas_clean_sessions.py:9 #: management/commands/cas_clean_sessions.py:22
msgid "Clean deleted sessions" msgid "Clean deleted sessions"
msgstr "Clean deleted sessions" msgstr "Clean deleted sessions"
#: management/commands/cas_clean_tickets.py:9 #: management/commands/cas_clean_tickets.py:22
msgid "Clean old trickets" msgid "Clean old trickets"
msgstr "Clean old trickets" msgstr "Clean old trickets"
#: models.py:55 #: models.py:42
msgid "identity provider"
msgstr "identity provider"
#: models.py:43
msgid "identity providers"
msgstr "identity providers"
#: models.py:47
msgid "suffix"
msgstr ""
#: models.py:48
msgid ""
"Suffix append to backend CAS returner username: `returned_username`@`suffix`"
msgstr ""
#: models.py:50
msgid "server url"
msgstr ""
#: models.py:59
msgid "CAS protocol version"
msgstr ""
#: models.py:60
msgid ""
"Version of the CAS protocol to use when sending requests the the backend CAS"
msgstr ""
#: models.py:65
msgid "verbose name"
msgstr ""
#: models.py:66
msgid "Name for this identity provider displayed on the login page"
msgstr ""
#: models.py:70 models.py:312
msgid "position"
msgstr "position"
#: models.py:159
msgid "User" msgid "User"
msgstr "" msgstr ""
#: models.py:56 #: models.py:160
msgid "Users" msgid "Users"
msgstr "" msgstr ""
#: models.py:114 #: models.py:229
#, python-format #, python-format
msgid "Error during service logout %s" msgid "Error during service logout %s"
msgstr "Error during service logout %s" msgstr "Error during service logout %s"
#: models.py:182 #: models.py:307
msgid "Service pattern" msgid "Service pattern"
msgstr "Service pattern" msgstr "Service pattern"
#: models.py:183 #: models.py:308
msgid "Services patterns" msgid "Services patterns"
msgstr "" msgstr ""
#: models.py:187 #: models.py:313
msgid "position" msgid "service patterns are sorted using the position attribute"
msgstr "position" msgstr ""
#: models.py:194 models.py:316 #: models.py:320 models.py:444
msgid "name" msgid "name"
msgstr "name" msgstr "name"
#: models.py:195 #: models.py:321
msgid "A name for the service" msgid "A name for the service"
msgstr "A name for the service" msgstr "A name for the service"
#: models.py:200 models.py:344 models.py:362 #: models.py:326 models.py:473 models.py:492
msgid "pattern" msgid "pattern"
msgstr "pattern" msgstr "pattern"
#: models.py:202 #: models.py:328
msgid "" msgid ""
"A regular expression matching services. Will usually looks like '^https://" "A regular expression matching services. Will usually looks like '^https://"
"some\\.server\\.com/path/.*$'.As it is a regular expression, special " "some\\.server\\.com/path/.*$'.As it is a regular expression, special "
@ -108,73 +155,73 @@ msgstr ""
"some\\.server\\.com/path/.*$'.As it is a regular expression, special " "some\\.server\\.com/path/.*$'.As it is a regular expression, special "
"character must be escaped with a '\\'." "character must be escaped with a '\\'."
#: models.py:211 #: models.py:337
msgid "user field" msgid "user field"
msgstr "" msgstr ""
#: models.py:212 #: models.py:338
msgid "Name of the attribut to transmit as username, empty = login" msgid "Name of the attribut to transmit as username, empty = login"
msgstr "Name of the attribut to transmit as username, empty = login" msgstr "Name of the attribut to transmit as username, empty = login"
#: models.py:216 #: models.py:342
msgid "restrict username" msgid "restrict username"
msgstr "" msgstr ""
#: models.py:217 #: models.py:343
msgid "Limit username allowed to connect to the list provided bellow" msgid "Limit username allowed to connect to the list provided bellow"
msgstr "Limit username allowed to connect to the list provided bellow" msgstr "Limit username allowed to connect to the list provided bellow"
#: models.py:221 #: models.py:347
msgid "proxy" msgid "proxy"
msgstr "proxy" msgstr "proxy"
#: models.py:222 #: models.py:348
msgid "Proxy tickets can be delivered to the service" msgid "Proxy tickets can be delivered to the service"
msgstr "Proxy tickets can be delivered to the service" msgstr "Proxy tickets can be delivered to the service"
#: models.py:226 #: models.py:352
msgid "proxy callback" msgid "proxy callback"
msgstr "proxy callback" msgstr "proxy callback"
#: models.py:227 #: models.py:353
msgid "can be used as a proxy callback to deliver PGT" msgid "can be used as a proxy callback to deliver PGT"
msgstr "can be used as a proxy callback to deliver PGT" msgstr "can be used as a proxy callback to deliver PGT"
#: models.py:231 #: models.py:357
msgid "single log out" msgid "single log out"
msgstr "" msgstr ""
#: models.py:232 #: models.py:358
msgid "Enable SLO for the service" msgid "Enable SLO for the service"
msgstr "Enable SLO for the service" msgstr "Enable SLO for the service"
#: models.py:239 #: models.py:365
msgid "single log out callback" msgid "single log out callback"
msgstr "" msgstr ""
#: models.py:240 #: models.py:366
msgid "" msgid ""
"URL where the SLO request will be POST. empty = service url\n" "URL where the SLO request will be POST. empty = service url\n"
"This is usefull for non HTTP proxied services." "This is usefull for non HTTP proxied services."
msgstr "" msgstr ""
#: models.py:301 #: models.py:428
msgid "username" msgid "username"
msgstr "" msgstr ""
#: models.py:302 #: models.py:429
msgid "username allowed to connect to the service" msgid "username allowed to connect to the service"
msgstr "username allowed to connect to the service" msgstr "username allowed to connect to the service"
#: models.py:317 #: models.py:445
msgid "name of an attribut to send to the service, use * for all attributes" msgid "name of an attribut to send to the service, use * for all attributes"
msgstr "name of an attribut to send to the service, use * for all attributes" msgstr "name of an attribut to send to the service, use * for all attributes"
#: models.py:322 models.py:368 #: models.py:450 models.py:498
msgid "replace" msgid "replace"
msgstr "replace" msgstr "replace"
#: models.py:323 #: models.py:451
msgid "" msgid ""
"name under which the attribut will be showto the service. empty = default " "name under which the attribut will be showto the service. empty = default "
"name of the attribut" "name of the attribut"
@ -182,39 +229,30 @@ msgstr ""
"name under which the attribut will be showto the service. empty = default " "name under which the attribut will be showto the service. empty = default "
"name of the attribut" "name of the attribut"
#: models.py:339 models.py:357 #: models.py:468 models.py:487
msgid "attribut" msgid "attribut"
msgstr "attribut" msgstr "attribut"
#: models.py:340 #: models.py:469
msgid "Name of the attribut which must verify pattern" msgid "Name of the attribut which must verify pattern"
msgstr "Name of the attribut which must verify pattern" msgstr "Name of the attribut which must verify pattern"
#: models.py:345 #: models.py:474
msgid "a regular expression" msgid "a regular expression"
msgstr "a regular expression" msgstr "a regular expression"
#: models.py:358 #: models.py:488
msgid "Name of the attribut for which the value must be replace" msgid "Name of the attribut for which the value must be replace"
msgstr "Name of the attribut for which the value must be replace" msgstr "Name of the attribut for which the value must be replace"
#: models.py:363 #: models.py:493
msgid "An regular expression maching whats need to be replaced" msgid "An regular expression maching whats need to be replaced"
msgstr "An regular expression maching whats need to be replaced" msgstr "An regular expression maching whats need to be replaced"
#: models.py:369 #: models.py:499
msgid "replace expression, groups are capture by \\1, \\2 …" msgid "replace expression, groups are capture by \\1, \\2 …"
msgstr "replace expression, groups are capture by \\1, \\2 …" msgstr "replace expression, groups are capture by \\1, \\2 …"
#: models.py:476
#, python-format
msgid ""
"Error during service logout %(service)s:\n"
"%(error)s"
msgstr ""
"Error during service logout %(service)s:\n"
"%(error)s"
#: templates/cas_server/logged.html:6 #: templates/cas_server/logged.html:6
msgid "Logged" msgid "Logged"
msgstr "" msgstr ""
@ -243,7 +281,7 @@ msgstr "Login"
msgid "Connect to the service" msgid "Connect to the service"
msgstr "Connect to the service" msgstr "Connect to the service"
#: views.py:140 #: views.py:152
msgid "" msgid ""
"<h3>Logout successful</h3>You have successfully logged out from the Central " "<h3>Logout successful</h3>You have successfully logged out from the Central "
"Authentication Service. For security reasons, exit your web browser." "Authentication Service. For security reasons, exit your web browser."
@ -251,7 +289,7 @@ msgstr ""
"<h3>Logout successful</h3>You have successfully logged out from the Central " "<h3>Logout successful</h3>You have successfully logged out from the Central "
"Authentication Service. For security reasons, exit your web browser." "Authentication Service. For security reasons, exit your web browser."
#: views.py:146 #: views.py:158
#, python-format #, python-format
msgid "" msgid ""
"<h3>Logout successful</h3>You have successfully logged out from %s sessions " "<h3>Logout successful</h3>You have successfully logged out from %s sessions "
@ -262,7 +300,7 @@ msgstr ""
"of the Central Authentication Service. For security reasons, exit your web " "of the Central Authentication Service. For security reasons, exit your web "
"browser." "browser."
#: views.py:153 #: views.py:165
msgid "" msgid ""
"<h3>Logout successful</h3>You were already logged out from the Central " "<h3>Logout successful</h3>You were already logged out from the Central "
"Authentication Service. For security reasons, exit your web browser." "Authentication Service. For security reasons, exit your web browser."
@ -270,48 +308,55 @@ msgstr ""
"<h3>Logout successful</h3>You were already logged out from the Central " "<h3>Logout successful</h3>You were already logged out from the Central "
"Authentication Service. For security reasons, exit your web browser." "Authentication Service. For security reasons, exit your web browser."
#: views.py:294 #: views.py:349
msgid "Invalid login ticket" msgid "Invalid login ticket"
msgstr "Invalid login ticket, please retry to login" msgstr "Invalid login ticket, please retry to login"
#: views.py:410 #: views.py:470
#, python-format #, python-format
msgid "Authentication has been required by service %(name)s (%(url)s)" msgid "Authentication has been required by service %(name)s (%(url)s)"
msgstr "Authentication has been required by service %(name)s (%(url)s)" msgstr "Authentication has been required by service %(name)s (%(url)s)"
#: views.py:448 #: views.py:508
#, python-format #, python-format
msgid "Service %(url)s non allowed." msgid "Service %(url)s non allowed."
msgstr "Service %(url)s non allowed." msgstr "Service %(url)s non allowed."
#: views.py:455 #: views.py:515
msgid "Username non allowed" msgid "Username non allowed"
msgstr "Username non allowed" msgstr "Username non allowed"
#: views.py:462 #: views.py:522
msgid "User charateristics non allowed" msgid "User charateristics non allowed"
msgstr "User charateristics non allowed" msgstr "User charateristics non allowed"
#: views.py:469 #: views.py:529
#, python-format #, python-format
msgid "The attribut %(field)s is needed to use that service" msgid "The attribut %(field)s is needed to use that service"
msgstr "The attribut %(field)s is needed to use that service" msgstr "The attribut %(field)s is needed to use that service"
#: views.py:539 #: views.py:599
#, python-format #, python-format
msgid "Authentication renewal required by service %(name)s (%(url)s)." msgid "Authentication renewal required by service %(name)s (%(url)s)."
msgstr "Authentication renewal required by service %(name)s (%(url)s)." msgstr "Authentication renewal required by service %(name)s (%(url)s)."
#: views.py:546 #: views.py:606
#, python-format #, python-format
msgid "Authentication required by service %(name)s (%(url)s)." msgid "Authentication required by service %(name)s (%(url)s)."
msgstr "Authentication required by service %(name)s (%(url)s)." msgstr "Authentication required by service %(name)s (%(url)s)."
#: views.py:553 #: views.py:613
#, python-format #, python-format
msgid "Service %s non allowed" msgid "Service %s non allowed"
msgstr "Service %s non allowed" msgstr "Service %s non allowed"
#~ msgid ""
#~ "Error during service logout %(service)s:\n"
#~ "%(error)s"
#~ msgstr ""
#~ "Error during service logout %(service)s:\n"
#~ "%(error)s"
#~ msgid "Successfully logout" #~ msgid "Successfully logout"
#~ msgstr "" #~ msgstr ""
#~ "<h3>Logout successful</h3>You have successfully logged out of the Central " #~ "<h3>Logout successful</h3>You have successfully logged out of the Central "

View file

@ -7,8 +7,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: cas_server\n" "Project-Id-Version: cas_server\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-06-21 00:14+0200\n" "POT-Creation-Date: 2016-07-04 17:15+0200\n"
"PO-Revision-Date: 2016-06-21 00:15+0200\n" "PO-Revision-Date: 2016-07-04 17:21+0200\n"
"Last-Translator: Valentin Samir <valentin.samir@crans.org>\n" "Last-Translator: Valentin Samir <valentin.samir@crans.org>\n"
"Language-Team: django <LL@li.org>\n" "Language-Team: django <LL@li.org>\n"
"Language: fr\n" "Language: fr\n"
@ -18,88 +18,141 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n > 1);\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Generator: Poedit 1.8.8\n" "X-Generator: Poedit 1.8.8\n"
#: apps.py:7 templates/cas_server/base.html:3 templates/cas_server/base.html:21 #: apps.py:19 templates/cas_server/base.html:3
#: templates/cas_server/base.html:20
msgid "Central Authentication Service" msgid "Central Authentication Service"
msgstr "Service Central d'Authentification" msgstr "Service Central d'Authentification"
#: forms.py:32 #: forms.py:43
msgid "Identity provider" msgid "Identity provider"
msgstr "fournisseur d'identité" msgstr "fournisseur d'identité"
#: forms.py:35 forms.py:44 forms.py:92 #: forms.py:45 forms.py:55 forms.py:106
msgid "service" msgid "service"
msgstr "service" msgstr "service"
#: forms.py:37 #: forms.py:47
msgid "Remember the identity provider" msgid "Remember the identity provider"
msgstr "Se souvenir du fournisseur d'identité" msgstr "Se souvenir du fournisseur d'identité"
#: forms.py:38 forms.py:48 #: forms.py:48 forms.py:59
msgid "warn" msgid "warn"
msgstr "Prévenez-moi avant d'accéder à d'autres services." msgstr "Prévenez-moi avant d'accéder à d'autres services."
#: forms.py:43 #: forms.py:54
msgid "login" msgid "login"
msgstr "Identifiant" msgstr "Identifiant"
#: forms.py:45 #: forms.py:56
msgid "password" msgid "password"
msgstr "mot de passe" msgstr "mot de passe"
#: forms.py:59 #: forms.py:71
msgid "Bad user" msgid "Bad user"
msgstr "Les informations transmises n'ont pas permis de vous authentifier." msgstr "Les informations transmises n'ont pas permis de vous authentifier."
#: management/commands/cas_clean_federate.py:13 #: forms.py:96
msgid "User not found in the temporary database, please try to reconnect"
msgstr ""
"Utilisateur non trouvé dans la base de donnée temporaire, essayez de vous "
"reconnecter"
#: management/commands/cas_clean_federate.py:20
msgid "Clean old federated users" msgid "Clean old federated users"
msgstr "Nettoyer les anciens utilisateurs fédéré" msgstr "Nettoyer les anciens utilisateurs fédéré"
#: management/commands/cas_clean_sessions.py:9 #: management/commands/cas_clean_sessions.py:22
msgid "Clean deleted sessions" msgid "Clean deleted sessions"
msgstr "Nettoyer les sessions supprimées" msgstr "Nettoyer les sessions supprimées"
#: management/commands/cas_clean_tickets.py:9 #: management/commands/cas_clean_tickets.py:22
msgid "Clean old trickets" msgid "Clean old trickets"
msgstr "Nettoyer les vieux tickets" msgstr "Nettoyer les vieux tickets"
#: models.py:55 #: models.py:42
msgid "identity provider"
msgstr "fournisseur d'identité"
#: models.py:43
msgid "identity providers"
msgstr "fournisseurs d'identités"
#: models.py:47
msgid "suffix"
msgstr "suffixe"
#: models.py:48
msgid ""
"Suffix append to backend CAS returner username: `returned_username`@`suffix`"
msgstr ""
"Suffixe ajouté au nom d'utilisateur retourné par le CAS du fournisseur "
"d'identité : `nom retourné`@`suffixe`"
#: models.py:50
msgid "server url"
msgstr "url du serveur"
#: models.py:59
msgid "CAS protocol version"
msgstr "Version du protocole CAS"
#: models.py:60
msgid ""
"Version of the CAS protocol to use when sending requests the the backend CAS"
msgstr ""
"Version du protocole CAS à utiliser lorsque l'on envoie des requête au CAS "
"du fournisseur d'identité"
#: models.py:65
msgid "verbose name"
msgstr "Nom du fournisseur"
#: models.py:66
msgid "Name for this identity provider displayed on the login page"
msgstr "Nom affiché pour ce fournisseur d'identité sur la page de connexion"
#: models.py:70 models.py:312
msgid "position"
msgstr "position"
#: models.py:159
msgid "User" msgid "User"
msgstr "Utilisateur" msgstr "Utilisateur"
#: models.py:56 #: models.py:160
msgid "Users" msgid "Users"
msgstr "Utilisateurs" msgstr "Utilisateurs"
#: models.py:114 #: models.py:229
#, python-format #, python-format
msgid "Error during service logout %s" msgid "Error during service logout %s"
msgstr "Une erreur est survenue durant la déconnexion du service %s" msgstr "Une erreur est survenue durant la déconnexion du service %s"
#: models.py:182 #: models.py:307
msgid "Service pattern" msgid "Service pattern"
msgstr "Motif de service" msgstr "Motif de service"
#: models.py:183 #: models.py:308
msgid "Services patterns" msgid "Services patterns"
msgstr "Motifs de services" msgstr "Motifs de services"
#: models.py:187 #: models.py:313
msgid "position" msgid "service patterns are sorted using the position attribute"
msgstr "position" msgstr "Les motifs de service sont trié selon l'attribut position"
#: models.py:194 models.py:316 #: models.py:320 models.py:444
msgid "name" msgid "name"
msgstr "nom" msgstr "nom"
#: models.py:195 #: models.py:321
msgid "A name for the service" msgid "A name for the service"
msgstr "Un nom pour le service" msgstr "Un nom pour le service"
#: models.py:200 models.py:344 models.py:362 #: models.py:326 models.py:473 models.py:492
msgid "pattern" msgid "pattern"
msgstr "motif" msgstr "motif"
#: models.py:202 #: models.py:328
msgid "" msgid ""
"A regular expression matching services. Will usually looks like '^https://" "A regular expression matching services. Will usually looks like '^https://"
"some\\.server\\.com/path/.*$'.As it is a regular expression, special " "some\\.server\\.com/path/.*$'.As it is a regular expression, special "
@ -110,55 +163,55 @@ msgstr ""
"expression rationnelle, les caractères spéciaux doivent être échappés avec " "expression rationnelle, les caractères spéciaux doivent être échappés avec "
"un '\\'." "un '\\'."
#: models.py:211 #: models.py:337
msgid "user field" msgid "user field"
msgstr "champ utilisateur" msgstr "champ utilisateur"
#: models.py:212 #: models.py:338
msgid "Name of the attribut to transmit as username, empty = login" msgid "Name of the attribut to transmit as username, empty = login"
msgstr "" msgstr ""
"Nom de l'attribut devant être transmis comme nom d'utilisateur au service. " "Nom de l'attribut devant être transmis comme nom d'utilisateur au service. "
"vide = nom de connection" "vide = nom de connection"
#: models.py:216 #: models.py:342
msgid "restrict username" msgid "restrict username"
msgstr "limiter les noms d'utilisateurs" msgstr "limiter les noms d'utilisateurs"
#: models.py:217 #: models.py:343
msgid "Limit username allowed to connect to the list provided bellow" msgid "Limit username allowed to connect to the list provided bellow"
msgstr "" msgstr ""
"Limiter les noms d'utilisateurs autorisé à se connecter à la liste fournie " "Limiter les noms d'utilisateurs autorisé à se connecter à la liste fournie "
"ci-dessous" "ci-dessous"
#: models.py:221 #: models.py:347
msgid "proxy" msgid "proxy"
msgstr "proxy" msgstr "proxy"
#: models.py:222 #: models.py:348
msgid "Proxy tickets can be delivered to the service" msgid "Proxy tickets can be delivered to the service"
msgstr "des proxy tickets peuvent être délivrés au service" msgstr "des proxy tickets peuvent être délivrés au service"
#: models.py:226 #: models.py:352
msgid "proxy callback" msgid "proxy callback"
msgstr "" msgstr ""
#: models.py:227 #: models.py:353
msgid "can be used as a proxy callback to deliver PGT" msgid "can be used as a proxy callback to deliver PGT"
msgstr "peut être utilisé comme un callback pour recevoir un PGT" msgstr "peut être utilisé comme un callback pour recevoir un PGT"
#: models.py:231 #: models.py:357
msgid "single log out" msgid "single log out"
msgstr "" msgstr ""
#: models.py:232 #: models.py:358
msgid "Enable SLO for the service" msgid "Enable SLO for the service"
msgstr "Active le SLO pour le service" msgstr "Active le SLO pour le service"
#: models.py:239 #: models.py:365
msgid "single log out callback" msgid "single log out callback"
msgstr "" msgstr ""
#: models.py:240 #: models.py:366
msgid "" msgid ""
"URL where the SLO request will be POST. empty = service url\n" "URL where the SLO request will be POST. empty = service url\n"
"This is usefull for non HTTP proxied services." "This is usefull for non HTTP proxied services."
@ -167,63 +220,54 @@ msgstr ""
"service\n" "service\n"
"Ceci n'est utilise que pour des services non HTTP proxifiés" "Ceci n'est utilise que pour des services non HTTP proxifiés"
#: models.py:301 #: models.py:428
msgid "username" msgid "username"
msgstr "nom d'utilisateur" msgstr "nom d'utilisateur"
#: models.py:302 #: models.py:429
msgid "username allowed to connect to the service" msgid "username allowed to connect to the service"
msgstr "noms d'utilisateurs autorisé à se connecter au service" msgstr "noms d'utilisateurs autorisé à se connecter au service"
#: models.py:317 #: models.py:445
msgid "name of an attribut to send to the service, use * for all attributes" msgid "name of an attribut to send to the service, use * for all attributes"
msgstr "" msgstr ""
"nom d'un attribut a envoyer au service, utiliser * pour tous les attributs" "nom d'un attribut a envoyer au service, utiliser * pour tous les attributs"
#: models.py:322 models.py:368 #: models.py:450 models.py:498
msgid "replace" msgid "replace"
msgstr "remplacement" msgstr "remplacement"
#: models.py:323 #: models.py:451
msgid "" msgid ""
"name under which the attribut will be showto the service. empty = default " "name under which the attribut will be showto the service. empty = default "
"name of the attribut" "name of the attribut"
msgstr "" msgstr ""
"nom sous lequel l'attribut sera rendu visible au service. vide = inchangé" "nom sous lequel l'attribut sera rendu visible au service. vide = inchangé"
#: models.py:339 models.py:357 #: models.py:468 models.py:487
msgid "attribut" msgid "attribut"
msgstr "attribut" msgstr "attribut"
#: models.py:340 #: models.py:469
msgid "Name of the attribut which must verify pattern" msgid "Name of the attribut which must verify pattern"
msgstr "Nom de l'attribut devant vérifier un motif" msgstr "Nom de l'attribut devant vérifier un motif"
#: models.py:345 #: models.py:474
msgid "a regular expression" msgid "a regular expression"
msgstr "une expression régulière" msgstr "une expression régulière"
#: models.py:358 #: models.py:488
msgid "Name of the attribut for which the value must be replace" msgid "Name of the attribut for which the value must be replace"
msgstr "nom de l'attribue pour lequel la valeur doit être remplacé" msgstr "nom de l'attribue pour lequel la valeur doit être remplacé"
#: models.py:363 #: models.py:493
msgid "An regular expression maching whats need to be replaced" msgid "An regular expression maching whats need to be replaced"
msgstr "une expression régulière reconnaissant ce qui doit être remplacé" msgstr "une expression régulière reconnaissant ce qui doit être remplacé"
#: models.py:369 #: models.py:499
msgid "replace expression, groups are capture by \\1, \\2 …" msgid "replace expression, groups are capture by \\1, \\2 …"
msgstr "expression de remplacement, les groupe sont capturé par \\1, \\2" msgstr "expression de remplacement, les groupe sont capturé par \\1, \\2"
#: models.py:476
#, python-format
msgid ""
"Error during service logout %(service)s:\n"
"%(error)s"
msgstr ""
"Une erreur est survenue durant la déconnexion du service %(service)s:"
"%(error)s"
#: templates/cas_server/logged.html:6 #: templates/cas_server/logged.html:6
msgid "Logged" msgid "Logged"
msgstr "" msgstr ""
@ -252,7 +296,7 @@ msgstr "Connexion"
msgid "Connect to the service" msgid "Connect to the service"
msgstr "Se connecter au service" msgstr "Se connecter au service"
#: views.py:140 #: views.py:152
msgid "" msgid ""
"<h3>Logout successful</h3>You have successfully logged out from the Central " "<h3>Logout successful</h3>You have successfully logged out from the Central "
"Authentication Service. For security reasons, exit your web browser." "Authentication Service. For security reasons, exit your web browser."
@ -261,7 +305,7 @@ msgstr ""
"d'Authentification. Pour des raisons de sécurité, veuillez fermer votre " "d'Authentification. Pour des raisons de sécurité, veuillez fermer votre "
"navigateur." "navigateur."
#: views.py:146 #: views.py:158
#, python-format #, python-format
msgid "" msgid ""
"<h3>Logout successful</h3>You have successfully logged out from %s sessions " "<h3>Logout successful</h3>You have successfully logged out from %s sessions "
@ -272,7 +316,7 @@ msgstr ""
"Service Central d'Authentification. Pour des raisons de sécurité, veuillez " "Service Central d'Authentification. Pour des raisons de sécurité, veuillez "
"fermer votre navigateur." "fermer votre navigateur."
#: views.py:153 #: views.py:165
msgid "" msgid ""
"<h3>Logout successful</h3>You were already logged out from the Central " "<h3>Logout successful</h3>You were already logged out from the Central "
"Authentication Service. For security reasons, exit your web browser." "Authentication Service. For security reasons, exit your web browser."
@ -281,50 +325,57 @@ msgstr ""
"d'Authentification. Pour des raisons de sécurité, veuillez fermer votre " "d'Authentification. Pour des raisons de sécurité, veuillez fermer votre "
"navigateur." "navigateur."
#: views.py:294 #: views.py:349
msgid "Invalid login ticket" msgid "Invalid login ticket"
msgstr "Ticket de connexion invalide, merci de réessayé de vous connecter" msgstr "Ticket de connexion invalide, merci de réessayé de vous connecter"
#: views.py:410 #: views.py:470
#, python-format #, python-format
msgid "Authentication has been required by service %(name)s (%(url)s)" msgid "Authentication has been required by service %(name)s (%(url)s)"
msgstr "" msgstr ""
"Une demande d'authentification a été émise pour le service %(name)s " "Une demande d'authentification a été émise pour le service %(name)s "
"(%(url)s)." "(%(url)s)."
#: views.py:448 #: views.py:508
#, python-format #, python-format
msgid "Service %(url)s non allowed." msgid "Service %(url)s non allowed."
msgstr "le service %(url)s n'est pas autorisé." msgstr "le service %(url)s n'est pas autorisé."
#: views.py:455 #: views.py:515
msgid "Username non allowed" msgid "Username non allowed"
msgstr "Nom d'utilisateur non authorisé" msgstr "Nom d'utilisateur non authorisé"
#: views.py:462 #: views.py:522
msgid "User charateristics non allowed" msgid "User charateristics non allowed"
msgstr "Caractéristique utilisateur non autorisée" msgstr "Caractéristique utilisateur non autorisée"
#: views.py:469 #: views.py:529
#, python-format #, python-format
msgid "The attribut %(field)s is needed to use that service" msgid "The attribut %(field)s is needed to use that service"
msgstr "L'attribut %(field)s est nécessaire pour se connecter à ce service" msgstr "L'attribut %(field)s est nécessaire pour se connecter à ce service"
#: views.py:539 #: views.py:599
#, python-format #, python-format
msgid "Authentication renewal required by service %(name)s (%(url)s)." msgid "Authentication renewal required by service %(name)s (%(url)s)."
msgstr "Demande de réauthentification pour le service %(name)s (%(url)s)." msgstr "Demande de réauthentification pour le service %(name)s (%(url)s)."
#: views.py:546 #: views.py:606
#, python-format #, python-format
msgid "Authentication required by service %(name)s (%(url)s)." msgid "Authentication required by service %(name)s (%(url)s)."
msgstr "Authentification requise par le service %(name)s (%(url)s)." msgstr "Authentification requise par le service %(name)s (%(url)s)."
#: views.py:553 #: views.py:613
#, python-format #, python-format
msgid "Service %s non allowed" msgid "Service %s non allowed"
msgstr "Le service %s n'est pas autorisé" msgstr "Le service %s n'est pas autorisé"
#~ msgid ""
#~ "Error during service logout %(service)s:\n"
#~ "%(error)s"
#~ msgstr ""
#~ "Une erreur est survenue durant la déconnexion du service %(service)s:"
#~ "%(error)s"
#~ msgid "Successfully logout" #~ msgid "Successfully logout"
#~ msgstr "" #~ msgstr ""
#~ "<h3>Déconnexion réussie</h3>\n" #~ "<h3>Déconnexion réussie</h3>\n"

View file

@ -0,0 +1,50 @@
# -*- 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')]),
),
]

View file

@ -17,6 +17,7 @@ from django.db.models import Q
from django.contrib import messages from django.contrib import messages
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils import timezone from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible
from picklefield.fields import PickledObjectField from picklefield.fields import PickledObjectField
import re import re
@ -34,18 +35,93 @@ SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@python_2_unicode_compatible
class FederatedIendityProvider(models.Model):
"""An identity provider for the federated mode"""
class Meta:
verbose_name = _("identity provider")
verbose_name_plural = _("identity providers")
suffix = models.CharField(
max_length=30,
unique=True,
verbose_name=_(u"suffix"),
help_text=_("Suffix append to backend CAS returner username: `returned_username`@`suffix`")
)
server_url = models.CharField(max_length=255, verbose_name=_(u"server url"))
cas_protocol_version = models.CharField(
max_length=30,
choices=[
("1", "CAS 1.0"),
("2", "CAS 2.0"),
("3", "CAS 3.0"),
("CAS_2_SAML_1_0", "SAML 1.1")
],
verbose_name=_(u"CAS protocol version"),
help_text=_("Version of the CAS protocol to use when sending requests the the backend CAS"),
default="3"
)
verbose_name = models.CharField(
max_length=255,
verbose_name=_(u"verbose name"),
help_text=_("Name for this identity provider displayed on the login page")
)
pos = models.IntegerField(
default=100,
verbose_name=_(u"position"),
help_text=_(
(
u"Identity provider are sorted using the "
u"(position, verbose name, suffix) attributes"
)
)
)
def __str__(self):
return self.verbose_name
@staticmethod
def build_username_from_suffix(username, suffix):
"""Transform backend username into federated username using `suffix`"""
return u'%s@%s' % (username, suffix)
def build_username(self, username):
"""Transform backend username into federated username"""
return u'%s@%s' % (username, self.suffix)
@python_2_unicode_compatible
class FederatedUser(models.Model): class FederatedUser(models.Model):
"""A federated user as returner by a CAS provider (username and attributes)""" """A federated user as returner by a CAS provider (username and attributes)"""
class Meta: class Meta:
unique_together = ("username", "provider") unique_together = ("username", "provider")
username = models.CharField(max_length=124) username = models.CharField(max_length=124)
provider = models.CharField(max_length=124) provider = models.ForeignKey(FederatedIendityProvider, on_delete=models.CASCADE)
attributs = PickledObjectField() attributs = PickledObjectField()
ticket = models.CharField(max_length=255) ticket = models.CharField(max_length=255)
last_update = models.DateTimeField(auto_now=True) last_update = models.DateTimeField(auto_now=True)
def __unicode__(self): def __str__(self):
return u"%s@%s" % (self.username, self.provider) return self.federated_username
@property
def federated_username(self):
"""return the federated username with a suffix"""
return self.provider.build_username(self.username)
@classmethod
def get_from_federated_username(cls, username):
"""return a FederatedUser object from a federated username"""
if username is None:
raise cls.DoesNotExist()
else:
component = username.split('@')
username = '@'.join(component[:-1])
suffix = component[-1]
try:
provider = FederatedIendityProvider.objects.get(suffix=suffix)
return cls.objects.get(username=username, provider=provider)
except FederatedIendityProvider.DoesNotExist:
raise cls.DoesNotExist()
@classmethod @classmethod
def clean_old_entries(cls): def clean_old_entries(cls):
@ -55,17 +131,17 @@ class FederatedUser(models.Model):
) )
known_users = {user.username for user in User.objects.all()} known_users = {user.username for user in User.objects.all()}
for user in federated_users: for user in federated_users:
if not ('%s@%s' % (user.username, user.provider)) in known_users: if user.federated_username not in known_users:
user.delete() user.delete()
class FederateSLO(models.Model): class FederateSLO(models.Model):
"""An association between a CAS provider ticket and a (username, session) for processing SLO""" """An association between a CAS provider ticket and a (username, session) for processing SLO"""
class Meta: class Meta:
unique_together = ("username", "session_key") unique_together = ("username", "session_key", "ticket")
username = models.CharField(max_length=30) username = models.CharField(max_length=30)
session_key = models.CharField(max_length=40, blank=True, null=True) session_key = models.CharField(max_length=40, blank=True, null=True)
ticket = models.CharField(max_length=255) ticket = models.CharField(max_length=255, db_index=True)
@classmethod @classmethod
def clean_deleted_sessions(cls): def clean_deleted_sessions(cls):
@ -75,6 +151,7 @@ class FederateSLO(models.Model):
federate_slo.delete() federate_slo.delete()
@python_2_unicode_compatible
class User(models.Model): class User(models.Model):
"""A user logged into the CAS""" """A user logged into the CAS"""
class Meta: class Meta:
@ -117,7 +194,7 @@ class User(models.Model):
"""return a fresh dict for the user attributs""" """return a fresh dict for the user attributs"""
return utils.import_attr(settings.CAS_AUTH_CLASS)(self.username).attributs() return utils.import_attr(settings.CAS_AUTH_CLASS)(self.username).attributs()
def __unicode__(self): def __str__(self):
return u"%s - %s" % (self.username, self.session_key) return u"%s - %s" % (self.username, self.session_key)
def logout(self, request=None): def logout(self, request=None):
@ -222,6 +299,7 @@ class UserFieldNotDefined(ServicePatternException):
pass pass
@python_2_unicode_compatible
class ServicePattern(models.Model): class ServicePattern(models.Model):
"""Allowed services pattern agains services are tested to""" """Allowed services pattern agains services are tested to"""
class Meta: class Meta:
@ -231,7 +309,8 @@ class ServicePattern(models.Model):
pos = models.IntegerField( pos = models.IntegerField(
default=100, default=100,
verbose_name=_(u"position") verbose_name=_(u"position"),
help_text=_(u"service patterns are sorted using the position attribute")
) )
name = models.CharField( name = models.CharField(
max_length=255, max_length=255,
@ -288,7 +367,7 @@ class ServicePattern(models.Model):
u"This is usefull for non HTTP proxied services.") u"This is usefull for non HTTP proxied services.")
) )
def __unicode__(self): def __str__(self):
return u"%s: %s" % (self.pos, self.pattern) return u"%s: %s" % (self.pos, self.pattern)
def check_user(self, user): def check_user(self, user):
@ -341,6 +420,7 @@ class ServicePattern(models.Model):
raise cls.DoesNotExist() raise cls.DoesNotExist()
@python_2_unicode_compatible
class Username(models.Model): class Username(models.Model):
"""A list of allowed usernames on a service pattern""" """A list of allowed usernames on a service pattern"""
value = models.CharField( value = models.CharField(
@ -350,10 +430,11 @@ class Username(models.Model):
) )
service_pattern = models.ForeignKey(ServicePattern, related_name="usernames") service_pattern = models.ForeignKey(ServicePattern, related_name="usernames")
def __unicode__(self): def __str__(self):
return self.value return self.value
@python_2_unicode_compatible
class ReplaceAttributName(models.Model): class ReplaceAttributName(models.Model):
"""A list of replacement of attributs name for a service pattern""" """A list of replacement of attributs name for a service pattern"""
class Meta: class Meta:
@ -372,13 +453,14 @@ class ReplaceAttributName(models.Model):
) )
service_pattern = models.ForeignKey(ServicePattern, related_name="attributs") service_pattern = models.ForeignKey(ServicePattern, related_name="attributs")
def __unicode__(self): def __str__(self):
if not self.replace: if not self.replace:
return self.name return self.name
else: else:
return u"%s%s" % (self.name, self.replace) return u"%s%s" % (self.name, self.replace)
@python_2_unicode_compatible
class FilterAttributValue(models.Model): class FilterAttributValue(models.Model):
"""A list of filter on attributs for a service pattern""" """A list of filter on attributs for a service pattern"""
attribut = models.CharField( attribut = models.CharField(
@ -393,10 +475,11 @@ class FilterAttributValue(models.Model):
) )
service_pattern = models.ForeignKey(ServicePattern, related_name="filters") service_pattern = models.ForeignKey(ServicePattern, related_name="filters")
def __unicode__(self): def __str__(self):
return u"%s %s" % (self.attribut, self.pattern) return u"%s %s" % (self.attribut, self.pattern)
@python_2_unicode_compatible
class ReplaceAttributValue(models.Model): class ReplaceAttributValue(models.Model):
"""Replacement to apply on attributs values for a service pattern""" """Replacement to apply on attributs values for a service pattern"""
attribut = models.CharField( attribut = models.CharField(
@ -417,10 +500,11 @@ class ReplaceAttributValue(models.Model):
) )
service_pattern = models.ForeignKey(ServicePattern, related_name="replacements") service_pattern = models.ForeignKey(ServicePattern, related_name="replacements")
def __unicode__(self): def __str__(self):
return u"%s %s %s" % (self.attribut, self.pattern, self.replace) return u"%s %s %s" % (self.attribut, self.pattern, self.replace)
@python_2_unicode_compatible
class Ticket(models.Model): class Ticket(models.Model):
"""Generic class for a Ticket""" """Generic class for a Ticket"""
class Meta: class Meta:
@ -437,7 +521,7 @@ class Ticket(models.Model):
VALIDITY = settings.CAS_TICKET_VALIDITY VALIDITY = settings.CAS_TICKET_VALIDITY
TIMEOUT = settings.CAS_TICKET_TIMEOUT TIMEOUT = settings.CAS_TICKET_TIMEOUT
def __unicode__(self): def __str__(self):
return u"Ticket-%s" % self.pk return u"Ticket-%s" % self.pk
@classmethod @classmethod
@ -507,34 +591,38 @@ class Ticket(models.Model):
) )
@python_2_unicode_compatible
class ServiceTicket(Ticket): class ServiceTicket(Ticket):
"""A Service Ticket""" """A Service Ticket"""
PREFIX = settings.CAS_SERVICE_TICKET_PREFIX PREFIX = settings.CAS_SERVICE_TICKET_PREFIX
value = models.CharField(max_length=255, default=utils.gen_st, unique=True) value = models.CharField(max_length=255, default=utils.gen_st, unique=True)
def __unicode__(self): def __str__(self):
return u"ServiceTicket-%s" % self.pk return u"ServiceTicket-%s" % self.pk
@python_2_unicode_compatible
class ProxyTicket(Ticket): class ProxyTicket(Ticket):
"""A Proxy Ticket""" """A Proxy Ticket"""
PREFIX = settings.CAS_PROXY_TICKET_PREFIX PREFIX = settings.CAS_PROXY_TICKET_PREFIX
value = models.CharField(max_length=255, default=utils.gen_pt, unique=True) value = models.CharField(max_length=255, default=utils.gen_pt, unique=True)
def __unicode__(self): def __str__(self):
return u"ProxyTicket-%s" % self.pk return u"ProxyTicket-%s" % self.pk
@python_2_unicode_compatible
class ProxyGrantingTicket(Ticket): class ProxyGrantingTicket(Ticket):
"""A Proxy Granting Ticket""" """A Proxy Granting Ticket"""
PREFIX = settings.CAS_PROXY_GRANTING_TICKET_PREFIX PREFIX = settings.CAS_PROXY_GRANTING_TICKET_PREFIX
VALIDITY = settings.CAS_PGT_VALIDITY VALIDITY = settings.CAS_PGT_VALIDITY
value = models.CharField(max_length=255, default=utils.gen_pgt, unique=True) value = models.CharField(max_length=255, default=utils.gen_pgt, unique=True)
def __unicode__(self): def __str__(self):
return u"ProxyGrantingTicket-%s" % self.pk return u"ProxyGrantingTicket-%s" % self.pk
@python_2_unicode_compatible
class Proxy(models.Model): class Proxy(models.Model):
"""A list of proxies on `ProxyTicket`""" """A list of proxies on `ProxyTicket`"""
class Meta: class Meta:
@ -542,5 +630,5 @@ class Proxy(models.Model):
url = models.CharField(max_length=255) url = models.CharField(max_length=255)
proxy_ticket = models.ForeignKey(ProxyTicket, related_name="proxies") proxy_ticket = models.ForeignKey(ProxyTicket, related_name="proxies")
def __unicode__(self): def __str__(self):
return self.url return self.url

View file

@ -23,27 +23,28 @@ from cas_server.tests.utils import get_auth_client
class BaseServicePattern(object): class BaseServicePattern(object):
"""Mixing for setting up service pattern for testing""" """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""" """setting up service pattern"""
# For general purpose testing # For general purpose testing
self.service = "https://www.example.com" cls.service = "https://www.example.com"
self.service_pattern = models.ServicePattern.objects.create( cls.service_pattern = models.ServicePattern.objects.create(
name="example", name="example",
pattern="^https://www\.example\.com(/.*)?$", pattern="^https://www\.example\.com(/.*)?$",
proxy=proxy, 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 # For testing the restrict_users attributes
self.service_restrict_user_fail = "https://restrict_user_fail.example.com" cls.service_restrict_user_fail = "https://restrict_user_fail.example.com"
self.service_pattern_restrict_user_fail = models.ServicePattern.objects.create( cls.service_pattern_restrict_user_fail = models.ServicePattern.objects.create(
name="restrict_user_fail", name="restrict_user_fail",
pattern="^https://restrict_user_fail\.example\.com(/.*)?$", pattern="^https://restrict_user_fail\.example\.com(/.*)?$",
restrict_users=True, restrict_users=True,
proxy=proxy, proxy=proxy,
) )
self.service_restrict_user_success = "https://restrict_user_success.example.com" cls.service_restrict_user_success = "https://restrict_user_success.example.com"
self.service_pattern_restrict_user_success = models.ServicePattern.objects.create( cls.service_pattern_restrict_user_success = models.ServicePattern.objects.create(
name="restrict_user_success", name="restrict_user_success",
pattern="^https://restrict_user_success\.example\.com(/.*)?$", pattern="^https://restrict_user_success\.example\.com(/.*)?$",
restrict_users=True, restrict_users=True,
@ -51,12 +52,12 @@ class BaseServicePattern(object):
) )
models.Username.objects.create( models.Username.objects.create(
value=settings.CAS_TEST_USER, 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 # For testing the user attributes filtering conditions
self.service_filter_fail = "https://filter_fail.example.com" cls.service_filter_fail = "https://filter_fail.example.com"
self.service_pattern_filter_fail = models.ServicePattern.objects.create( cls.service_pattern_filter_fail = models.ServicePattern.objects.create(
name="filter_fail", name="filter_fail",
pattern="^https://filter_fail\.example\.com(/.*)?$", pattern="^https://filter_fail\.example\.com(/.*)?$",
proxy=proxy, proxy=proxy,
@ -64,10 +65,10 @@ class BaseServicePattern(object):
models.FilterAttributValue.objects.create( models.FilterAttributValue.objects.create(
attribut="right", attribut="right",
pattern="^admin$", 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" cls.service_filter_fail_alt = "https://filter_fail_alt.example.com"
self.service_pattern_filter_fail_alt = models.ServicePattern.objects.create( cls.service_pattern_filter_fail_alt = models.ServicePattern.objects.create(
name="filter_fail_alt", name="filter_fail_alt",
pattern="^https://filter_fail_alt\.example\.com(/.*)?$", pattern="^https://filter_fail_alt\.example\.com(/.*)?$",
proxy=proxy, proxy=proxy,
@ -75,10 +76,10 @@ class BaseServicePattern(object):
models.FilterAttributValue.objects.create( models.FilterAttributValue.objects.create(
attribut="nom", attribut="nom",
pattern="^toto$", 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" cls.service_filter_success = "https://filter_success.example.com"
self.service_pattern_filter_success = models.ServicePattern.objects.create( cls.service_pattern_filter_success = models.ServicePattern.objects.create(
name="filter_success", name="filter_success",
pattern="^https://filter_success\.example\.com(/.*)?$", pattern="^https://filter_success\.example\.com(/.*)?$",
proxy=proxy, proxy=proxy,
@ -86,26 +87,26 @@ class BaseServicePattern(object):
models.FilterAttributValue.objects.create( models.FilterAttributValue.objects.create(
attribut="email", attribut="email",
pattern="^%s$" % re.escape(settings.CAS_TEST_ATTRIBUTES['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 # For testing the user_field attributes
self.service_field_needed_fail = "https://field_needed_fail.example.com" cls.service_field_needed_fail = "https://field_needed_fail.example.com"
self.service_pattern_field_needed_fail = models.ServicePattern.objects.create( cls.service_pattern_field_needed_fail = models.ServicePattern.objects.create(
name="field_needed_fail", name="field_needed_fail",
pattern="^https://field_needed_fail\.example\.com(/.*)?$", pattern="^https://field_needed_fail\.example\.com(/.*)?$",
user_field="uid", user_field="uid",
proxy=proxy, proxy=proxy,
) )
self.service_field_needed_success = "https://field_needed_success.example.com" cls.service_field_needed_success = "https://field_needed_success.example.com"
self.service_pattern_field_needed_success = models.ServicePattern.objects.create( cls.service_pattern_field_needed_success = models.ServicePattern.objects.create(
name="field_needed_success", name="field_needed_success",
pattern="^https://field_needed_success\.example\.com(/.*)?$", pattern="^https://field_needed_success\.example\.com(/.*)?$",
user_field="alias", user_field="alias",
proxy=proxy, proxy=proxy,
) )
self.service_field_needed_success_alt = "https://field_needed_success_alt.example.com" cls.service_field_needed_success_alt = "https://field_needed_success_alt.example.com"
self.service_pattern_field_needed_success = models.ServicePattern.objects.create( cls.service_pattern_field_needed_success = models.ServicePattern.objects.create(
name="field_needed_success_alt", name="field_needed_success_alt",
pattern="^https://field_needed_success_alt\.example\.com(/.*)?$", pattern="^https://field_needed_success_alt\.example\.com(/.*)?$",
user_field="nom", user_field="nom",
@ -238,3 +239,17 @@ class CanLogin(object):
self.assertTrue(client.session.get("username") is None) self.assertTrue(client.session.get("username") is None)
self.assertTrue(client.session.get("warn") is None) self.assertTrue(client.session.get("warn") is None)
self.assertTrue(client.session.get("authenticated") is None) self.assertTrue(client.session.get("authenticated") is None)
class FederatedIendityProviderModel(object):
"""Mixin for test classes using the FederatedIendityProvider model"""
@staticmethod
def setup_federated_identity_provider(providers):
"""setting up federated identity providers"""
for suffix, (server_url, cas_protocol_version, verbose_name) in providers.items():
models.FederatedIendityProvider.objects.create(
suffix=suffix,
server_url=server_url,
cas_protocol_version=cas_protocol_version,
verbose_name=verbose_name
)

View file

@ -19,43 +19,37 @@ from django.test.utils import override_settings
from six.moves import reload_module from six.moves import reload_module
from cas_server import utils, forms from cas_server import utils, models
from cas_server.tests.mixin import BaseServicePattern, CanLogin from cas_server.tests.mixin import BaseServicePattern, CanLogin, FederatedIendityProviderModel
from cas_server.tests import utils as tests_utils from cas_server.tests import utils as tests_utils
PROVIDERS = { PROVIDERS = {
"example.com": ("http://127.0.0.1:8080", 1, "Example dot com"), "example.com": ("http://127.0.0.1:8080", '1', "Example dot com"),
"example.org": ("http://127.0.0.1:8081", 2, "Example dot org"), "example.org": ("http://127.0.0.1:8081", '2', "Example dot org"),
"example.net": ("http://127.0.0.1:8082", 3, "Example dot net"), "example.net": ("http://127.0.0.1:8082", '3', "Example dot net"),
"example.test": ("http://127.0.0.1:8083", 'CAS_2_SAML_1_0'), "example.test": ("http://127.0.0.1:8083", 'CAS_2_SAML_1_0', 'Example fot test'),
} }
PROVIDERS_LIST = list(PROVIDERS.keys())
PROVIDERS_LIST.sort()
@override_settings( @override_settings(
CAS_FEDERATE=True, CAS_FEDERATE=True,
CAS_FEDERATE_PROVIDERS=PROVIDERS,
CAS_FEDERATE_PROVIDERS_LIST=PROVIDERS_LIST,
CAS_AUTH_CLASS="cas_server.auth.CASFederateAuth", CAS_AUTH_CLASS="cas_server.auth.CASFederateAuth",
# test with a non ascii username # test with a non ascii username
CAS_TEST_USER=u"dédé" CAS_TEST_USER=u"dédé"
) )
class FederateAuthLoginLogoutTestCase(TestCase, BaseServicePattern, CanLogin): class FederateAuthLoginLogoutTestCase(
TestCase, BaseServicePattern, CanLogin, FederatedIendityProviderModel
):
"""tests for the views login logout and federate then the federated mode is enabled""" """tests for the views login logout and federate then the federated mode is enabled"""
def setUp(self): def setUp(self):
"""Prepare the test context""" """Prepare the test context"""
self.setup_service_patterns() self.setup_service_patterns()
reload_module(forms) self.setup_federated_identity_provider(PROVIDERS)
def test_default_settings(self): def test_default_settings(self):
"""default settings should populated some default variable then CAS_FEDERATE is True""" """default settings should populated some default variable then CAS_FEDERATE is True"""
provider_list = settings.CAS_FEDERATE_PROVIDERS_LIST
del settings.CAS_FEDERATE_PROVIDERS_LIST
del settings.CAS_AUTH_CLASS del settings.CAS_AUTH_CLASS
reload_module(default_settings) reload_module(default_settings)
self.assertEqual(settings.CAS_FEDERATE_PROVIDERS_LIST, provider_list)
self.assertEqual(settings.CAS_AUTH_CLASS, "cas_server.auth.CASFederateAuth") self.assertEqual(settings.CAS_AUTH_CLASS, "cas_server.auth.CASFederateAuth")
def test_login_get_provider(self): def test_login_get_provider(self):
@ -63,10 +57,10 @@ class FederateAuthLoginLogoutTestCase(TestCase, BaseServicePattern, CanLogin):
client = Client() client = Client()
response = client.get("/login") response = client.get("/login")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
for key, value in settings.CAS_FEDERATE_PROVIDERS.items(): for provider in models.FederatedIendityProvider.objects.all():
self.assertTrue('<option value="%s">%s</option>' % ( self.assertTrue('<option value="%s">%s</option>' % (
key, provider.suffix,
utils.get_tuple(value, 2, key) provider.verbose_name
) in response.content.decode("utf-8")) ) in response.content.decode("utf-8"))
self.assertEqual(response.context['post_url'], '/federate') self.assertEqual(response.context['post_url'], '/federate')
@ -74,10 +68,11 @@ class FederateAuthLoginLogoutTestCase(TestCase, BaseServicePattern, CanLogin):
"""test a successful login wrokflow""" """test a successful login wrokflow"""
tickets = [] tickets = []
# choose the example.com provider # choose the example.com provider
for (provider, cas_port) in [ for (suffix, cas_port) in [
("example.com", 8080), ("example.org", 8081), ("example.com", 8080), ("example.org", 8081),
("example.net", 8082), ("example.test", 8083) ("example.net", 8082), ("example.test", 8083)
]: ]:
provider = models.FederatedIendityProvider.objects.get(suffix=suffix)
# get a bare client # get a bare client
client = Client() client = Client()
# fetch the login page # fetch the login page
@ -86,7 +81,7 @@ class FederateAuthLoginLogoutTestCase(TestCase, BaseServicePattern, CanLogin):
self.assertEqual(response.context['post_url'], '/federate') self.assertEqual(response.context['post_url'], '/federate')
# get current form parameter # get current form parameter
params = tests_utils.copy_form(response.context["form"]) params = tests_utils.copy_form(response.context["form"])
params['provider'] = provider params['provider'] = provider.suffix
if remember: if remember:
params['remember'] = 'on' params['remember'] = 'on'
# post the choosed provider # post the choosed provider
@ -96,22 +91,22 @@ class FederateAuthLoginLogoutTestCase(TestCase, BaseServicePattern, CanLogin):
if remember: if remember:
self.assertEqual(response["Location"], '%s/federate/%s?remember=on' % ( self.assertEqual(response["Location"], '%s/federate/%s?remember=on' % (
'http://testserver' if django.VERSION < (1, 9) else "", 'http://testserver' if django.VERSION < (1, 9) else "",
provider provider.suffix
)) ))
else: else:
self.assertEqual(response["Location"], '%s/federate/%s' % ( self.assertEqual(response["Location"], '%s/federate/%s' % (
'http://testserver' if django.VERSION < (1, 9) else "", 'http://testserver' if django.VERSION < (1, 9) else "",
provider provider.suffix
)) ))
# let's follow the redirect # let's follow the redirect
response = client.get('/federate/%s' % provider) response = client.get('/federate/%s' % provider.suffix)
# we are redirected to the provider CAS for authentication # we are redirected to the provider CAS for authentication
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual( self.assertEqual(
response["Location"], response["Location"],
"%s/login?service=http%%3A%%2F%%2Ftestserver%%2Ffederate%%2F%s" % ( "%s/login?service=http%%3A%%2F%%2Ftestserver%%2Ffederate%%2F%s" % (
settings.CAS_FEDERATE_PROVIDERS[provider][0], provider.server_url,
provider provider.suffix
) )
) )
# let's generate a ticket # let's generate a ticket
@ -119,7 +114,7 @@ class FederateAuthLoginLogoutTestCase(TestCase, BaseServicePattern, CanLogin):
# we lauch a dummy CAS server that only validate once for the service # we lauch a dummy CAS server that only validate once for the service
# http://testserver/federate/example.com with `ticket` # http://testserver/federate/example.com with `ticket`
tests_utils.DummyCAS.run( tests_utils.DummyCAS.run(
("http://testserver/federate/%s" % provider).encode("ascii"), ("http://testserver/federate/%s" % provider.suffix).encode("ascii"),
ticket.encode("ascii"), ticket.encode("ascii"),
settings.CAS_TEST_USER.encode("utf8"), settings.CAS_TEST_USER.encode("utf8"),
[], [],
@ -127,7 +122,7 @@ class FederateAuthLoginLogoutTestCase(TestCase, BaseServicePattern, CanLogin):
) )
# we normally provide a good ticket and should be redirected to /login as the ticket # we normally provide a good ticket and should be redirected to /login as the ticket
# get successfully validated again the dummy CAS # get successfully validated again the dummy CAS
response = client.get('/federate/%s' % provider, {'ticket': ticket}) response = client.get('/federate/%s' % provider.suffix, {'ticket': ticket})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response["Location"], "%s/login" % ( self.assertEqual(response["Location"], "%s/login" % (
'http://testserver' if django.VERSION < (1, 9) else "" 'http://testserver' if django.VERSION < (1, 9) else ""
@ -143,7 +138,7 @@ class FederateAuthLoginLogoutTestCase(TestCase, BaseServicePattern, CanLogin):
response = client.post("/login", params) response = client.post("/login", params)
# the user should now being authenticated using username test@`provider` # the user should now being authenticated using username test@`provider`
self.assert_logged( self.assert_logged(
client, response, username='%s@%s' % (settings.CAS_TEST_USER, provider) client, response, username=provider.build_username(settings.CAS_TEST_USER)
) )
tickets.append((provider, ticket, client)) tickets.append((provider, ticket, client))
@ -198,7 +193,7 @@ class FederateAuthLoginLogoutTestCase(TestCase, BaseServicePattern, CanLogin):
self.assertEqual( self.assertEqual(
response["Location"], response["Location"],
"%s/login?service=http%%3A%%2F%%2Ftestserver%%2Ffederate%%2F%s" % ( "%s/login?service=http%%3A%%2F%%2Ftestserver%%2Ffederate%%2F%s" % (
settings.CAS_FEDERATE_PROVIDERS[good_provider][0], models.FederatedIendityProvider.objects.get(suffix=good_provider).server_url,
good_provider good_provider
) )
) )
@ -216,7 +211,7 @@ class FederateAuthLoginLogoutTestCase(TestCase, BaseServicePattern, CanLogin):
self.assertEqual( self.assertEqual(
response["Location"], response["Location"],
"%s/login?service=http%%3A%%2F%%2Ftestserver%%2Ffederate%%2F%s" % ( "%s/login?service=http%%3A%%2F%%2Ftestserver%%2Ffederate%%2F%s" % (
settings.CAS_FEDERATE_PROVIDERS[good_provider][0], models.FederatedIendityProvider.objects.get(suffix=good_provider).server_url,
good_provider good_provider
) )
) )
@ -234,45 +229,45 @@ class FederateAuthLoginLogoutTestCase(TestCase, BaseServicePattern, CanLogin):
for (provider, ticket, client) in tickets: for (provider, ticket, client) in tickets:
# 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, "/federate/%s" % provider.suffix,
{'logoutRequest': tests_utils.logout_request(utils.gen_st())} {'logoutRequest': tests_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")
# Bad SLO format should do nothing # Bad SLO format should do nothing
response = client.post( response = client.post(
"/federate/%s" % provider, "/federate/%s" % provider.suffix,
{'logoutRequest': ""} {'logoutRequest': ""}
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b"ok") self.assertEqual(response.content, b"ok")
# Bad SLO format should do nothing # Bad SLO format should do nothing
response = client.post( response = client.post(
"/federate/%s" % provider, "/federate/%s" % provider.suffix,
{'logoutRequest': "<root></root>"} {'logoutRequest': "<root></root>"}
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b"ok") self.assertEqual(response.content, b"ok")
response = client.get("/login") response = client.get("/login")
self.assert_logged( self.assert_logged(
client, response, username='%s@%s' % (settings.CAS_TEST_USER, provider) client, response, username=provider.build_username(settings.CAS_TEST_USER)
) )
# SLO for a previously logged ticket should log out the user if CAS version is # SLO for a previously logged ticket should log out the user if CAS version is
# 3 or 'CAS_2_SAML_1_0' # 3 or 'CAS_2_SAML_1_0'
response = client.post( response = client.post(
"/federate/%s" % provider, "/federate/%s" % provider.suffix,
{'logoutRequest': tests_utils.logout_request(ticket)} {'logoutRequest': tests_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")
response = client.get("/login") response = client.get("/login")
if settings.CAS_FEDERATE_PROVIDERS[provider][1] in {3, 'CAS_2_SAML_1_0'}: # support SLO if provider.cas_protocol_version in {'3', 'CAS_2_SAML_1_0'}: # support SLO
self.assert_login_failed(client, response) self.assert_login_failed(client, response)
else: else:
self.assert_logged( self.assert_logged(
client, response, username='%s@%s' % (settings.CAS_TEST_USER, provider) client, response, username=provider.build_username(settings.CAS_TEST_USER)
) )
def test_federate_logout(self): def test_federate_logout(self):
@ -287,7 +282,7 @@ class FederateAuthLoginLogoutTestCase(TestCase, BaseServicePattern, CanLogin):
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual( self.assertEqual(
response["Location"], response["Location"],
"%s/logout" % settings.CAS_FEDERATE_PROVIDERS[provider][0] "%s/logout" % provider.server_url,
) )
response = client.get("/login") response = client.get("/login")
self.assert_login_failed(client, response) self.assert_login_failed(client, response)
@ -326,7 +321,7 @@ class FederateAuthLoginLogoutTestCase(TestCase, BaseServicePattern, CanLogin):
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response["Location"], "%s/federate/%s" % ( self.assertEqual(response["Location"], "%s/federate/%s" % (
'http://testserver' if django.VERSION < (1, 9) else "", 'http://testserver' if django.VERSION < (1, 9) else "",
provider provider.suffix
)) ))
def test_login_bad_ticket(self): def test_login_bad_ticket(self):
@ -338,7 +333,10 @@ class FederateAuthLoginLogoutTestCase(TestCase, BaseServicePattern, CanLogin):
# get a bare client # get a bare client
client = Client() client = Client()
session = client.session session = client.session
session["federate_username"] = '%s@%s' % (settings.CAS_TEST_USER, provider) session["federate_username"] = models.FederatedIendityProvider.build_username_from_suffix(
settings.CAS_TEST_USER,
provider
)
session["federate_ticket"] = utils.gen_st() session["federate_ticket"] = utils.gen_st()
if django.VERSION >= (1, 8): if django.VERSION >= (1, 8):
session.save() session.save()
@ -351,9 +349,12 @@ class FederateAuthLoginLogoutTestCase(TestCase, BaseServicePattern, CanLogin):
# POST, as (username, ticket) are not valid, we should get the federate login page # POST, as (username, ticket) are not valid, we should get the federate login page
response = client.post("/login", params) response = client.post("/login", params)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
for key, value in settings.CAS_FEDERATE_PROVIDERS.items(): for provider in models.FederatedIendityProvider.objects.all():
self.assertTrue('<option value="%s">%s</option>' % ( self.assertIn(
key, '<option value="%s">%s</option>' % (
utils.get_tuple(value, 2, key) provider.suffix,
) in response.content.decode("utf-8")) provider.verbose_name
),
response.content.decode("utf-8")
)
self.assertEqual(response.context['post_url'], '/federate') self.assertEqual(response.context['post_url'], '/federate')

View file

@ -22,32 +22,39 @@ from importlib import import_module
from cas_server import models, utils from cas_server import models, utils
from cas_server.tests.utils import get_auth_client, HttpParamsHandler from cas_server.tests.utils import get_auth_client, HttpParamsHandler
from cas_server.tests.mixin import UserModels, BaseServicePattern from cas_server.tests.mixin import UserModels, BaseServicePattern, FederatedIendityProviderModel
from cas_server.tests.test_federate import PROVIDERS
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
class FederatedUserTestCase(TestCase, UserModels): class FederatedUserTestCase(TestCase, UserModels, FederatedIendityProviderModel):
"""test for the federated user model""" """test for the federated user model"""
def setUp(self):
"""Prepare the test context"""
self.setup_federated_identity_provider(PROVIDERS)
def test_clean_old_entries(self): def test_clean_old_entries(self):
"""tests for clean_old_entries that should delete federated user no longer used""" """tests for clean_old_entries that should delete federated user no longer used"""
client = Client() client = Client()
client.get("/login") client.get("/login")
provider = models.FederatedIendityProvider.objects.get(suffix="example.com")
models.FederatedUser.objects.create( models.FederatedUser.objects.create(
username="test1", provider="example.com", attributs={}, ticket="" username="test1", provider=provider, attributs={}, ticket=""
) )
models.FederatedUser.objects.create( models.FederatedUser.objects.create(
username="test2", provider="example.com", attributs={}, ticket="" username="test2", provider=provider, attributs={}, ticket=""
) )
models.FederatedUser.objects.all().update( models.FederatedUser.objects.all().update(
last_update=(timezone.now() - timedelta(seconds=settings.CAS_TICKET_TIMEOUT + 10)) last_update=(timezone.now() - timedelta(seconds=settings.CAS_TICKET_TIMEOUT + 10))
) )
models.FederatedUser.objects.create( models.FederatedUser.objects.create(
username="test3", provider="example.com", attributs={}, ticket="" username="test3", provider=provider, attributs={}, ticket=""
) )
models.User.objects.create( models.User.objects.create(
username="test1@example.com", session_key=client.session.session_key username="test1@example.com", session_key=client.session.session_key
) )
self.assertEqual(len(models.FederatedUser.objects.all()), 3)
models.FederatedUser.clean_old_entries() models.FederatedUser.clean_old_entries()
self.assertEqual(len(models.FederatedUser.objects.all()), 2) self.assertEqual(len(models.FederatedUser.objects.all()), 2)
with self.assertRaises(models.FederatedUser.DoesNotExist): with self.assertRaises(models.FederatedUser.DoesNotExist):

View file

@ -22,6 +22,7 @@ from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.middleware.csrf import CsrfViewMiddleware from django.middleware.csrf import CsrfViewMiddleware
from django.views.generic import View from django.views.generic import View
from django.utils.encoding import python_2_unicode_compatible
import re import re
import logging import logging
@ -37,7 +38,7 @@ import cas_server.models as models
from .utils import json_response from .utils import json_response
from .models import ServiceTicket, ProxyTicket, ProxyGrantingTicket from .models import ServiceTicket, ProxyTicket, ProxyGrantingTicket
from .models import ServicePattern from .models import ServicePattern, FederatedIendityProvider, FederatedUser
from .federate import CASFederateValidateUser from .federate import CASFederateValidateUser
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
@ -123,11 +124,12 @@ class LogoutView(View, LogoutMixin):
self.init_get(request) self.init_get(request)
# if CAS federation mode is enable, bakup the provider before flushing the sessions # if CAS federation mode is enable, bakup the provider before flushing the sessions
if settings.CAS_FEDERATE: if settings.CAS_FEDERATE:
if "username" in self.request.session: try:
component = self.request.session["username"].split('@') user = FederatedUser.get_from_federated_username(
provider = component[-1] self.request.session.get("username")
auth = CASFederateValidateUser(provider, service_url="") )
else: auth = CASFederateValidateUser(user.provider, service_url="")
except FederatedUser.DoesNotExist:
auth = None auth = None
session_nb = self.logout(self.request.GET.get("all")) session_nb = self.logout(self.request.GET.get("all"))
# if CAS federation mode is enable, redirect to user CAS logout page # if CAS federation mode is enable, redirect to user CAS logout page
@ -135,7 +137,6 @@ class LogoutView(View, LogoutMixin):
if auth is not None: if auth is not None:
params = utils.copy_params(request.GET) params = utils.copy_params(request.GET)
url = auth.get_logout_url() url = auth.get_logout_url()
if url:
return HttpResponseRedirect(utils.update_url(url, params)) return HttpResponseRedirect(utils.update_url(url, params))
# if service is set, redirect to service after logout # if service is set, redirect to service after logout
if self.service: if self.service:
@ -201,7 +202,6 @@ class FederateAuth(View):
@staticmethod @staticmethod
def get_cas_client(request, provider): def get_cas_client(request, provider):
"""return a CAS client object matching provider""" """return a CAS client object matching provider"""
if provider in settings.CAS_FEDERATE_PROVIDERS: # pragma: no branch (should always be true)
service_url = utils.get_current_url(request, {"ticket", "provider"}) service_url = utils.get_current_url(request, {"ticket", "provider"})
return CASFederateValidateUser(provider, service_url) return CASFederateValidateUser(provider, service_url)
@ -210,7 +210,8 @@ class FederateAuth(View):
if not settings.CAS_FEDERATE: if not settings.CAS_FEDERATE:
return redirect("cas_server:login") return redirect("cas_server:login")
# POST with a provider, this is probably an SLO request # POST with a provider, this is probably an SLO request
if provider in settings.CAS_FEDERATE_PROVIDERS: try:
provider = FederatedIendityProvider.objects.get(suffix=provider)
auth = self.get_cas_client(request, provider) auth = self.get_cas_client(request, provider)
try: try:
auth.clean_sessions(request.POST['logoutRequest']) auth.clean_sessions(request.POST['logoutRequest'])
@ -218,7 +219,7 @@ class FederateAuth(View):
pass pass
return HttpResponse("ok") return HttpResponse("ok")
# else, a User is trying to log in using an identity provider # else, a User is trying to log in using an identity provider
else: except FederatedIendityProvider.DoesNotExist:
# Manually checking for csrf to protect the code below # Manually checking for csrf to protect the code below
reason = CsrfViewMiddleware().process_view(request, None, (), {}) reason = CsrfViewMiddleware().process_view(request, None, (), {})
if reason is not None: # pragma: no cover (csrf checks are disabled during tests) if reason is not None: # pragma: no cover (csrf checks are disabled during tests)
@ -231,7 +232,7 @@ class FederateAuth(View):
) )
url = utils.reverse_params( url = utils.reverse_params(
"cas_server:federateAuth", "cas_server:federateAuth",
kwargs=dict(provider=form.cleaned_data["provider"]), kwargs=dict(provider=form.cleaned_data["provider"].suffix),
params=params params=params
) )
response = HttpResponseRedirect(url) response = HttpResponseRedirect(url)
@ -240,7 +241,7 @@ class FederateAuth(View):
utils.set_cookie( utils.set_cookie(
response, response,
"_remember_provider", "_remember_provider",
request.POST["provider"], form.cleaned_data["provider"].suffix,
max_age max_age
) )
return response return response
@ -251,8 +252,8 @@ class FederateAuth(View):
"""method called on GET request""" """method called on GET request"""
if not settings.CAS_FEDERATE: if not settings.CAS_FEDERATE:
return redirect("cas_server:login") return redirect("cas_server:login")
if provider not in settings.CAS_FEDERATE_PROVIDERS: try:
return redirect("cas_server:login") provider = FederatedIendityProvider.objects.get(suffix=provider)
auth = self.get_cas_client(request, provider) auth = self.get_cas_client(request, provider)
if 'ticket' not in request.GET: if 'ticket' not in request.GET:
return HttpResponseRedirect(auth.get_login_url()) return HttpResponseRedirect(auth.get_login_url())
@ -260,14 +261,15 @@ class FederateAuth(View):
ticket = request.GET['ticket'] ticket = request.GET['ticket']
if auth.verify_ticket(ticket): if auth.verify_ticket(ticket):
params = utils.copy_params(request.GET, ignore={"ticket"}) params = utils.copy_params(request.GET, ignore={"ticket"})
username = u"%s@%s" % (auth.username, auth.provider) request.session["federate_username"] = auth.federated_username
request.session["federate_username"] = username
request.session["federate_ticket"] = ticket request.session["federate_ticket"] = ticket
auth.register_slo(username, request.session.session_key, ticket) auth.register_slo(auth.federated_username, request.session.session_key, ticket)
url = utils.reverse_params("cas_server:login", params) url = utils.reverse_params("cas_server:login", params)
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
else: else:
return HttpResponseRedirect(auth.get_login_url()) return HttpResponseRedirect(auth.get_login_url())
except FederatedIendityProvider.DoesNotExist:
return redirect("cas_server:login")
class LoginView(View, LogoutMixin): class LoginView(View, LogoutMixin):
@ -347,17 +349,10 @@ class LoginView(View, LogoutMixin):
_(u"Invalid login ticket") _(u"Invalid login ticket")
) )
elif ret == self.USER_LOGIN_OK: elif ret == self.USER_LOGIN_OK:
try: self.user = models.User.objects.get_or_create(
self.user = models.User.objects.get(
username=self.request.session['username'], username=self.request.session['username'],
session_key=self.request.session.session_key session_key=self.request.session.session_key
) )[0]
self.user.save() # pragma: no cover (should not happend)
except models.User.DoesNotExist:
self.user = models.User.objects.create(
username=self.request.session['username'],
session_key=self.request.session.session_key
)
self.user.save() self.user.save()
elif ret == self.USER_LOGIN_FAILURE: # bad user login elif ret == self.USER_LOGIN_FAILURE: # bad user login
if settings.CAS_FEDERATE: if settings.CAS_FEDERATE:
@ -639,8 +634,9 @@ class LoginView(View, LogoutMixin):
else: else:
if ( if (
self.request.COOKIES.get('_remember_provider') and self.request.COOKIES.get('_remember_provider') and
self.request.COOKIES['_remember_provider'] in FederatedIendityProvider.objects.filter(
settings.CAS_FEDERATE_PROVIDERS suffix=self.request.COOKIES['_remember_provider']
)
): ):
params = utils.copy_params(self.request.GET) params = utils.copy_params(self.request.GET)
url = utils.reverse_params( url = utils.reverse_params(
@ -708,16 +704,10 @@ class Auth(View):
) )
if form.is_valid(): if form.is_valid():
try: try:
try: user = models.User.objects.get_or_create(
user = models.User.objects.get(
username=form.cleaned_data['username'], username=form.cleaned_data['username'],
session_key=request.session.session_key session_key=request.session.session_key
) )[0]
except models.User.DoesNotExist:
user = models.User.objects.create(
username=form.cleaned_data['username'],
session_key=request.session.session_key
)
user.save() user.save()
# is the service allowed # is the service allowed
service_pattern = ServicePattern.validate(service) service_pattern = ServicePattern.validate(service)
@ -789,6 +779,7 @@ class Validate(View):
return HttpResponse(u"no\n", content_type="text/plain; charset=utf-8") return HttpResponse(u"no\n", content_type="text/plain; charset=utf-8")
@python_2_unicode_compatible
class ValidateError(Exception): class ValidateError(Exception):
"""handle service validation error""" """handle service validation error"""
def __init__(self, code, msg=""): def __init__(self, code, msg=""):
@ -796,7 +787,7 @@ class ValidateError(Exception):
self.msg = msg self.msg = msg
super(ValidateError, self).__init__(code) super(ValidateError, self).__init__(code)
def __unicode__(self): def __str__(self):
return u"%s" % self.msg return u"%s" % self.msg
def render(self, request): def render(self, request):
@ -1039,6 +1030,7 @@ class Proxy(View):
) )
@python_2_unicode_compatible
class SamlValidateError(Exception): class SamlValidateError(Exception):
"""handle saml validation error""" """handle saml validation error"""
def __init__(self, code, msg=""): def __init__(self, code, msg=""):
@ -1046,7 +1038,7 @@ class SamlValidateError(Exception):
self.msg = msg self.msg = msg
super(SamlValidateError, self).__init__(code) super(SamlValidateError, self).__init__(code)
def __unicode__(self): def __str__(self):
return u"%s" % self.msg return u"%s" % self.msg
def render(self, request): def render(self, request):