From aa433d3c58e896b92937465ecf2e38f490b7ab71 Mon Sep 17 00:00:00 2001 From: Valentin Samir Date: Mon, 4 Jul 2016 17:23:11 +0200 Subject: [PATCH] Use django admin application to add/modif identty providers when CAS_FEDERATE is True --- .coveragerc | 1 + .gitignore | 2 + README.rst | 39 ++-- cas_server/admin.py | 6 + cas_server/auth.py | 9 +- cas_server/default_settings.py | 25 --- cas_server/federate.py | 58 +++--- cas_server/forms.py | 23 +-- cas_server/locale/en/LC_MESSAGES/django.mo | Bin 6117 -> 6105 bytes cas_server/locale/en/LC_MESSAGES/django.po | 183 ++++++++++------- cas_server/locale/fr/LC_MESSAGES/django.mo | Bin 7285 -> 8347 bytes cas_server/locale/fr/LC_MESSAGES/django.po | 189 +++++++++++------- .../migrations/0007_auto_20160704_1510.py | 50 +++++ cas_server/models.py | 124 ++++++++++-- cas_server/tests/mixin.py | 63 +++--- cas_server/tests/test_federate.py | 93 ++++----- cas_server/tests/test_models.py | 17 +- cas_server/views.py | 106 +++++----- 18 files changed, 600 insertions(+), 388 deletions(-) create mode 100644 cas_server/migrations/0007_auto_20160704_1510.py diff --git a/.coveragerc b/.coveragerc index 9163f3e..771fe83 100644 --- a/.coveragerc +++ b/.coveragerc @@ -12,6 +12,7 @@ exclude_lines = pragma: no cover def __repr__ def __unicode__ + def __str__ raise AssertionError raise NotImplementedError if six.PY3: diff --git a/.gitignore b/.gitignore index 3b1bcb6..273399d 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ coverage.xml test_venv .coverage htmlcov/ +tox_logs/ +.cache/ diff --git a/README.rst b/README.rst index 4819129..ebcaaa0 100644 --- a/README.rst +++ b/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``. diff --git a/cas_server/admin.py b/cas_server/admin.py index 472e1df..f2baf81 100644 --- a/cas_server/admin.py +++ b/cas_server/admin.py @@ -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) diff --git a/cas_server/auth.py b/cas_server/auth.py index 160adc2..9f40ae4 100644 --- a/cas_server/auth.py +++ b/cas_server/auth.py @@ -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""" diff --git a/cas_server/default_settings.py b/cas_server/default_settings.py index 6b418fd..0b24f62 100644 --- a/cas_server/default_settings.py +++ b/cas_server/default_settings.py @@ -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) diff --git a/cas_server/federate.py b/cas_server/federate.py index 997e56f..4534cda 100644 --- a/cas_server/federate.py +++ b/cas_server/federate.py @@ -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""" diff --git a/cas_server/forms.py b/cas_server/forms.py index 233938e..bf7a0e2 100644 --- a/cas_server/forms.py +++ b/cas_server/forms.py @@ -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( diff --git a/cas_server/locale/en/LC_MESSAGES/django.mo b/cas_server/locale/en/LC_MESSAGES/django.mo index 3ebf9a68d87017e3222c7d49705f72c4480bb712..ac062279497e09c2d1d8f373a76b2b01c87a1880 100644 GIT binary patch delta 1352 zcma*nOGs346vy%7IGUqP=9u=L(HhM%9mht;9?rssLV8fMERaBl5Na};vWI0Y!pIhd z#6T@<6SRn4Q$i5AXw$Z66GVhG5Cu^rt^59*YXxokRD{LlGgF7!4u^4{mZ zZj30gf_PJ4){R#R`NeqPHk-hQcop{+nT_EzR%5Ez>=9l>eWPQZSqujp?_)djuULY0 z{PJTn^8SeJ;$kT``p}2x@(=79E@OTN^}-9Rz-e5BGuVk9k68e>BRSYXBsWVr^E7ra zKZBd`F*f5j^s>GMO3j+M(S%xPKdKU^Q9I0FIo`tvK1VJ11C>Z+S>6UDC+kElunm>) zan!~}QHk9^ifOkn&iZzniyr)p`at)5Gpc0?)O;W6#a>*1{m%1~sD#erI(&lq@CQuN z#vD#C-@!|2-4`6gVRoa9l`N$G>Ro^f_3kysu#~iAH>!7+unVV96>_m1eV`WAx(?*W z63+c3s&&Jtzt14A+9WpOOH}J-eAHj1Y^^l=hzZnAqtsQs>_qi0j_Tc6RB4~07eAp| z_Y<{HuqxlWt*8VKpjtPC5gbLW_Y8T(-c?b5U3}+;N?pubTA&=2aSTb;_Mo2kqDp)S z<9HN%aMF2RwK(58KkD!6Q5y;&)wee1c^s9{&Jixuy9>A;?;@=;4(IPdSFE1kyv-)F z5;qWagwDy_Xmc)>qrR!3O{^ibYZswpfbOz|OvDb~}{GYMa+)zn0)LZp+l_t)hL$j?> zyQsE}zbXo3r`#3p@tm(A+vqv$3ikIUQ|bQnshWYoBPW;)+CO)WWiw?r3reF+Es>gV fbW^x(e2drr*EHMb-CdN;`7Rg!-`7&rLj}J8=$new delta 1323 zcmZwHOGs346vy%7V{Fu>$4qKgKFb`<=VW6jN=hUW#4HM2i6T=Os4=F5ltTn9k{}s0 z5ps$)E}}(uBSD*1Em~BYXd_533)pV=I~$6_4JGkcCx*pA^HX5BdCnMTdMj(#lSqa4eT z@k#6AqLdrwF^J>t1G|L>=r3Ue9n`?DxEFo-X8W)NyRZQ(@CvdQyNm46=Dq#{JV}2A zJMjZHvA%^0%mUo##YT*w7Mew6VH1v`HnM~y z(N^#@KEXlOw@ofIpl_!cS+W?aKY$uIjJxoX_k0?)fmy6a2Q~3J#wp(?Ow;dYkV^I) z=WvGI>mYf1+>(`HQZ;+ag=*#}ud)wSvpd*}FHsp$eKbJ@s$@~*&tl&F5md=$FoFxH zaj%gn>>D1#->8z+gvh^AI1)1ZjJHuIzD!|N!*Nv2rcgC|hDzxVRLM$<+>+Iz7P^2c z*)`Ng=TRkF#Wq|+t@jg|!UDzQUl-MUTPbfxEpW=~PoPqJ9aXcNIEb&j=T#+c$?8$# zBd7y(B3ZW{?|BThfdN#>?qLJ2CApwv#>=?fsD{-NykAB_GAE1RQ9|#>R?~!PI`LLh zn!`jTkxghbHN*+x-`2v7WEme-1n*4d_PnR-*U?WWTzmCfEAWl1ec zsK#}~K|<-*iIwI3g!ga7j&Ob4{hC`#rQWujC@k}S31t4Li9l{%ZbzjBJg zlkvp#=t#V7vZJ6T98V-B5_OZQ#o!sopFfuEEEg_k6+{~&t(7e;9j$Gt?m+q9Zq&IL eIOqHS6wYFBHOD{2?Bl~@aVK5ala+c}()b5Q?0@6{ diff --git a/cas_server/locale/en/LC_MESSAGES/django.po b/cas_server/locale/en/LC_MESSAGES/django.po index 805843a..0ab0f93 100644 --- a/cas_server/locale/en/LC_MESSAGES/django.po +++ b/cas_server/locale/en/LC_MESSAGES/django.po @@ -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 \n" "Language-Team: django \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 "" "

Logout successful

You have successfully logged out from the Central " "Authentication Service. For security reasons, exit your web browser." @@ -251,7 +289,7 @@ msgstr "" "

Logout successful

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 "" "

Logout successful

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 "" "

Logout successful

You were already logged out from the Central " "Authentication Service. For security reasons, exit your web browser." @@ -270,48 +308,55 @@ msgstr "" "

Logout successful

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 "" #~ "

Logout successful

You have successfully logged out of the Central " diff --git a/cas_server/locale/fr/LC_MESSAGES/django.mo b/cas_server/locale/fr/LC_MESSAGES/django.mo index 5b0180af5885870fe08c3dc4b0859370d8b631ad..362bc4caf94487b373335bdc130cf87f19738c44 100644 GIT binary patch delta 2695 zcmZ{kTWl0n7{^bA)>2A=S}eT)M<^8JTI|hAp#>45wMMHHLu@U2IBbBdID56*>e!jIu$xcN?{?uO^#C-67e z0^coGY9qW5bM_3Sn%Q?lj?~Mr0=^CxDwS0ySV*9AunPVN%i*8#c~OZ{)$A8QUQn%Y zF6@O8$b#+gAY25`LTXe$!9w_Jy#F0O!2TL+g{x-DKC6_;Vhsn!;VgIou7;PPBrGk> zE3*m3MVX^eZuSwB1gGJH@Ke|a|AQiRA63#7m4dQ&puD#~K0gSvQt>M+B*AA8 zb=24K7k+>u)z45Qt(ud6uAu~63GaoCkV~t6C<}hcjc8V zu0a2*94tZ)xk1@Hr5=Ypa5Fp!rDs>-eI5GGXP<(L;fwGdcml43UqbQ9byx{Y`B(t! z;0CxE%6XQLE%2R6^xw(iA_vQ0Bhd?}d>6!j>KPo#Uyp~U+h*290H6uPX2UliO9rIk--S!7tGp(t<(z6KZbTf~3r6qE#KVK2M_HSFM8 zBDn!m(1zqt-$7~3b=U@PK)G=fPNr7%D3rJf_cO)ZGBo+sLrjrc4A#OFHE7!{ zqmCWch!m0s;(kmXnMw8{xyUbLT^^B9#dMR0^jzAql(~vo%l{m8A5(ftYa+8qpIVur zRkSGATR1SSqO(V5T*r5YoU|S>UC(lC6)q@R7=BayR(NE_v8JTsYJa!oX)9sczU3c~ z?~kx?b;9y8Y2$#I(2mV_Or3U8maQ{JDjY7U%so=lQP9;BB$L*DZDca0ozT9c2aTa; zWx0jSuIUG^O{BmxUE3Ho+w?$umgpZp8|WDDq96m|p_yN`YuoX4(g_GbF3IYf!x_gl z+ygpc_{N~&nX7arZ9?sHNKV%ra%|fi^25hUYrA&juc@76^tu!>Apu_|uim%Yw6#Z> zmYw45y@BcZUOpKKCtV@jQ`%j3n=Ysz+%w5(p&s2f_^8k)lA z%N9;-*Ei%&lm&%VN82)kCJJ z?%I~;nQRmF@f~^Yu}>S)cT*=!9cjIvf^&V9rwX!p2bkK};{-n6HUe5WOn|_*(w1lV z5{|A*a7ktmEGkJ;<5VdziUgT>ZNenxfaN>#)$r1*l zQ@WT&N>5;uvC#=jSuR#GJr9pRCyz&*z`JQC(+%fWuT4#*s2)3}^H-$nX~*?&kxti> z-Lyv>OY%vuy}{TAKI#O~FJkH|hhLP}ghut&hAAXblg_=#?eTIatD6gU;yV#ktcT#^ y)>B;+G3@wOl6UZ4-k5mM_7TaF=9+0rPT$mY+>YZW{H3P9TEWi@=KlepZXzZC delta 1558 zcmYk+Z%mC*9KiA4Q@5hHu2c%Qd*xqAbxZe_n?EIH!{q;XLBbVESxuPD#Tzp-w!;g< zm|2^dwROiBlJNpB%o{c}ZRQOYvnI27zTX&2VLOAl-FA zC<~}lsICN&bvT&Fg;JR$auw&{Iedqwac{Cn7XHL;9GfCifxWm8e>ql+5(&}oK>p-5 zmt4GyjE~A28adn;#B@v^9iKP{C(#dJ8LmJbcofIu70kdc)WttTA10(S2j(Gbl>(<< zf|c|`ScR=v%=_gQjj`P5!y^2I`l76~ctnDzJ6((8a1ZJN4r4i<#=Y2!E-V}qUq}FT zfvZs$5^?%X$X?|b7V~~N<^01v)CqcU2|mTGIEvNjfJP*$vft?+LY?@ybN?jjg3h8Y z@R@V}EwU#WK%M6^&c-2(>I=PY23dp*@6hjNo1M7ZBXSeRF`Gug5NorEzfN?R8?*2l>Pnv@o0l)B zhw?Y-j@_KpGq?^(w|qdI_$&G`hi#EH38U_C85a%tb_`<^>bOqS53D;%V>ykxj@IP( z1S?TLfZ2sd64isCMIZ9a zj)s0t`Uz>7N1bN3M5a6bB|t+DmPSsCel;5QDypV~^JN#8V+HCWbz{0nvKF-u2\n" "Language-Team: django \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 "" "

Logout successful

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 "" "

Logout successful

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 "" "

Logout successful

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 "" #~ "

Déconnexion réussie

\n" diff --git a/cas_server/migrations/0007_auto_20160704_1510.py b/cas_server/migrations/0007_auto_20160704_1510.py new file mode 100644 index 0000000..a89627d --- /dev/null +++ b/cas_server/migrations/0007_auto_20160704_1510.py @@ -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')]), + ), + ] diff --git a/cas_server/models.py b/cas_server/models.py index 17c4d83..971cce5 100644 --- a/cas_server/models.py +++ b/cas_server/models.py @@ -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 diff --git a/cas_server/tests/mixin.py b/cas_server/tests/mixin.py index 09ddadc..859c3a0 100644 --- a/cas_server/tests/mixin.py +++ b/cas_server/tests/mixin.py @@ -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 + ) diff --git a/cas_server/tests/test_federate.py b/cas_server/tests/test_federate.py index 2fe4728..a33feed 100644 --- a/cas_server/tests/test_federate.py +++ b/cas_server/tests/test_federate.py @@ -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('' % ( - 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': ""} ) 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('' % ( - key, - utils.get_tuple(value, 2, key) - ) in response.content.decode("utf-8")) + for provider in models.FederatedIendityProvider.objects.all(): + self.assertIn( + '' % ( + provider.suffix, + provider.verbose_name + ), + response.content.decode("utf-8") + ) self.assertEqual(response.context['post_url'], '/federate') diff --git a/cas_server/tests/test_models.py b/cas_server/tests/test_models.py index e027429..7a4403c 100644 --- a/cas_server/tests/test_models.py +++ b/cas_server/tests/test_models.py @@ -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): diff --git a/cas_server/views.py b/cas_server/views.py index 55ecec4..a95a6cd 100644 --- a/cas_server/views.py +++ b/cas_server/views.py @@ -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):