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
def __repr__
def __unicode__
def __str__
raise AssertionError
raise NotImplementedError
if six.PY3:

2
.gitignore vendored
View file

@ -15,3 +15,5 @@ coverage.xml
test_venv
.coverage
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).
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
provider" expire. The default is ``604800``, one week. The cookie is called
``_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
services using ``django-cas-server`` as CAS.
The list of allowed identity providers is defined using the ``CAS_FEDERATE_PROVIDERS`` parameter.
For instance:
The list of allowed identity providers is defined using the django admin application.
With the development server started, visit http://127.0.0.1:8000/admin/ to add identity providers.
.. code-block:: python
An identity provider comes with 5 fields:
CAS_FEDERATE_PROVIDERS = {
"example.com": ("https://cas.example.com", 3, "Example dot com"),
"exemple.fr": ("https://cas.exemple.fr", 3, "Exemple point fr"),
}
* `Position`: an integer used to tweak the order in which identity providers are displayed on
the login page. Identity providers are sorted using position first, then, on equal position,
using `verbose name` and then, on equal `verbose name`, using `suffix`.
* `Suffix`: the suffix that will be append to the username returned by the identity provider.
It must be unique.
* `Server url`: the url to the identity provider CAS. For instance, if you are using
`https://cas.example.org/login` to authenticate on the CAS, the `server url` is
`https://cas.example.org`
* `CAS protocol version`: the version of the CAS protocol to use to contact the identity provider.
The default is version 3.
* `Verbose name`: the name used on the login page to display the identity provider.
``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:
``provider_returned_username@provider_name``.
You can choose the provider returned username for ``django-cas-server`` and the provider name
in order to make sense.
The "provider verbose name" is showed on the select menu of the login page.
``provider_returned_username@provider_suffix``.
Choose the provider returned username for ``django-cas-server`` and the provider suffix
in order to make sense, as this built username is likely to be displayed to end users in
applications.
Then using federate mode, you should add one command to a daily crontab: ``cas_clean_federate``.

View file

@ -12,6 +12,7 @@
from django.contrib import admin
from .models import ServiceTicket, ProxyTicket, ProxyGrantingTicket, User, ServicePattern
from .models import Username, ReplaceAttributName, ReplaceAttributValue, FilterAttributValue
from .models import FederatedIendityProvider
from .forms import TicketForm
TICKETS_READONLY_FIELDS = ('validate', 'service', 'service_pattern',
@ -91,5 +92,10 @@ class ServicePatternAdmin(admin.ModelAdmin):
'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(ServicePattern, ServicePatternAdmin)
admin.site.register(FederatedIendityProvider, FederatedIendityProviderAdmin)

View file

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

View file

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

View file

@ -33,16 +33,14 @@ class FederateSelect(forms.Form):
Form used on the login page when CAS_FEDERATE is True
allowing the user to choose a identity provider.
"""
provider = forms.ChoiceField(
provider = forms.ModelChoiceField(
queryset=models.FederatedIendityProvider.objects.all().order_by(
"pos",
"verbose_name",
"suffix"
),
to_field_name="suffix",
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)
method = forms.CharField(widget=forms.HiddenInput(), required=False)
@ -88,13 +86,10 @@ class FederateUserCredential(UserCredential):
def clean(self):
cleaned_data = super(FederateUserCredential, self).clean()
try:
component = cleaned_data["username"].split('@')
username = '@'.join(component[:-1])
provider = component[-1]
user = models.FederatedUser.objects.get(username=username, provider=provider)
user = models.FederatedUser.get_from_federated_username(cleaned_data["username"])
user.ticket = ""
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")
except models.FederatedUser.DoesNotExist: # pragma: no cover (should not happend)
raise forms.ValidationError(

View file

@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: cas_server\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-06-21 00:14+0200\n"
"PO-Revision-Date: 2016-06-21 00:16+0200\n"
"POT-Creation-Date: 2016-07-04 17:15+0200\n"
"PO-Revision-Date: 2016-07-04 17:15+0200\n"
"Last-Translator: Valentin Samir <valentin.samir@crans.org>\n"
"Language-Team: django <LL@li.org>\n"
"Language: en\n"
@ -17,88 +17,135 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\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"
msgstr "Central Authentication Service"
#: forms.py:32
#: forms.py:43
msgid "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"
msgstr ""
#: forms.py:37
#: forms.py:47
msgid "Remember the identity provider"
msgstr "Remember the identity provider"
#: forms.py:38 forms.py:48
#: forms.py:48 forms.py:59
msgid "warn"
msgstr " Warn me before logging me into other sites."
#: forms.py:43
#: forms.py:54
msgid "login"
msgstr "username"
#: forms.py:45
#: forms.py:56
msgid "password"
msgstr "password"
#: forms.py:59
#: forms.py:71
msgid "Bad user"
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"
msgstr "Clean old federated users"
#: management/commands/cas_clean_sessions.py:9
#: management/commands/cas_clean_sessions.py:22
msgid "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"
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"
msgstr ""
#: models.py:56
#: models.py:160
msgid "Users"
msgstr ""
#: models.py:114
#: models.py:229
#, python-format
msgid "Error during service logout %s"
msgstr "Error during service logout %s"
#: models.py:182
#: models.py:307
msgid "Service pattern"
msgstr "Service pattern"
#: models.py:183
#: models.py:308
msgid "Services patterns"
msgstr ""
#: models.py:187
msgid "position"
msgstr "position"
#: models.py:313
msgid "service patterns are sorted using the position attribute"
msgstr ""
#: models.py:194 models.py:316
#: models.py:320 models.py:444
msgid "name"
msgstr "name"
#: models.py:195
#: models.py:321
msgid "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"
msgstr "pattern"
#: models.py:202
#: models.py:328
msgid ""
"A regular expression matching services. Will usually looks like '^https://"
"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 "
"character must be escaped with a '\\'."
#: models.py:211
#: models.py:337
msgid "user field"
msgstr ""
#: models.py:212
#: models.py:338
msgid "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"
msgstr ""
#: models.py:217
#: models.py:343
msgid "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"
msgstr "proxy"
#: models.py:222
#: models.py:348
msgid "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"
msgstr "proxy callback"
#: models.py:227
#: models.py:353
msgid "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"
msgstr ""
#: models.py:232
#: models.py:358
msgid "Enable SLO for the service"
msgstr "Enable SLO for the service"
#: models.py:239
#: models.py:365
msgid "single log out callback"
msgstr ""
#: models.py:240
#: models.py:366
msgid ""
"URL where the SLO request will be POST. empty = service url\n"
"This is usefull for non HTTP proxied services."
msgstr ""
#: models.py:301
#: models.py:428
msgid "username"
msgstr ""
#: models.py:302
#: models.py:429
msgid "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"
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"
msgstr "replace"
#: models.py:323
#: models.py:451
msgid ""
"name under which the attribut will be showto the service. empty = default "
"name of the attribut"
@ -182,39 +229,30 @@ msgstr ""
"name under which the attribut will be showto the service. empty = default "
"name of the attribut"
#: models.py:339 models.py:357
#: models.py:468 models.py:487
msgid "attribut"
msgstr "attribut"
#: models.py:340
#: models.py:469
msgid "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"
msgstr "a regular expression"
#: models.py:358
#: models.py:488
msgid "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"
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 …"
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
msgid "Logged"
msgstr ""
@ -243,7 +281,7 @@ msgstr "Login"
msgid "Connect to the service"
msgstr "Connect to the service"
#: views.py:140
#: views.py:152
msgid ""
"<h3>Logout successful</h3>You have successfully logged out from the Central "
"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 "
"Authentication Service. For security reasons, exit your web browser."
#: views.py:146
#: views.py:158
#, python-format
msgid ""
"<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 "
"browser."
#: views.py:153
#: views.py:165
msgid ""
"<h3>Logout successful</h3>You were already logged out from the Central "
"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 "
"Authentication Service. For security reasons, exit your web browser."
#: views.py:294
#: views.py:349
msgid "Invalid login ticket"
msgstr "Invalid login ticket, please retry to login"
#: views.py:410
#: views.py:470
#, python-format
msgid "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
msgid "Service %(url)s non allowed."
msgstr "Service %(url)s non allowed."
#: views.py:455
#: views.py:515
msgid "Username non allowed"
msgstr "Username non allowed"
#: views.py:462
#: views.py:522
msgid "User charateristics non allowed"
msgstr "User charateristics non allowed"
#: views.py:469
#: views.py:529
#, python-format
msgid "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
msgid "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
msgid "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
msgid "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"
#~ msgstr ""
#~ "<h3>Logout successful</h3>You have successfully logged out of the Central "

View file

@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: cas_server\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-06-21 00:14+0200\n"
"PO-Revision-Date: 2016-06-21 00:15+0200\n"
"POT-Creation-Date: 2016-07-04 17:15+0200\n"
"PO-Revision-Date: 2016-07-04 17:21+0200\n"
"Last-Translator: Valentin Samir <valentin.samir@crans.org>\n"
"Language-Team: django <LL@li.org>\n"
"Language: fr\n"
@ -18,88 +18,141 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n > 1);\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"
msgstr "Service Central d'Authentification"
#: forms.py:32
#: forms.py:43
msgid "Identity provider"
msgstr "fournisseur d'identité"
#: forms.py:35 forms.py:44 forms.py:92
#: forms.py:45 forms.py:55 forms.py:106
msgid "service"
msgstr "service"
#: forms.py:37
#: forms.py:47
msgid "Remember the identity provider"
msgstr "Se souvenir du fournisseur d'identité"
#: forms.py:38 forms.py:48
#: forms.py:48 forms.py:59
msgid "warn"
msgstr "Prévenez-moi avant d'accéder à d'autres services."
#: forms.py:43
#: forms.py:54
msgid "login"
msgstr "Identifiant"
#: forms.py:45
#: forms.py:56
msgid "password"
msgstr "mot de passe"
#: forms.py:59
#: forms.py:71
msgid "Bad user"
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"
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"
msgstr "Nettoyer les sessions supprimées"
#: management/commands/cas_clean_tickets.py:9
#: management/commands/cas_clean_tickets.py:22
msgid "Clean old trickets"
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"
msgstr "Utilisateur"
#: models.py:56
#: models.py:160
msgid "Users"
msgstr "Utilisateurs"
#: models.py:114
#: models.py:229
#, python-format
msgid "Error during service logout %s"
msgstr "Une erreur est survenue durant la déconnexion du service %s"
#: models.py:182
#: models.py:307
msgid "Service pattern"
msgstr "Motif de service"
#: models.py:183
#: models.py:308
msgid "Services patterns"
msgstr "Motifs de services"
#: models.py:187
msgid "position"
msgstr "position"
#: models.py:313
msgid "service patterns are sorted using the position attribute"
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"
msgstr "nom"
#: models.py:195
#: models.py:321
msgid "A name for the 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"
msgstr "motif"
#: models.py:202
#: models.py:328
msgid ""
"A regular expression matching services. Will usually looks like '^https://"
"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 "
"un '\\'."
#: models.py:211
#: models.py:337
msgid "user field"
msgstr "champ utilisateur"
#: models.py:212
#: models.py:338
msgid "Name of the attribut to transmit as username, empty = login"
msgstr ""
"Nom de l'attribut devant être transmis comme nom d'utilisateur au service. "
"vide = nom de connection"
#: models.py:216
#: models.py:342
msgid "restrict username"
msgstr "limiter les noms d'utilisateurs"
#: models.py:217
#: models.py:343
msgid "Limit username allowed to connect to the list provided bellow"
msgstr ""
"Limiter les noms d'utilisateurs autorisé à se connecter à la liste fournie "
"ci-dessous"
#: models.py:221
#: models.py:347
msgid "proxy"
msgstr "proxy"
#: models.py:222
#: models.py:348
msgid "Proxy tickets can be delivered to the service"
msgstr "des proxy tickets peuvent être délivrés au service"
#: models.py:226
#: models.py:352
msgid "proxy callback"
msgstr ""
#: models.py:227
#: models.py:353
msgid "can be used as a proxy callback to deliver PGT"
msgstr "peut être utilisé comme un callback pour recevoir un PGT"
#: models.py:231
#: models.py:357
msgid "single log out"
msgstr ""
#: models.py:232
#: models.py:358
msgid "Enable SLO for the service"
msgstr "Active le SLO pour le service"
#: models.py:239
#: models.py:365
msgid "single log out callback"
msgstr ""
#: models.py:240
#: models.py:366
msgid ""
"URL where the SLO request will be POST. empty = service url\n"
"This is usefull for non HTTP proxied services."
@ -167,63 +220,54 @@ msgstr ""
"service\n"
"Ceci n'est utilise que pour des services non HTTP proxifiés"
#: models.py:301
#: models.py:428
msgid "username"
msgstr "nom d'utilisateur"
#: models.py:302
#: models.py:429
msgid "username allowed to connect to the 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"
msgstr ""
"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"
msgstr "remplacement"
#: models.py:323
#: models.py:451
msgid ""
"name under which the attribut will be showto the service. empty = default "
"name of the attribut"
msgstr ""
"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"
msgstr "attribut"
#: models.py:340
#: models.py:469
msgid "Name of the attribut which must verify pattern"
msgstr "Nom de l'attribut devant vérifier un motif"
#: models.py:345
#: models.py:474
msgid "a regular expression"
msgstr "une expression régulière"
#: models.py:358
#: models.py:488
msgid "Name of the attribut for which the value must be replace"
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"
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 …"
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
msgid "Logged"
msgstr ""
@ -252,7 +296,7 @@ msgstr "Connexion"
msgid "Connect to the service"
msgstr "Se connecter au service"
#: views.py:140
#: views.py:152
msgid ""
"<h3>Logout successful</h3>You have successfully logged out from the Central "
"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 "
"navigateur."
#: views.py:146
#: views.py:158
#, python-format
msgid ""
"<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 "
"fermer votre navigateur."
#: views.py:153
#: views.py:165
msgid ""
"<h3>Logout successful</h3>You were already logged out from the Central "
"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 "
"navigateur."
#: views.py:294
#: views.py:349
msgid "Invalid login ticket"
msgstr "Ticket de connexion invalide, merci de réessayé de vous connecter"
#: views.py:410
#: views.py:470
#, python-format
msgid "Authentication has been required by service %(name)s (%(url)s)"
msgstr ""
"Une demande d'authentification a été émise pour le service %(name)s "
"(%(url)s)."
#: views.py:448
#: views.py:508
#, python-format
msgid "Service %(url)s non allowed."
msgstr "le service %(url)s n'est pas autorisé."
#: views.py:455
#: views.py:515
msgid "Username non allowed"
msgstr "Nom d'utilisateur non authorisé"
#: views.py:462
#: views.py:522
msgid "User charateristics non allowed"
msgstr "Caractéristique utilisateur non autorisée"
#: views.py:469
#: views.py:529
#, python-format
msgid "The attribut %(field)s is needed to use that service"
msgstr "L'attribut %(field)s est nécessaire pour se connecter à ce service"
#: views.py:539
#: views.py:599
#, python-format
msgid "Authentication renewal required by 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
msgid "Authentication required by service %(name)s (%(url)s)."
msgstr "Authentification requise par le service %(name)s (%(url)s)."
#: views.py:553
#: views.py:613
#, python-format
msgid "Service %s non allowed"
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"
#~ msgstr ""
#~ "<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.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible
from picklefield.fields import PickledObjectField
import re
@ -34,18 +35,93 @@ SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
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):
"""A federated user as returner by a CAS provider (username and attributes)"""
class Meta:
unique_together = ("username", "provider")
username = models.CharField(max_length=124)
provider = models.CharField(max_length=124)
provider = models.ForeignKey(FederatedIendityProvider, on_delete=models.CASCADE)
attributs = PickledObjectField()
ticket = models.CharField(max_length=255)
last_update = models.DateTimeField(auto_now=True)
def __unicode__(self):
return u"%s@%s" % (self.username, self.provider)
def __str__(self):
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
def clean_old_entries(cls):
@ -55,17 +131,17 @@ class FederatedUser(models.Model):
)
known_users = {user.username for user in User.objects.all()}
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()
class FederateSLO(models.Model):
"""An association between a CAS provider ticket and a (username, session) for processing SLO"""
class Meta:
unique_together = ("username", "session_key")
unique_together = ("username", "session_key", "ticket")
username = models.CharField(max_length=30)
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
def clean_deleted_sessions(cls):
@ -75,6 +151,7 @@ class FederateSLO(models.Model):
federate_slo.delete()
@python_2_unicode_compatible
class User(models.Model):
"""A user logged into the CAS"""
class Meta:
@ -117,7 +194,7 @@ class User(models.Model):
"""return a fresh dict for the user 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)
def logout(self, request=None):
@ -222,6 +299,7 @@ class UserFieldNotDefined(ServicePatternException):
pass
@python_2_unicode_compatible
class ServicePattern(models.Model):
"""Allowed services pattern agains services are tested to"""
class Meta:
@ -231,7 +309,8 @@ class ServicePattern(models.Model):
pos = models.IntegerField(
default=100,
verbose_name=_(u"position")
verbose_name=_(u"position"),
help_text=_(u"service patterns are sorted using the position attribute")
)
name = models.CharField(
max_length=255,
@ -288,7 +367,7 @@ class ServicePattern(models.Model):
u"This is usefull for non HTTP proxied services.")
)
def __unicode__(self):
def __str__(self):
return u"%s: %s" % (self.pos, self.pattern)
def check_user(self, user):
@ -341,6 +420,7 @@ class ServicePattern(models.Model):
raise cls.DoesNotExist()
@python_2_unicode_compatible
class Username(models.Model):
"""A list of allowed usernames on a service pattern"""
value = models.CharField(
@ -350,10 +430,11 @@ class Username(models.Model):
)
service_pattern = models.ForeignKey(ServicePattern, related_name="usernames")
def __unicode__(self):
def __str__(self):
return self.value
@python_2_unicode_compatible
class ReplaceAttributName(models.Model):
"""A list of replacement of attributs name for a service pattern"""
class Meta:
@ -372,13 +453,14 @@ class ReplaceAttributName(models.Model):
)
service_pattern = models.ForeignKey(ServicePattern, related_name="attributs")
def __unicode__(self):
def __str__(self):
if not self.replace:
return self.name
else:
return u"%s%s" % (self.name, self.replace)
@python_2_unicode_compatible
class FilterAttributValue(models.Model):
"""A list of filter on attributs for a service pattern"""
attribut = models.CharField(
@ -393,10 +475,11 @@ class FilterAttributValue(models.Model):
)
service_pattern = models.ForeignKey(ServicePattern, related_name="filters")
def __unicode__(self):
def __str__(self):
return u"%s %s" % (self.attribut, self.pattern)
@python_2_unicode_compatible
class ReplaceAttributValue(models.Model):
"""Replacement to apply on attributs values for a service pattern"""
attribut = models.CharField(
@ -417,10 +500,11 @@ class ReplaceAttributValue(models.Model):
)
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)
@python_2_unicode_compatible
class Ticket(models.Model):
"""Generic class for a Ticket"""
class Meta:
@ -437,7 +521,7 @@ class Ticket(models.Model):
VALIDITY = settings.CAS_TICKET_VALIDITY
TIMEOUT = settings.CAS_TICKET_TIMEOUT
def __unicode__(self):
def __str__(self):
return u"Ticket-%s" % self.pk
@classmethod
@ -507,34 +591,38 @@ class Ticket(models.Model):
)
@python_2_unicode_compatible
class ServiceTicket(Ticket):
"""A Service Ticket"""
PREFIX = settings.CAS_SERVICE_TICKET_PREFIX
value = models.CharField(max_length=255, default=utils.gen_st, unique=True)
def __unicode__(self):
def __str__(self):
return u"ServiceTicket-%s" % self.pk
@python_2_unicode_compatible
class ProxyTicket(Ticket):
"""A Proxy Ticket"""
PREFIX = settings.CAS_PROXY_TICKET_PREFIX
value = models.CharField(max_length=255, default=utils.gen_pt, unique=True)
def __unicode__(self):
def __str__(self):
return u"ProxyTicket-%s" % self.pk
@python_2_unicode_compatible
class ProxyGrantingTicket(Ticket):
"""A Proxy Granting Ticket"""
PREFIX = settings.CAS_PROXY_GRANTING_TICKET_PREFIX
VALIDITY = settings.CAS_PGT_VALIDITY
value = models.CharField(max_length=255, default=utils.gen_pgt, unique=True)
def __unicode__(self):
def __str__(self):
return u"ProxyGrantingTicket-%s" % self.pk
@python_2_unicode_compatible
class Proxy(models.Model):
"""A list of proxies on `ProxyTicket`"""
class Meta:
@ -542,5 +630,5 @@ class Proxy(models.Model):
url = models.CharField(max_length=255)
proxy_ticket = models.ForeignKey(ProxyTicket, related_name="proxies")
def __unicode__(self):
def __str__(self):
return self.url

View file

@ -23,27 +23,28 @@ from cas_server.tests.utils import get_auth_client
class BaseServicePattern(object):
"""Mixing for setting up service pattern for testing"""
def setup_service_patterns(self, proxy=False):
@classmethod
def setup_service_patterns(cls, proxy=False):
"""setting up service pattern"""
# For general purpose testing
self.service = "https://www.example.com"
self.service_pattern = models.ServicePattern.objects.create(
cls.service = "https://www.example.com"
cls.service_pattern = models.ServicePattern.objects.create(
name="example",
pattern="^https://www\.example\.com(/.*)?$",
proxy=proxy,
)
models.ReplaceAttributName.objects.create(name="*", service_pattern=self.service_pattern)
models.ReplaceAttributName.objects.create(name="*", service_pattern=cls.service_pattern)
# For testing the restrict_users attributes
self.service_restrict_user_fail = "https://restrict_user_fail.example.com"
self.service_pattern_restrict_user_fail = models.ServicePattern.objects.create(
cls.service_restrict_user_fail = "https://restrict_user_fail.example.com"
cls.service_pattern_restrict_user_fail = models.ServicePattern.objects.create(
name="restrict_user_fail",
pattern="^https://restrict_user_fail\.example\.com(/.*)?$",
restrict_users=True,
proxy=proxy,
)
self.service_restrict_user_success = "https://restrict_user_success.example.com"
self.service_pattern_restrict_user_success = models.ServicePattern.objects.create(
cls.service_restrict_user_success = "https://restrict_user_success.example.com"
cls.service_pattern_restrict_user_success = models.ServicePattern.objects.create(
name="restrict_user_success",
pattern="^https://restrict_user_success\.example\.com(/.*)?$",
restrict_users=True,
@ -51,12 +52,12 @@ class BaseServicePattern(object):
)
models.Username.objects.create(
value=settings.CAS_TEST_USER,
service_pattern=self.service_pattern_restrict_user_success
service_pattern=cls.service_pattern_restrict_user_success
)
# For testing the user attributes filtering conditions
self.service_filter_fail = "https://filter_fail.example.com"
self.service_pattern_filter_fail = models.ServicePattern.objects.create(
cls.service_filter_fail = "https://filter_fail.example.com"
cls.service_pattern_filter_fail = models.ServicePattern.objects.create(
name="filter_fail",
pattern="^https://filter_fail\.example\.com(/.*)?$",
proxy=proxy,
@ -64,10 +65,10 @@ class BaseServicePattern(object):
models.FilterAttributValue.objects.create(
attribut="right",
pattern="^admin$",
service_pattern=self.service_pattern_filter_fail
service_pattern=cls.service_pattern_filter_fail
)
self.service_filter_fail_alt = "https://filter_fail_alt.example.com"
self.service_pattern_filter_fail_alt = models.ServicePattern.objects.create(
cls.service_filter_fail_alt = "https://filter_fail_alt.example.com"
cls.service_pattern_filter_fail_alt = models.ServicePattern.objects.create(
name="filter_fail_alt",
pattern="^https://filter_fail_alt\.example\.com(/.*)?$",
proxy=proxy,
@ -75,10 +76,10 @@ class BaseServicePattern(object):
models.FilterAttributValue.objects.create(
attribut="nom",
pattern="^toto$",
service_pattern=self.service_pattern_filter_fail_alt
service_pattern=cls.service_pattern_filter_fail_alt
)
self.service_filter_success = "https://filter_success.example.com"
self.service_pattern_filter_success = models.ServicePattern.objects.create(
cls.service_filter_success = "https://filter_success.example.com"
cls.service_pattern_filter_success = models.ServicePattern.objects.create(
name="filter_success",
pattern="^https://filter_success\.example\.com(/.*)?$",
proxy=proxy,
@ -86,26 +87,26 @@ class BaseServicePattern(object):
models.FilterAttributValue.objects.create(
attribut="email",
pattern="^%s$" % re.escape(settings.CAS_TEST_ATTRIBUTES['email']),
service_pattern=self.service_pattern_filter_success
service_pattern=cls.service_pattern_filter_success
)
# For testing the user_field attributes
self.service_field_needed_fail = "https://field_needed_fail.example.com"
self.service_pattern_field_needed_fail = models.ServicePattern.objects.create(
cls.service_field_needed_fail = "https://field_needed_fail.example.com"
cls.service_pattern_field_needed_fail = models.ServicePattern.objects.create(
name="field_needed_fail",
pattern="^https://field_needed_fail\.example\.com(/.*)?$",
user_field="uid",
proxy=proxy,
)
self.service_field_needed_success = "https://field_needed_success.example.com"
self.service_pattern_field_needed_success = models.ServicePattern.objects.create(
cls.service_field_needed_success = "https://field_needed_success.example.com"
cls.service_pattern_field_needed_success = models.ServicePattern.objects.create(
name="field_needed_success",
pattern="^https://field_needed_success\.example\.com(/.*)?$",
user_field="alias",
proxy=proxy,
)
self.service_field_needed_success_alt = "https://field_needed_success_alt.example.com"
self.service_pattern_field_needed_success = models.ServicePattern.objects.create(
cls.service_field_needed_success_alt = "https://field_needed_success_alt.example.com"
cls.service_pattern_field_needed_success = models.ServicePattern.objects.create(
name="field_needed_success_alt",
pattern="^https://field_needed_success_alt\.example\.com(/.*)?$",
user_field="nom",
@ -238,3 +239,17 @@ class CanLogin(object):
self.assertTrue(client.session.get("username") is None)
self.assertTrue(client.session.get("warn") 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 cas_server import utils, forms
from cas_server.tests.mixin import BaseServicePattern, CanLogin
from cas_server import utils, models
from cas_server.tests.mixin import BaseServicePattern, CanLogin, FederatedIendityProviderModel
from cas_server.tests import utils as tests_utils
PROVIDERS = {
"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.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.com": ("http://127.0.0.1:8080", '1', "Example dot com"),
"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.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(
CAS_FEDERATE=True,
CAS_FEDERATE_PROVIDERS=PROVIDERS,
CAS_FEDERATE_PROVIDERS_LIST=PROVIDERS_LIST,
CAS_AUTH_CLASS="cas_server.auth.CASFederateAuth",
# test with a non ascii username
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"""
def setUp(self):
"""Prepare the test context"""
self.setup_service_patterns()
reload_module(forms)
self.setup_federated_identity_provider(PROVIDERS)
def test_default_settings(self):
"""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
reload_module(default_settings)
self.assertEqual(settings.CAS_FEDERATE_PROVIDERS_LIST, provider_list)
self.assertEqual(settings.CAS_AUTH_CLASS, "cas_server.auth.CASFederateAuth")
def test_login_get_provider(self):
@ -63,10 +57,10 @@ class FederateAuthLoginLogoutTestCase(TestCase, BaseServicePattern, CanLogin):
client = Client()
response = client.get("/login")
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>' % (
key,
utils.get_tuple(value, 2, key)
provider.suffix,
provider.verbose_name
) in response.content.decode("utf-8"))
self.assertEqual(response.context['post_url'], '/federate')
@ -74,10 +68,11 @@ class FederateAuthLoginLogoutTestCase(TestCase, BaseServicePattern, CanLogin):
"""test a successful login wrokflow"""
tickets = []
# choose the example.com provider
for (provider, cas_port) in [
for (suffix, cas_port) in [
("example.com", 8080), ("example.org", 8081),
("example.net", 8082), ("example.test", 8083)
]:
provider = models.FederatedIendityProvider.objects.get(suffix=suffix)
# get a bare client
client = Client()
# fetch the login page
@ -86,7 +81,7 @@ class FederateAuthLoginLogoutTestCase(TestCase, BaseServicePattern, CanLogin):
self.assertEqual(response.context['post_url'], '/federate')
# get current form parameter
params = tests_utils.copy_form(response.context["form"])
params['provider'] = provider
params['provider'] = provider.suffix
if remember:
params['remember'] = 'on'
# post the choosed provider
@ -96,22 +91,22 @@ class FederateAuthLoginLogoutTestCase(TestCase, BaseServicePattern, CanLogin):
if remember:
self.assertEqual(response["Location"], '%s/federate/%s?remember=on' % (
'http://testserver' if django.VERSION < (1, 9) else "",
provider
provider.suffix
))
else:
self.assertEqual(response["Location"], '%s/federate/%s' % (
'http://testserver' if django.VERSION < (1, 9) else "",
provider
provider.suffix
))
# 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
self.assertEqual(response.status_code, 302)
self.assertEqual(
response["Location"],
"%s/login?service=http%%3A%%2F%%2Ftestserver%%2Ffederate%%2F%s" % (
settings.CAS_FEDERATE_PROVIDERS[provider][0],
provider
provider.server_url,
provider.suffix
)
)
# 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
# http://testserver/federate/example.com with `ticket`
tests_utils.DummyCAS.run(
("http://testserver/federate/%s" % provider).encode("ascii"),
("http://testserver/federate/%s" % provider.suffix).encode("ascii"),
ticket.encode("ascii"),
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
# 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["Location"], "%s/login" % (
'http://testserver' if django.VERSION < (1, 9) else ""
@ -143,7 +138,7 @@ class FederateAuthLoginLogoutTestCase(TestCase, BaseServicePattern, CanLogin):
response = client.post("/login", params)
# the user should now being authenticated using username test@`provider`
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))
@ -198,7 +193,7 @@ class FederateAuthLoginLogoutTestCase(TestCase, BaseServicePattern, CanLogin):
self.assertEqual(
response["Location"],
"%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
)
)
@ -216,7 +211,7 @@ class FederateAuthLoginLogoutTestCase(TestCase, BaseServicePattern, CanLogin):
self.assertEqual(
response["Location"],
"%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
)
)
@ -234,45 +229,45 @@ class FederateAuthLoginLogoutTestCase(TestCase, BaseServicePattern, CanLogin):
for (provider, ticket, client) in tickets:
# SLO for an unkown ticket should do nothing
response = client.post(
"/federate/%s" % provider,
"/federate/%s" % provider.suffix,
{'logoutRequest': tests_utils.logout_request(utils.gen_st())}
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b"ok")
# Bad SLO format should do nothing
response = client.post(
"/federate/%s" % provider,
"/federate/%s" % provider.suffix,
{'logoutRequest': ""}
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b"ok")
# Bad SLO format should do nothing
response = client.post(
"/federate/%s" % provider,
"/federate/%s" % provider.suffix,
{'logoutRequest': "<root></root>"}
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b"ok")
response = client.get("/login")
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
# 3 or 'CAS_2_SAML_1_0'
response = client.post(
"/federate/%s" % provider,
"/federate/%s" % provider.suffix,
{'logoutRequest': tests_utils.logout_request(ticket)}
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b"ok")
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)
else:
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):
@ -287,7 +282,7 @@ class FederateAuthLoginLogoutTestCase(TestCase, BaseServicePattern, CanLogin):
self.assertEqual(response.status_code, 302)
self.assertEqual(
response["Location"],
"%s/logout" % settings.CAS_FEDERATE_PROVIDERS[provider][0]
"%s/logout" % provider.server_url,
)
response = client.get("/login")
self.assert_login_failed(client, response)
@ -326,7 +321,7 @@ class FederateAuthLoginLogoutTestCase(TestCase, BaseServicePattern, CanLogin):
self.assertEqual(response.status_code, 302)
self.assertEqual(response["Location"], "%s/federate/%s" % (
'http://testserver' if django.VERSION < (1, 9) else "",
provider
provider.suffix
))
def test_login_bad_ticket(self):
@ -338,7 +333,10 @@ class FederateAuthLoginLogoutTestCase(TestCase, BaseServicePattern, CanLogin):
# get a bare client
client = Client()
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()
if django.VERSION >= (1, 8):
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
response = client.post("/login", params)
self.assertEqual(response.status_code, 200)
for key, value in settings.CAS_FEDERATE_PROVIDERS.items():
self.assertTrue('<option value="%s">%s</option>' % (
key,
utils.get_tuple(value, 2, key)
) in response.content.decode("utf-8"))
for provider in models.FederatedIendityProvider.objects.all():
self.assertIn(
'<option value="%s">%s</option>' % (
provider.suffix,
provider.verbose_name
),
response.content.decode("utf-8")
)
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.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
class FederatedUserTestCase(TestCase, UserModels):
class FederatedUserTestCase(TestCase, UserModels, FederatedIendityProviderModel):
"""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):
"""tests for clean_old_entries that should delete federated user no longer used"""
client = Client()
client.get("/login")
provider = models.FederatedIendityProvider.objects.get(suffix="example.com")
models.FederatedUser.objects.create(
username="test1", provider="example.com", attributs={}, ticket=""
username="test1", provider=provider, attributs={}, ticket=""
)
models.FederatedUser.objects.create(
username="test2", provider="example.com", attributs={}, ticket=""
username="test2", provider=provider, attributs={}, ticket=""
)
models.FederatedUser.objects.all().update(
last_update=(timezone.now() - timedelta(seconds=settings.CAS_TICKET_TIMEOUT + 10))
)
models.FederatedUser.objects.create(
username="test3", provider="example.com", attributs={}, ticket=""
username="test3", provider=provider, attributs={}, ticket=""
)
models.User.objects.create(
username="test1@example.com", session_key=client.session.session_key
)
self.assertEqual(len(models.FederatedUser.objects.all()), 3)
models.FederatedUser.clean_old_entries()
self.assertEqual(len(models.FederatedUser.objects.all()), 2)
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.middleware.csrf import CsrfViewMiddleware
from django.views.generic import View
from django.utils.encoding import python_2_unicode_compatible
import re
import logging
@ -37,7 +38,7 @@ import cas_server.models as models
from .utils import json_response
from .models import ServiceTicket, ProxyTicket, ProxyGrantingTicket
from .models import ServicePattern
from .models import ServicePattern, FederatedIendityProvider, FederatedUser
from .federate import CASFederateValidateUser
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
@ -123,11 +124,12 @@ class LogoutView(View, LogoutMixin):
self.init_get(request)
# if CAS federation mode is enable, bakup the provider before flushing the sessions
if settings.CAS_FEDERATE:
if "username" in self.request.session:
component = self.request.session["username"].split('@')
provider = component[-1]
auth = CASFederateValidateUser(provider, service_url="")
else:
try:
user = FederatedUser.get_from_federated_username(
self.request.session.get("username")
)
auth = CASFederateValidateUser(user.provider, service_url="")
except FederatedUser.DoesNotExist:
auth = None
session_nb = self.logout(self.request.GET.get("all"))
# 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:
params = utils.copy_params(request.GET)
url = auth.get_logout_url()
if url:
return HttpResponseRedirect(utils.update_url(url, params))
# if service is set, redirect to service after logout
if self.service:
@ -201,7 +202,6 @@ class FederateAuth(View):
@staticmethod
def get_cas_client(request, 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"})
return CASFederateValidateUser(provider, service_url)
@ -210,7 +210,8 @@ class FederateAuth(View):
if not settings.CAS_FEDERATE:
return redirect("cas_server:login")
# 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)
try:
auth.clean_sessions(request.POST['logoutRequest'])
@ -218,7 +219,7 @@ class FederateAuth(View):
pass
return HttpResponse("ok")
# 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
reason = CsrfViewMiddleware().process_view(request, None, (), {})
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(
"cas_server:federateAuth",
kwargs=dict(provider=form.cleaned_data["provider"]),
kwargs=dict(provider=form.cleaned_data["provider"].suffix),
params=params
)
response = HttpResponseRedirect(url)
@ -240,7 +241,7 @@ class FederateAuth(View):
utils.set_cookie(
response,
"_remember_provider",
request.POST["provider"],
form.cleaned_data["provider"].suffix,
max_age
)
return response
@ -251,8 +252,8 @@ class FederateAuth(View):
"""method called on GET request"""
if not settings.CAS_FEDERATE:
return redirect("cas_server:login")
if provider not in settings.CAS_FEDERATE_PROVIDERS:
return redirect("cas_server:login")
try:
provider = FederatedIendityProvider.objects.get(suffix=provider)
auth = self.get_cas_client(request, provider)
if 'ticket' not in request.GET:
return HttpResponseRedirect(auth.get_login_url())
@ -260,14 +261,15 @@ class FederateAuth(View):
ticket = request.GET['ticket']
if auth.verify_ticket(ticket):
params = utils.copy_params(request.GET, ignore={"ticket"})
username = u"%s@%s" % (auth.username, auth.provider)
request.session["federate_username"] = username
request.session["federate_username"] = auth.federated_username
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)
return HttpResponseRedirect(url)
else:
return HttpResponseRedirect(auth.get_login_url())
except FederatedIendityProvider.DoesNotExist:
return redirect("cas_server:login")
class LoginView(View, LogoutMixin):
@ -347,17 +349,10 @@ class LoginView(View, LogoutMixin):
_(u"Invalid login ticket")
)
elif ret == self.USER_LOGIN_OK:
try:
self.user = models.User.objects.get(
self.user = models.User.objects.get_or_create(
username=self.request.session['username'],
session_key=self.request.session.session_key
)
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
)
)[0]
self.user.save()
elif ret == self.USER_LOGIN_FAILURE: # bad user login
if settings.CAS_FEDERATE:
@ -639,8 +634,9 @@ class LoginView(View, LogoutMixin):
else:
if (
self.request.COOKIES.get('_remember_provider') and
self.request.COOKIES['_remember_provider'] in
settings.CAS_FEDERATE_PROVIDERS
FederatedIendityProvider.objects.filter(
suffix=self.request.COOKIES['_remember_provider']
)
):
params = utils.copy_params(self.request.GET)
url = utils.reverse_params(
@ -708,16 +704,10 @@ class Auth(View):
)
if form.is_valid():
try:
try:
user = models.User.objects.get(
user = models.User.objects.get_or_create(
username=form.cleaned_data['username'],
session_key=request.session.session_key
)
except models.User.DoesNotExist:
user = models.User.objects.create(
username=form.cleaned_data['username'],
session_key=request.session.session_key
)
)[0]
user.save()
# is the service allowed
service_pattern = ServicePattern.validate(service)
@ -789,6 +779,7 @@ class Validate(View):
return HttpResponse(u"no\n", content_type="text/plain; charset=utf-8")
@python_2_unicode_compatible
class ValidateError(Exception):
"""handle service validation error"""
def __init__(self, code, msg=""):
@ -796,7 +787,7 @@ class ValidateError(Exception):
self.msg = msg
super(ValidateError, self).__init__(code)
def __unicode__(self):
def __str__(self):
return u"%s" % self.msg
def render(self, request):
@ -1039,6 +1030,7 @@ class Proxy(View):
)
@python_2_unicode_compatible
class SamlValidateError(Exception):
"""handle saml validation error"""
def __init__(self, code, msg=""):
@ -1046,7 +1038,7 @@ class SamlValidateError(Exception):
self.msg = msg
super(SamlValidateError, self).__init__(code)
def __unicode__(self):
def __str__(self):
return u"%s" % self.msg
def render(self, request):