Use django admin application to add/modif identty providers when CAS_FEDERATE is True
This commit is contained in:
parent
40b4f07001
commit
aa433d3c58
@ -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
2
.gitignore
vendored
@ -15,3 +15,5 @@ coverage.xml
|
||||
test_venv
|
||||
.coverage
|
||||
htmlcov/
|
||||
tox_logs/
|
||||
.cache/
|
||||
|
39
README.rst
39
README.rst
@ -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``.
|
||||
|
@ -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)
|
||||
|
@ -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"""
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
renew=False,
|
||||
)
|
||||
self.client = CASClient(
|
||||
service_url=service_url,
|
||||
version=provider.cas_protocol_version,
|
||||
server_url=provider.server_url,
|
||||
renew=False,
|
||||
)
|
||||
|
||||
def get_login_url(self):
|
||||
"""return the CAS provider login url"""
|
||||
return self.client.get_login_url() 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(
|
||||
username=username,
|
||||
provider=self.provider,
|
||||
attributs=attributs,
|
||||
ticket=ticket
|
||||
)
|
||||
user.save()
|
||||
user = FederatedUser.objects.update_or_create(
|
||||
username=username,
|
||||
provider=self.provider,
|
||||
defaults=dict(attributs=attributs, ticket=ticket)
|
||||
)[0]
|
||||
user.save()
|
||||
self.federated_username = user.federated_username
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@ -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"""
|
||||
FederateSLO.objects.create(
|
||||
username=username,
|
||||
session_key=session_key,
|
||||
ticket=ticket
|
||||
)
|
||||
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"""
|
||||
|
@ -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(
|
||||
|
Binary file not shown.
@ -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 "
|
||||
|
Binary file not shown.
@ -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"
|
||||
|
50
cas_server/migrations/0007_auto_20160704_1510.py
Normal file
50
cas_server/migrations/0007_auto_20160704_1510.py
Normal 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')]),
|
||||
),
|
||||
]
|
@ -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
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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')
|
||||
|
@ -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):
|
||||
|
@ -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,8 +137,7 @@ 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))
|
||||
return HttpResponseRedirect(utils.update_url(url, params))
|
||||
# if service is set, redirect to service after logout
|
||||
if self.service:
|
||||
list(messages.get_messages(request)) # clean messages before leaving the django app
|
||||
@ -201,16 +202,16 @@ 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)
|
||||
service_url = utils.get_current_url(request, {"ticket", "provider"})
|
||||
return CASFederateValidateUser(provider, service_url)
|
||||
|
||||
def post(self, request, provider=None):
|
||||
"""method called on POST request"""
|
||||
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,23 +252,24 @@ 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")
|
||||
auth = self.get_cas_client(request, provider)
|
||||
if 'ticket' not in request.GET:
|
||||
return HttpResponseRedirect(auth.get_login_url())
|
||||
else:
|
||||
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_ticket"] = ticket
|
||||
auth.register_slo(username, request.session.session_key, ticket)
|
||||
url = utils.reverse_params("cas_server:login", params)
|
||||
return HttpResponseRedirect(url)
|
||||
else:
|
||||
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())
|
||||
else:
|
||||
ticket = request.GET['ticket']
|
||||
if auth.verify_ticket(ticket):
|
||||
params = utils.copy_params(request.GET, ignore={"ticket"})
|
||||
request.session["federate_username"] = auth.federated_username
|
||||
request.session["federate_ticket"] = 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,18 +349,11 @@ class LoginView(View, LogoutMixin):
|
||||
_(u"Invalid login ticket")
|
||||
)
|
||||
elif ret == self.USER_LOGIN_OK:
|
||||
try:
|
||||
self.user = models.User.objects.get(
|
||||
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
|
||||
)
|
||||
self.user.save()
|
||||
self.user = models.User.objects.get_or_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:
|
||||
self.ticket = None
|
||||
@ -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(
|
||||
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
|
||||
)
|
||||
user = models.User.objects.get_or_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):
|
||||
|
Loading…
Reference in New Issue
Block a user