From 58be6fb3e402d594fffca56d1cb8d1aff339b9a9 Mon Sep 17 00:00:00 2001 From: Valentin Samir Date: Sat, 10 Sep 2016 01:16:33 +0200 Subject: [PATCH 01/13] Update coverage URLs --- .update_coverage | 2 +- README.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.update_coverage b/.update_coverage index a4a6887..7895cf4 100755 --- a/.update_coverage +++ b/.update_coverage @@ -27,7 +27,7 @@ if [[ "$BRANCH" = "HEAD" ]] || [ -z "$BRANCH" ]; then exit 0 fi -curl https://badges.genua.fr/local/coverage/ \ +curl https://badges.genua.fr/coverage/ \ -F "secret=$COVERAGE_TOKEN" \ -F "tar=@$BASEDIR/coverage.tar.gz" \ -F "project=$PROJECT_NAME" \ diff --git a/README.rst b/README.rst index 38ea941..283a5ed 100644 --- a/README.rst +++ b/README.rst @@ -638,8 +638,8 @@ You could for example do as bellow:: .. |codacy| image:: https://badges.genua.fr/codacy/grade/255c21623d6946ef8802fa7995b61366/master.svg :target: https://www.codacy.com/app/valentin-samir/django-cas-server -.. |coverage| image:: https://badges.genua.fr/local/coverage/?project=django-cas-server&branch=master - :target: https://badges.genua.fr/local/coverage/django-cas-server/master +.. |coverage| image:: https://intranet.genua.fr/coverage/badge/django-cas-server/master.svg + :target: https://badges.genua.fr/coverage/django-cas-server/master .. |doc| image:: https://badges.genua.fr/local/readthedocs/?version=latest :target: http://django-cas-server.readthedocs.io From c7171bb386a0db61bee1cf61f44dcb85c5e59487 Mon Sep 17 00:00:00 2001 From: Valentin Samir Date: Sat, 10 Sep 2016 15:24:30 +0200 Subject: [PATCH 02/13] Add a test for login with missing parameter (username or password or both) --- cas_server/tests/test_view.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/cas_server/tests/test_view.py b/cas_server/tests/test_view.py index c623a27..22b3bd7 100644 --- a/cas_server/tests/test_view.py +++ b/cas_server/tests/test_view.py @@ -132,6 +132,37 @@ class LoginTestCase(TestCase, BaseServicePattern, CanLogin): # The LoginTicket is conssumed and should no longer be valid self.assertTrue(params['lt'] not in client.session['lt']) + def test_login_post_missing_params(self): + """Test a login attempt with missing POST parameters (username or password or both)""" + # we get a client who fetch a frist time the login page and the login form default + # parameters + client, params = get_login_page_params() + # we set only set username + params["username"] = settings.CAS_TEST_USER + # we post a login attempt + response = client.post('/login', params) + # as the LT is not valid, login should fail + self.assert_login_failed(client, response) + + # we get a client who fetch a frist time the login page and the login form default + # parameters + client, params = get_login_page_params() + # we set only set password + params["password"] = settings.CAS_TEST_PASSWORD + # we post a login attempt + response = client.post('/login', params) + # as the LT is not valid, login should fail + self.assert_login_failed(client, response) + + # we get a client who fetch a frist time the login page and the login form default + # parameters + client, params = get_login_page_params() + # we set neither username nor password + # we post a login attempt + response = client.post('/login', params) + # as the LT is not valid, login should fail + self.assert_login_failed(client, response) + def test_login_view_post_goodpass_goodlt_warn(self): """Test a successul login requesting to be warned before creating services tickets""" # get a client and initial login params From 7321583429c31802a5841f5744acbe2695e1dc14 Mon Sep 17 00:00:00 2001 From: Valentin Samir Date: Sat, 10 Sep 2016 15:25:31 +0200 Subject: [PATCH 03/13] Update CHANGELOG --- CHANGELOG.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8f3f0d6..c119e1c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,14 @@ All notable changes to this project will be documented in this file. .. contents:: Table of Contents :depth: 2 +Unreleased +========== + +Added +----- +* Add a test for login with missing parameter (username or password or both) + + v0.7.4 - 2016-09-07 =================== From 75b3fe4db0dfc958bb7a1d9211ae5734cb75dbec Mon Sep 17 00:00:00 2001 From: Valentin Samir Date: Sun, 11 Sep 2016 19:30:26 +0200 Subject: [PATCH 04/13] Update coverage upload script --- .update_coverage | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/.update_coverage b/.update_coverage index 7895cf4..402179c 100755 --- a/.update_coverage +++ b/.update_coverage @@ -2,24 +2,25 @@ BASEDIR="$1" PROJECT_NAME="$2" -cd "$BASEDIR/htmlcov/"; tar czf "$BASEDIR/coverage.tar.gz" ./ - - -cd "$BASEDIR" +TITLE="Coverage report of $PROJECT_NAME" # build by gitlab CI if [ -n "$CI_BUILD_REF_NAME" ]; then BRANCH="$CI_BUILD_REF_NAME" + TITLE="$TITLE, $BRANCH branch" # build by travis elif [ -n "$TRAVIS_BRANCH" ]; then # if this a pull request ? if [[ "$TRAVIS_PULL_REQUEST" != "false" ]]; then BRANCH="pull-request-$TRAVIS_PULL_REQUEST" + TITLE="$TITLE, pull request n°$BRANCH" else BRANCH="$TRAVIS_BRANCH" + TITLE="$TITLE, $BRANCH branch" fi else BRANCH="$(git rev-parse --abbrev-ref HEAD)" + TITLE="$TITLE, $BRANCH branch" fi if [[ "$BRANCH" = "HEAD" ]] || [ -z "$BRANCH" ]; then @@ -27,6 +28,22 @@ if [[ "$BRANCH" = "HEAD" ]] || [ -z "$BRANCH" ]; then exit 0 fi + +VENV="$(mktemp -d)" +HTMLREPORT="$(mktemp -d)" +virtualenv "$VENV" +"$VENV/bin/pip" install coverage +"$VENV/bin/coverage" html --title "$TITLE" --directory "$HTMLREPORT" +rm -rf "$VENV" + + +cd "$HTMLREPORT"; tar czf "$BASEDIR/coverage.tar.gz" ./ + +cd "$BASEDIR" + +rm -rf "$HTMLREPORT" + + curl https://badges.genua.fr/coverage/ \ -F "secret=$COVERAGE_TOKEN" \ -F "tar=@$BASEDIR/coverage.tar.gz" \ From 37c975eaf7b3c422fb1b21ead1db7ac1d46c7c88 Mon Sep 17 00:00:00 2001 From: Valentin Samir Date: Sun, 18 Sep 2016 11:21:33 +0200 Subject: [PATCH 05/13] Allow both unicode and bytes dotted string in utils.import_attr --- CHANGELOG.rst | 4 ++++ cas_server/utils.py | 12 ++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c119e1c..ab149b6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,6 +13,10 @@ Added ----- * Add a test for login with missing parameter (username or password or both) +Fixed +----- +* Allow both unicode and bytes dotted string in utils.import_attr + v0.7.4 - 2016-09-07 =================== diff --git a/cas_server/utils.py b/cas_server/utils.py index 78fde92..af1e81e 100644 --- a/cas_server/utils.py +++ b/cas_server/utils.py @@ -117,14 +117,18 @@ def import_attr(path): transform a python dotted path to the attr :param path: A dotted path to a python object or a python object - :type path: :obj:`unicode` or anything + :type path: :obj:`unicode` or :obj:`str` or anything :return: The python object pointed by the dotted path or the python object unchanged """ - if not isinstance(path, str): + # if we got a str, decode it to unicode (normally it should only contain ascii) + if isinstance(path, six.binary_type): + path = path.decode("utf-8") + # if path is not an unicode, return it unchanged (may be it is already the attribute to import) + if not isinstance(path, six.text_type): return path - if "." not in path: + if u"." not in path: ValueError("%r should be of the form `module.attr` and we just got `attr`" % path) - module, attr = path.rsplit('.', 1) + module, attr = path.rsplit(u'.', 1) try: return getattr(import_module(module), attr) except ImportError: From 816d350548267c13f93b5778164d6d7c9fb57a71 Mon Sep 17 00:00:00 2001 From: Allie Micka Date: Sun, 18 Sep 2016 11:26:09 +0200 Subject: [PATCH 06/13] Fix some spelling and grammar on log messages. --- cas_server/views.py | 50 ++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/cas_server/views.py b/cas_server/views.py index c2b18b4..9f8c83c 100644 --- a/cas_server/views.py +++ b/cas_server/views.py @@ -61,7 +61,7 @@ class LogoutMixin(object): username = self.request.session.get("username") if username: if all_session: - logger.info("Logging out user %s from all of they sessions." % username) + logger.info("Logging out user %s from all sessions." % username) else: logger.info("Logging out user %s." % username) users = [] @@ -143,7 +143,7 @@ class LogoutView(View, LogoutMixin): def get(self, request, *args, **kwargs): """ - methode called on GET request on this view + method called on GET request on this view :param django.http.HttpRequest request: The current request object """ @@ -184,7 +184,7 @@ class LogoutView(View, LogoutMixin): logout_msg = _( "

Logout successful

" "You have successfully logged out from the Central Authentication Service. " - "For security reasons, exit your web browser." + "For security reasons, close your web browser." ) elif session_nb > 1: logout_msg = _( @@ -197,7 +197,7 @@ class LogoutView(View, LogoutMixin): logout_msg = _( "

Logout successful

" "You were already logged out from the Central Authentication Service. " - "For security reasons, exit your web browser." + "For security reasons, close your web browser." ) # depending of settings, redirect to the login page with a logout message or display @@ -261,7 +261,7 @@ class FederateAuth(CsrfExemptView): """ # if settings.CAS_FEDERATE is not True redirect to the login page if not settings.CAS_FEDERATE: - logger.warning("CAS_FEDERATE is False, set it to True to use the federated mode") + logger.warning("CAS_FEDERATE is False, set it to True to use federation") return redirect("cas_server:login") # POST with a provider suffix, this is probably an SLO request. csrf is disabled for # allowing SLO requests reception @@ -305,13 +305,13 @@ class FederateAuth(CsrfExemptView): """ # if settings.CAS_FEDERATE is not True redirect to the login page if not settings.CAS_FEDERATE: - logger.warning("CAS_FEDERATE is False, set it to True to use the federated mode") + logger.warning("CAS_FEDERATE is False, set it to True to use federation") return redirect("cas_server:login") renew = bool(request.GET.get('renew') and request.GET['renew'] != "False") # Is the user is already authenticated, no need to request authentication to the user # identity provider. if self.request.session.get("authenticated") and not renew: - logger.warning("User already authenticated, dropping federate authentication request") + logger.warning("User already authenticated, dropping federated authentication request") return redirect("cas_server:login") try: # get the identity provider from its suffix @@ -320,7 +320,7 @@ class FederateAuth(CsrfExemptView): auth = self.get_cas_client(request, provider, renew) # if no ticket submited, redirect to the identity provider CAS login page if 'ticket' not in request.GET: - logger.info("Trying to authenticate again %s" % auth.provider.server_url) + logger.info("Trying to authenticate %s again" % auth.provider.server_url) return HttpResponseRedirect(auth.get_login_url()) else: ticket = request.GET['ticket'] @@ -360,8 +360,8 @@ class FederateAuth(CsrfExemptView): else: logger.info( ( - "Got a invalid ticket %s from %s for service %s. " - "Retrying to authenticate" + "Got an invalid ticket %s from %s for service %s. " + "Retrying authentication" ) % ( ticket, auth.provider.server_url, @@ -485,7 +485,7 @@ class LoginView(View, LogoutMixin): def post(self, request, *args, **kwargs): """ - methode called on POST request on this view + method called on POST request on this view :param django.http.HttpRequest request: The current request object """ @@ -497,7 +497,7 @@ class LoginView(View, LogoutMixin): messages.add_message( self.request, messages.ERROR, - _(u"Invalid login ticket, please retry to login") + _(u"Invalid login ticket, please try to log in again") ) elif ret == self.USER_LOGIN_OK: # On successful login, update the :class:`models.User` ``date`` @@ -554,7 +554,7 @@ class LoginView(View, LogoutMixin): """ if not self.check_lt(): self.init_form(self.request.POST) - logger.warning("Receive an invalid login ticket") + logger.warning("Received an invalid login ticket") return self.INVALID_LOGIN_TICKET elif not self.request.session.get("authenticated") or self.renew: # authentication request receive, initialize the form to use @@ -569,10 +569,10 @@ class LoginView(View, LogoutMixin): logger.info("User %s successfully authenticated" % self.request.session["username"]) return self.USER_LOGIN_OK else: - logger.warning("A logging attemps failed") + logger.warning("A login attempt failed") return self.USER_LOGIN_FAILURE else: - logger.warning("Receuve a logging attempt whereas the user is already logged") + logger.warning("Received a login attempt for an already-active user") return self.USER_ALREADY_LOGGED def init_get(self, request): @@ -600,7 +600,7 @@ class LoginView(View, LogoutMixin): def get(self, request, *args, **kwargs): """ - methode called on GET request on this view + method called on GET request on this view :param django.http.HttpRequest request: The current request object """ @@ -667,7 +667,7 @@ class LoginView(View, LogoutMixin): def service_login(self): """ - Perform login agains a service + Perform login against a service :return: * The rendering of the ``settings.CAS_WARN_TEMPLATE`` if the user asked to be @@ -949,7 +949,7 @@ class Auth(CsrfExemptView): @staticmethod def post(request): """ - methode called on POST request on this view + method called on POST request on this view :param django.http.HttpRequest request: The current request object :return: ``HttpResponse(u"yes\\n")`` if the POSTed tuple (username, password, service) @@ -1005,7 +1005,7 @@ class Validate(View): @staticmethod def get(request): """ - methode called on GET request on this view + method called on GET request on this view :param django.http.HttpRequest request: The current request object :return: @@ -1116,7 +1116,7 @@ class ValidateService(View): def get(self, request): """ - methode called on GET request on this view + method called on GET request on this view :param django.http.HttpRequest request: The current request object: :return: The rendering of ``cas_server/serviceValidate.xml`` if no errors is raised, @@ -1284,7 +1284,7 @@ class Proxy(View): def get(self, request): """ - methode called on GET request on this view + method called on GET request on this view :param django.http.HttpRequest request: The current request object: :return: The returned value of :meth:`process_proxy` if no error is raised, @@ -1323,7 +1323,7 @@ class Proxy(View): if not pattern.proxy: raise ValidateError( u'UNAUTHORIZED_SERVICE', - u'the service %s do not allow proxy ticket' % self.target_service + u'the service %s does not allow proxy tickets' % self.target_service ) # is the proxy granting ticket valid ticket = ProxyGrantingTicket.get(self.pgt) @@ -1387,7 +1387,7 @@ class SamlValidate(CsrfExemptView): def post(self, request): """ - methode called on POST request on this view + method called on POST request on this view :param django.http.HttpRequest request: The current request object :return: the rendering of ``cas_server/samlValidate.xml`` if no error is raised, @@ -1417,7 +1417,7 @@ class SamlValidate(CsrfExemptView): ) ) logger.debug( - "SamlValidate: User attributs are:\n%s" % pprint.pformat(self.ticket.attributs) + "SamlValidate: User attributes are:\n%s" % pprint.pformat(self.ticket.attributs) ) return render( @@ -1446,7 +1446,7 @@ class SamlValidate(CsrfExemptView): if ticket.service != self.target: raise SamlValidateError( u'AuthnFailed', - u'TARGET %s do not match ticket service' % self.target + u'TARGET %s does not match ticket service' % self.target ) return ticket except (IndexError, KeyError): From 6185e9c68cd669889085729f484e82d1be3eaaa4 Mon Sep 17 00:00:00 2001 From: Valentin Samir Date: Sun, 18 Sep 2016 11:34:46 +0200 Subject: [PATCH 07/13] Fix more spelling and grammar errors --- CHANGELOG.rst | 1 + cas_server/auth.py | 20 ++++++++++---------- cas_server/federate.py | 2 +- cas_server/models.py | 2 +- cas_server/tests/test_view.py | 6 +++--- cas_server/views.py | 6 +++--- 6 files changed, 19 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ab149b6..bfdccc9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,7 @@ Added Fixed ----- * Allow both unicode and bytes dotted string in utils.import_attr +* Fix some spelling and grammar on log messages. (thanks to Allie Micka) v0.7.4 - 2016-09-07 diff --git a/cas_server/auth.py b/cas_server/auth.py index aa07593..fbd199e 100644 --- a/cas_server/auth.py +++ b/cas_server/auth.py @@ -49,7 +49,7 @@ class AuthUser(object): def test_password(self, password): """ - Tests ``password`` agains the user password. + Tests ``password`` against the user-supplied password. :raises NotImplementedError: always. The method need to be implemented by subclasses """ @@ -74,7 +74,7 @@ class DummyAuthUser(AuthUser): # pragma: no cover def test_password(self, password): """ - Tests ``password`` agains the user password. + Tests ``password`` against the user-supplied password. :param unicode password: a clear text password as submited by the user. :return: always ``False`` @@ -102,7 +102,7 @@ class TestAuthUser(AuthUser): def test_password(self, password): """ - Tests ``password`` agains the user password. + Tests ``password`` against the user-supplied password. :param unicode password: a clear text password as submited by the user. :return: ``True`` if :attr:`username` is valid and @@ -149,7 +149,7 @@ class MysqlAuthUser(DBAuthUser): # pragma: no cover """ DEPRECATED, use :class:`SqlAuthUser` instead. - A mysql authentication class: authenticate user agains a mysql database + A mysql authentication class: authenticate user against a mysql database :param unicode username: A username, stored in the :attr:`username` class attribute. Valid value are fetched from the MySQL database set with @@ -188,7 +188,7 @@ class MysqlAuthUser(DBAuthUser): # pragma: no cover def test_password(self, password): """ - Tests ``password`` agains the user password. + Tests ``password`` against the user-supplied password. :param unicode password: a clear text password as submited by the user. :return: ``True`` if :attr:`username` is valid and ``password`` is @@ -208,7 +208,7 @@ class MysqlAuthUser(DBAuthUser): # pragma: no cover class SqlAuthUser(DBAuthUser): # pragma: no cover """ - A SQL authentication class: authenticate user agains a SQL database. The SQL database + A SQL authentication class: authenticate user against a SQL database. The SQL database must be configures in settings.py as ``settings.DATABASES['cas_server']``. :param unicode username: A username, stored in the :attr:`username` @@ -238,7 +238,7 @@ class SqlAuthUser(DBAuthUser): # pragma: no cover def test_password(self, password): """ - Tests ``password`` agains the user password. + Tests ``password`` against the user-supplied password. :param unicode password: a clear text password as submited by the user. :return: ``True`` if :attr:`username` is valid and ``password`` is @@ -308,7 +308,7 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover def test_password(self, password): """ - Tests ``password`` agains the user password. + Tests ``password`` against the user-supplied password. :param unicode password: a clear text password as submited by the user. :return: ``True`` if :attr:`username` is valid and ``password`` is @@ -347,7 +347,7 @@ class DjangoAuthUser(AuthUser): # pragma: no cover def test_password(self, password): """ - Tests ``password`` agains the user password. + Tests ``password`` against the user-supplied password. :param unicode password: a clear text password as submited by the user. :return: ``True`` if :attr:`user` is valid and ``password`` is @@ -426,7 +426,7 @@ class CASFederateAuth(AuthUser): def test_password(self, ticket): """ - Tests ``password`` agains the user password. + Tests ``password`` against the user-supplied password. :param unicode password: The CAS tickets just used to validate the user authentication against its CAS backend. diff --git a/cas_server/federate.py b/cas_server/federate.py index d977771..4ad05e1 100644 --- a/cas_server/federate.py +++ b/cas_server/federate.py @@ -69,7 +69,7 @@ class CASFederateValidateUser(object): def verify_ticket(self, ticket): """ - test ``ticket`` agains the CAS provider, if valid, create a + test ``ticket`` against the CAS provider, if valid, create a :class:`FederatedUser` matching provider returned username and attributes. diff --git a/cas_server/models.py b/cas_server/models.py index d13f553..7a421a5 100644 --- a/cas_server/models.py +++ b/cas_server/models.py @@ -433,7 +433,7 @@ class ServicePattern(models.Model): """ Bases: :class:`django.db.models.Model` - Allowed services pattern agains services are tested to + Allowed services pattern against services are tested to """ class Meta: ordering = ("pos", ) diff --git a/cas_server/tests/test_view.py b/cas_server/tests/test_view.py index 22b3bd7..c1ffe01 100644 --- a/cas_server/tests/test_view.py +++ b/cas_server/tests/test_view.py @@ -825,7 +825,7 @@ class LogoutTestCase(TestCase): @override_settings(CAS_ENABLE_AJAX_AUTH=True) def test_ajax_logout(self): """ - test ajax logout. These methode are here, but I do not really see an use case for + test ajax logout. These methods are here, but I do not really see an use case for javascript logout """ # get a client that is authenticated @@ -1728,7 +1728,7 @@ class ProxyTestCase(TestCase, BaseServicePattern, XmlContent): self.assert_error( response, "UNAUTHORIZED_SERVICE", - 'the service %s do not allow proxy ticket' % params['service'] + 'the service %s does not allow proxy tickets' % params['service'] ) self.service_pattern.proxy = True @@ -1974,7 +1974,7 @@ class SamlValidateTestCase(TestCase, BaseServicePattern, XmlContent): self.assert_error( response, "AuthnFailed", - 'TARGET %s do not match ticket service' % bad_target + 'TARGET %s does not match ticket service' % bad_target ) def test_saml_bad_xml(self): diff --git a/cas_server/views.py b/cas_server/views.py index 9f8c83c..c14e19f 100644 --- a/cas_server/views.py +++ b/cas_server/views.py @@ -191,7 +191,7 @@ class LogoutView(View, LogoutMixin): "

Logout successful

" "You have successfully logged out from %s sessions of the Central " "Authentication Service. " - "For security reasons, exit your web browser." + "For security reasons, close your web browser." ) % session_nb else: logout_msg = _( @@ -229,7 +229,7 @@ class LogoutView(View, LogoutMixin): class FederateAuth(CsrfExemptView): """ - view to authenticated user agains a backend CAS then CAS_FEDERATE is True + view to authenticated user against a backend CAS then CAS_FEDERATE is True csrf is disabled for allowing SLO requests reception. """ @@ -801,7 +801,7 @@ class LoginView(View, LogoutMixin): else: return utils.redirect_params("cas_server:login", params=self.request.GET) - # if login agains a service + # if login against a service if self.service: return self.service_login() # else display the logged template From e77dbbcd0358bb4b7219f9242653b06079477a91 Mon Sep 17 00:00:00 2001 From: Valentin Samir Date: Sun, 18 Sep 2016 11:40:22 +0200 Subject: [PATCH 08/13] Update french translation --- CHANGELOG.rst | 4 +++ cas_server/locale/fr/LC_MESSAGES/django.mo | Bin 10143 -> 10151 bytes cas_server/locale/fr/LC_MESSAGES/django.po | 32 ++++++++++----------- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bfdccc9..519c400 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,10 @@ Fixed * Allow both unicode and bytes dotted string in utils.import_attr * Fix some spelling and grammar on log messages. (thanks to Allie Micka) +Changed +------- +* Update french translation + v0.7.4 - 2016-09-07 =================== diff --git a/cas_server/locale/fr/LC_MESSAGES/django.mo b/cas_server/locale/fr/LC_MESSAGES/django.mo index 603a6db75044837325395944d89391c04108e251..74d9ce92820483153aad3f48ad41d21e07c589ce 100644 GIT binary patch delta 1525 zcmX}sSx8h-7{KvwY-U_C%QCY@>!ziojoq0hQ?sJYq_kQLO+BVcnv4webo27#PRqQ({Xre zd^{&r!cxG1kAVqThx%X}X5%T;2cl?VA8x{*I2YHB5SfI>(Scp)#BQv>hggQct$s_J~}jhiiVGY3#c!ChNM;ATQ_{cQu>3aq4ltQy?R7fYb<0qU(K`h2Bm&hvI zgc{;boP*zxN2Qp2<>FGDj7>NlJ5X!t9x|sqN3EHUn1hAUVkIU~i52L_t=NH@(l4kB$u{D(G8^?h%~%koae{`fycek( zd5xOe&$ts4xR^@_TTvr&n$>C~Ug0^Ml*g9BUepPn;ClRl23Ajrr=}UV)8C6ee1~CO zS=Q8eE@xsn{R*q!h8mImsQdQ}4#O^7j(2bwCh$3$`xKF6RL5s4)vI=!S*P=$~1QHlY;M9pTBERgeu{5NLlLCn) zMweI+ndWfW#z&So+H4Ye;Bc)o8P#a(2kN1_Ow*^4oyVyksP#p;$WqRsLsQOo$99_? zNlGle`W^0w+nJZc;$wRnVux0%9@L3Aqs`84n`7&aP)kFkC&?43O{)9fI~;FCE0fO- zOK;uPZnTDs9ic`e5H#u=>!SmyZxg*`ZcnM<@sxYZicGI*rmXb4S2yeqv;;yy_u~52 chH}GedP>~p(0wz_=%0*Ko2@LGl6f-y9|#MwyZ`_I delta 1492 zcmYk+OGs2<6u|K_>G*0iAET9%Hfkzq<0GarHMP<#vm`~*GU$QY!}7s^1;uPyL`g~) zgUC<_l3b)YRxXM{2p8o}auHHcR8$N?5hN}8ALj;LocVp{n|tr~-E+=$xMr}X&+Q!) zk%3T=J}eFsxrQ^CfQ{iIH?RZ8F(^W03qHmY^qabti)hkK3?1Qe-V&MJtYA2##SfKEbW{(~NVX zL~@DGAX9QX$}8exaGwcXcn)>pU#L5eTOq=qWbvgNDa1Og#+NvRo3JHXWCsr5X?%yl zSj9z7tikQrftsOl)D2I28SG;)gZhMy*uV*uNYdk!C!HdR?~}dr=p@V$OT7GjKC;6ZOWgkh03WdBPVgAYMRC?RvJazvrPYP=r}n zj#Ne3vDYGU5w%xrq%n%P93K(atrWS3;j75HQ2#Lon(9Mz$S8KAA5&o4C=paRXjP?Wu>zn(`X8XFg&Qrt($H`clR~Z*U3q#39^?lXwdM zU<0<$AQ|`^wKSiQ^2-A5LhZz2sKgU^8MUNeP&Z1EKvKzBZY^o_hh zt?g&*#6?Ucg>9Uq8R=!Wnu%E)z?Eqtyje1Vy5I{uh~LqPD&q13q$FvOfeXd2#P!EzAS;j|?VB>eNBc6+8Hy;{gsW7@l zm$BaJuoM{;)^3a4cxH9fYw0xG`ha\n" "Language-Team: django \n" "Language: fr\n" @@ -16,7 +16,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -"X-Generator: Poedit 1.8.8\n" +"X-Generator: Poedit 1.8.9\n" #: apps.py:25 templates/cas_server/base.html:7 #: templates/cas_server/base.html:26 @@ -34,37 +34,37 @@ msgstr "" "identifiant et votre mot de passe chaque fois que vous changez de site, " "jusqu'à ce que votre session expire ou que vous vous déconnectiez." -#: forms.py:84 +#: forms.py:85 msgid "Identity provider" msgstr "fournisseur d'identité" -#: forms.py:88 forms.py:107 +#: forms.py:89 forms.py:111 msgid "Warn me before logging me into other sites." msgstr "Prévenez-moi avant d'accéder à d'autres services." -#: forms.py:92 +#: forms.py:93 msgid "Remember the identity provider" msgstr "Se souvenir du fournisseur d'identité" -#: forms.py:102 models.py:594 +#: forms.py:104 models.py:594 msgid "username" msgstr "nom d'utilisateur" -#: forms.py:104 +#: forms.py:108 msgid "password" msgstr "mot de passe" -#: forms.py:126 +#: forms.py:131 msgid "The credentials you provided cannot be determined to be authentic." msgstr "Les informations transmises n'ont pas permis de vous authentifier." -#: forms.py:178 +#: forms.py:183 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" -#: forms.py:192 +#: forms.py:197 msgid "service" msgstr "service" @@ -331,7 +331,7 @@ msgstr "Connexion" msgid "Connect to the service" msgstr "Se connecter au service" -#: utils.py:736 +#: utils.py:744 #, python-format msgid "\"%(value)s\" is not a valid regular expression" msgstr "\"%(value)s\" n'est pas une expression rationnelle valide" @@ -339,7 +339,7 @@ msgstr "\"%(value)s\" n'est pas une expression rationnelle valide" #: views.py:185 msgid "" "

Logout successful

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

Déconnexion réussie

Vous vous êtes déconnecté(e) du Service Central " "d'Authentification. Pour des raisons de sécurité, veuillez fermer votre " @@ -349,7 +349,7 @@ msgstr "" #, python-format msgid "" "

Logout successful

You have successfully logged out from %s sessions " -"of the Central Authentication Service. For security reasons, exit your web " +"of the Central Authentication Service. For security reasons, close your web " "browser." msgstr "" "

Déconnexion réussie

Vous vous êtes déconnecté(e) de %s sessions du " @@ -359,7 +359,7 @@ msgstr "" #: views.py:198 msgid "" "

Logout successful

You were already logged out from the Central " -"Authentication Service. For security reasons, exit your web browser." +"Authentication Service. For security reasons, close your web browser." msgstr "" "

Déconnexion réussie

Vous étiez déjà déconnecté(e) du Service Central " "d'Authentification. Pour des raisons de sécurité, veuillez fermer votre " @@ -375,7 +375,7 @@ msgstr "" "ticket %(ticket)s: %(error)r" #: views.py:500 -msgid "Invalid login ticket, please retry to login" +msgid "Invalid login ticket, please try to log in again" msgstr "Ticket de connexion invalide, merci de réessayé de vous connecter" #: views.py:692 From f1fed48b21c548d800cd35543a8e31622b329b70 Mon Sep 17 00:00:00 2001 From: Valentin Samir Date: Fri, 7 Oct 2016 15:22:49 +0200 Subject: [PATCH 09/13] Add ldap bind auth method and CAS_TGT_VALIDITY parameter. Fix #18 --- CHANGELOG.rst | 3 ++ README.rst | 17 ++++++ cas_server/admin.py | 33 +++++++++++- cas_server/auth.py | 53 +++++++++++++++++- cas_server/default_settings.py | 4 ++ .../management/commands/cas_clean_sessions.py | 1 + .../migrations/0011_auto_20161007_1258.py | 38 +++++++++++++ cas_server/models.py | 54 +++++++++++++++++-- cas_server/tests/auth.py | 29 ++++++++++ cas_server/tests/mixin.py | 11 ++++ cas_server/tests/test_models.py | 54 +++++++++++++++++++ cas_server/views.py | 1 + 12 files changed, 289 insertions(+), 9 deletions(-) create mode 100644 cas_server/migrations/0011_auto_20161007_1258.py create mode 100644 cas_server/tests/auth.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 519c400..1b1caee 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,6 +12,9 @@ Unreleased Added ----- * Add a test for login with missing parameter (username or password or both) +* Add ldap auth using bind method (use the user credentials to bind the the ldap server and let the + server check the credentials) +* Add CAS_TGT_VALIDITY parameter: Max time after with the user MUST reauthenticate. Fixed ----- diff --git a/README.rst b/README.rst index 283a5ed..715bf3e 100644 --- a/README.rst +++ b/README.rst @@ -268,6 +268,11 @@ Authentication settings which inactive users are logged out. The default is ``1209600`` (2 weeks). You probably should reduce it to something like ``86400`` seconds (1 day). +* ``CAS_TGT_VALIDITY``: Max time after with the user MUST reauthenticate. Let it to `None` for no + max time.This can be used to force refreshing cached informations only available upon user + authentication like the user attributes in federation mode or with the ldap auth in bind mode. + The default is ``None``. + * ``CAS_PROXY_CA_CERTIFICATE_PATH``: Path to certificate authorities file. Usually on linux the local CAs are in ``/etc/ssl/certs/ca-certificates.crt``. The default is ``True`` which tell requests to use its internal certificat authorities. Settings it to ``False`` should @@ -416,6 +421,14 @@ Only usefull if you are using the ldap authentication backend: The hashed password in the database is compare to the hexadecimal digest of the clear password hashed with the corresponding algorithm. * ``"plain"``, the password in the database must be in clear. + * ``"bind``, the user credentials are used to bind to the ldap database and retreive the user + attribute. In this mode, the settings ``CAS_LDAP_PASSWORD_ATTR`` and ``CAS_LDAP_PASSWORD_CHARSET`` + are ignored, and it is the ldap server that perform password check. The counterpart is that + the user attributes are only available upon user password check and so are cached for later + use. All the other modes directly fetch the user attributes from the database whenever there + are needed. This mean that is you use this mode, they can be some difference between the + attributes in database and the cached ones if changes happend in the database after the user + authentiate. See the parameter ``CAS_TGT_VALIDITY`` to force user to reauthenticate periodically. The default is ``"ldap"``. * ``CAS_LDAP_PASSWORD_CHARSET``: Charset the LDAP users passwords was hash with. This is needed to @@ -585,6 +598,10 @@ 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. +In federation mode, the user attributes are cached upon user authentication. See the settings +``CAS_TGT_VALIDITY`` to force users to reauthenticate periodically and allow ``django-cas-server`` +to refresh cached attributes. + 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. diff --git a/cas_server/admin.py b/cas_server/admin.py index 6e5c318..05ba1f5 100644 --- a/cas_server/admin.py +++ b/cas_server/admin.py @@ -9,10 +9,12 @@ # # (c) 2015-2016 Valentin Samir """module for the admin interface of the app""" +from .default_settings import settings + 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 .models import FederatedIendityProvider, FederatedUser, UserAttributes from .forms import TicketForm @@ -167,6 +169,33 @@ class FederatedIendityProviderAdmin(admin.ModelAdmin): list_display = ('verbose_name', 'suffix', 'display') -admin.site.register(User, UserAdmin) +class FederatedUserAdmin(admin.ModelAdmin): + """ + Bases: :class:`django.contrib.admin.ModelAdmin` + + :class:`FederatedUser` in admin + interface + """ + #: Fields to display on a object. + fields = ('username', 'provider', 'last_update') + #: Fields to display on the list of class:`FederatedUserAdmin` objects. + list_display = ('username', 'provider', 'last_update') + + +class UserAttributesAdmin(admin.ModelAdmin): + """ + Bases: :class:`django.contrib.admin.ModelAdmin` + + :class:`UserAttributes` in admin + interface + """ + #: Fields to display on a object. + fields = ('username', '_attributs') + + admin.site.register(ServicePattern, ServicePatternAdmin) admin.site.register(FederatedIendityProvider, FederatedIendityProviderAdmin) +if settings.DEBUG: # pragma: no branch (we always test with DEBUG True) + admin.site.register(User, UserAdmin) + admin.site.register(FederatedUser, FederatedUserAdmin) + admin.site.register(UserAttributes, UserAttributesAdmin) diff --git a/cas_server/auth.py b/cas_server/auth.py index fbd199e..bcdce71 100644 --- a/cas_server/auth.py +++ b/cas_server/auth.py @@ -30,7 +30,7 @@ try: # pragma: no cover except ImportError: ldap3 = None -from .models import FederatedUser +from .models import FederatedUser, UserAttributes from .utils import check_password, dictfetchall @@ -284,6 +284,10 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover def __init__(self, username): if not ldap3: raise RuntimeError("Please install ldap3 before using the LdapAuthUser backend") + if not settings.CAS_LDAP_BASE_DN: + raise ValueError( + "You must define CAS_LDAP_BASE_DN for using the ldap authentication backend" + ) # in case we got deconnected from the database, retry to connect 2 times for retry_nb in range(3): try: @@ -294,6 +298,8 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover attributes=ldap3.ALL_ATTRIBUTES ) and len(conn.entries) == 1: user = conn.entries[0].entry_get_attributes_dict() + # store the user dn + user["dn"] = conn.entries[0].entry_get_dn() if user.get(settings.CAS_LDAP_USERNAME_ATTR): self.user = user super(LdapAuthUser, self).__init__(user[settings.CAS_LDAP_USERNAME_ATTR][0]) @@ -315,7 +321,34 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover correct, ``False`` otherwise. :rtype: bool """ - if self.user and self.user.get(settings.CAS_LDAP_PASSWORD_ATTR): + if settings.CAS_LDAP_PASSWORD_CHECK == "bind": + try: + conn = ldap3.Connection( + settings.CAS_LDAP_SERVER, + self.user["dn"], + password, + auto_bind=True + ) + try: + # fetch the user attribute + if conn.search( + settings.CAS_LDAP_BASE_DN, + settings.CAS_LDAP_USER_QUERY % ldap3.utils.conv.escape_bytes(self.username), + attributes=ldap3.ALL_ATTRIBUTES + ) and len(conn.entries) == 1: + attributes = conn.entries[0].entry_get_attributes_dict() + attributes["dn"] = conn.entries[0].entry_get_dn() + # cache the attributes locally as we wont have access to the user password + # later. + user = UserAttributes.objects.get_or_create(username=self.username)[0] + user.attributs = attributes + user.save() + finally: + conn.unbind() + return True + except (ldap3.LDAPBindError, ldap3.LDAPCommunicationError): + return False + elif self.user and self.user.get(settings.CAS_LDAP_PASSWORD_ATTR): return check_password( settings.CAS_LDAP_PASSWORD_CHECK, password, @@ -325,6 +358,22 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover else: return False + def attributs(self): + """ + The user attributes. + + :return: a :class:`dict` with the user attributes. Attributes may be :func:`unicode` + or :class:`list` of :func:`unicode`. If the user do not exists, the returned + :class:`dict` is empty. + :rtype: dict + :raises NotImplementedError: if the password check method in `CAS_LDAP_PASSWORD_CHECK` + do not allow to fetch the attributes without the user credentials. + """ + if settings.CAS_LDAP_PASSWORD_CHECK == "bind": + raise NotImplementedError() + else: + return super(LdapAuthUser, self).attributs() + class DjangoAuthUser(AuthUser): # pragma: no cover """ diff --git a/cas_server/default_settings.py b/cas_server/default_settings.py index 238fc0a..737bb84 100644 --- a/cas_server/default_settings.py +++ b/cas_server/default_settings.py @@ -58,6 +58,10 @@ CAS_SLO_MAX_PARALLEL_REQUESTS = 10 CAS_SLO_TIMEOUT = 5 #: Shared to transmit then using the view :class:`cas_server.views.Auth` CAS_AUTH_SHARED_SECRET = '' +#: Max time after with the user MUST reauthenticate. Let it to `None` for no max time. +#: This can be used to force refreshing cached informations only available upon user authentication +#: like the user attributes in federation mode or with the ldap auth in bind mode. +CAS_TGT_VALIDITY = None #: Number of seconds the service tickets and proxy tickets are valid. This is the maximal time diff --git a/cas_server/management/commands/cas_clean_sessions.py b/cas_server/management/commands/cas_clean_sessions.py index 5de4ebf..d0c63fe 100644 --- a/cas_server/management/commands/cas_clean_sessions.py +++ b/cas_server/management/commands/cas_clean_sessions.py @@ -23,4 +23,5 @@ class Command(BaseCommand): def handle(self, *args, **options): models.User.clean_deleted_sessions() + models.UserAttributes.clean_old_entries() models.NewVersionWarning.send_mails() diff --git a/cas_server/migrations/0011_auto_20161007_1258.py b/cas_server/migrations/0011_auto_20161007_1258.py new file mode 100644 index 0000000..e21f8ec --- /dev/null +++ b/cas_server/migrations/0011_auto_20161007_1258.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.1 on 2016-10-07 12:58 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('cas_server', '0010_auto_20160824_2112'), + ] + + operations = [ + migrations.CreateModel( + name='UserAttributes', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('_attributs', models.TextField(blank=True, default=None, null=True)), + ('username', models.CharField(max_length=155, unique=True)), + ], + options={ + 'verbose_name': 'User attributes cache', + 'verbose_name_plural': 'User attributes caches', + }, + ), + migrations.AlterModelOptions( + name='federateduser', + options={'verbose_name': 'Federated user', 'verbose_name_plural': 'Federated users'}, + ), + migrations.AddField( + model_name='user', + name='last_login', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + ] diff --git a/cas_server/models.py b/cas_server/models.py index 7a421a5..be871b6 100644 --- a/cas_server/models.py +++ b/cas_server/models.py @@ -163,6 +163,8 @@ class FederatedUser(JsonAttributes): """ class Meta: unique_together = ("username", "provider") + verbose_name = _("Federated user") + verbose_name_plural = _("Federated users") #: The user username returned by the CAS backend on successful ticket validation username = models.CharField(max_length=124) #: A foreign key to :class:`FederatedIendityProvider` @@ -233,6 +235,30 @@ class FederateSLO(models.Model): federate_slo.delete() +@python_2_unicode_compatible +class UserAttributes(JsonAttributes): + """ + Bases: :class:`JsonAttributes` + + Local cache of the user attributes, used then needed + """ + class Meta: + verbose_name = _("User attributes cache") + verbose_name_plural = _("User attributes caches") + #: The username of the user for which we cache attributes + username = models.CharField(max_length=155, unique=True) + + def __str__(self): + return self.username + + @classmethod + def clean_old_entries(cls): + """Remove :class:`UserAttributes` for which no more :class:`User` exists.""" + for user in cls.objects.all(): + if User.objects.filter(username=user.username).count() == 0: + user.delete() + + @python_2_unicode_compatible class User(models.Model): """ @@ -250,6 +276,8 @@ class User(models.Model): username = models.CharField(max_length=30) #: Last time the authenticated user has do something (auth, fetch ticket, etc…) date = models.DateTimeField(auto_now=True) + #: last time the user logged + last_login = models.DateTimeField(auto_now_add=True) def delete(self, *args, **kwargs): """ @@ -269,9 +297,12 @@ class User(models.Model): Remove :class:`User` objects inactive since more that :django:setting:`SESSION_COOKIE_AGE` and send corresponding SingleLogOut requests. """ - users = cls.objects.filter( - date__lt=(timezone.now() - timedelta(seconds=settings.SESSION_COOKIE_AGE)) - ) + filter = Q(date__lt=(timezone.now() - timedelta(seconds=settings.SESSION_COOKIE_AGE))) + if settings.CAS_TGT_VALIDITY is not None: + filter |= Q( + last_login__lt=(timezone.now() - timedelta(seconds=settings.CAS_TGT_VALIDITY)) + ) + users = cls.objects.filter(filter) for user in users: user.logout() users.delete() @@ -288,9 +319,22 @@ class User(models.Model): def attributs(self): """ Property. - A fresh :class:`dict` for the user attributes, using ``settings.CAS_AUTH_CLASS`` + A fresh :class:`dict` for the user attributes, using ``settings.CAS_AUTH_CLASS`` if + possible, and if not, try to fallback to cached attributes (actually only used for ldap + auth class with bind password check mthode). """ - return utils.import_attr(settings.CAS_AUTH_CLASS)(self.username).attributs() + try: + return utils.import_attr(settings.CAS_AUTH_CLASS)(self.username).attributs() + except NotImplementedError: + try: + user = UserAttributes.objects.get(username=self.username) + attributes = user.attributs + if attributes is not None: + return attributes + else: + return {} + except UserAttributes.DoesNotExist: + return {} def __str__(self): return u"%s - %s" % (self.username, self.session_key) diff --git a/cas_server/tests/auth.py b/cas_server/tests/auth.py new file mode 100644 index 0000000..436cbce --- /dev/null +++ b/cas_server/tests/auth.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License version 3 for +# more details. +# +# You should have received a copy of the GNU General Public License version 3 +# along with this program; if not, write to the Free Software Foundation, Inc., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# (c) 2016 Valentin Samir + +from cas_server import auth + + +class TestCachedAttributesAuthUser(auth.TestAuthUser): + """ + A test authentication class only working for one unique user. + + :param unicode username: A username, stored in the :attr:`username` + class attribute. The uniq valid value is ``settings.CAS_TEST_USER``. + """ + def attributs(self): + """ + The user attributes. + + :raises NotImplementedError: as this class do not support fetching user attributes + """ + raise NotImplementedError() diff --git a/cas_server/tests/mixin.py b/cas_server/tests/mixin.py index e4a5c0d..d791b53 100644 --- a/cas_server/tests/mixin.py +++ b/cas_server/tests/mixin.py @@ -185,6 +185,17 @@ class UserModels(object): ).update(date=new_date) return client + @staticmethod + def tgt_expired_user(sec): + """return a user logged since sec seconds""" + client = get_auth_client() + new_date = timezone.now() - timedelta(seconds=(sec)) + models.User.objects.filter( + username=settings.CAS_TEST_USER, + session_key=client.session.session_key + ).update(last_login=new_date) + return client + @staticmethod def get_user(client): """return the user associated with an authenticated client""" diff --git a/cas_server/tests/test_models.py b/cas_server/tests/test_models.py index e0d417e..2139703 100644 --- a/cas_server/tests/test_models.py +++ b/cas_server/tests/test_models.py @@ -114,6 +114,24 @@ class FederateSLOTestCase(TestCase, UserModels): models.FederateSLO.objects.get(username="test1@example.com") +@override_settings(CAS_AUTH_CLASS='cas_server.auth.TestAuthUser') +class UserAttributesTestCase(TestCase, UserModels): + """test for the user attributes cache model""" + def test_clean_old_entries(self): + """test the clean_old_entries methode""" + client = get_auth_client() + user = self.get_user(client) + models.UserAttributes.objects.create(username=settings.CAS_TEST_USER) + + # test that attribute cache is removed for non existant users + self.assertEqual(len(models.UserAttributes.objects.all()), 1) + models.UserAttributes.clean_old_entries() + self.assertEqual(len(models.UserAttributes.objects.all()), 1) + user.delete() + models.UserAttributes.clean_old_entries() + self.assertEqual(len(models.UserAttributes.objects.all()), 0) + + @override_settings(CAS_AUTH_CLASS='cas_server.auth.TestAuthUser') class UserTestCase(TestCase, UserModels): """tests for the user models""" @@ -144,6 +162,24 @@ class UserTestCase(TestCase, UserModels): # assert the user has being well delete self.assertEqual(len(models.User.objects.all()), 0) + @override_settings(CAS_TGT_VALIDITY=3600) + def test_clean_old_entries_tgt_expired(self): + """test clean_old_entiers with CAS_TGT_VALIDITY set""" + # get an authenticated client + client = self.tgt_expired_user(settings.CAS_TGT_VALIDITY + 60) + # assert the user exists before being cleaned + self.assertEqual(len(models.User.objects.all()), 1) + # assert the last lofin date is before the expiry date + self.assertTrue( + self.get_user(client).last_login < ( + timezone.now() - timedelta(seconds=settings.CAS_TGT_VALIDITY) + ) + ) + # delete old inactive users + models.User.clean_old_entries() + # assert the user has being well delete + self.assertEqual(len(models.User.objects.all()), 0) + def test_clean_deleted_sessions(self): """test clean_deleted_sessions""" # get an authenticated client @@ -177,6 +213,24 @@ class UserTestCase(TestCase, UserModels): self.assertFalse(models.ServiceTicket.objects.all()) self.assertTrue(client2.session.get("authenticated")) + @override_settings(CAS_AUTH_CLASS='cas_server.tests.auth.TestCachedAttributesAuthUser') + def test_cached_attributs(self): + """ + Test gettting user attributes from cache for auth method that do not support direct + fetch (link the ldap bind auth methode) + """ + client = get_auth_client() + user = self.get_user(client) + # if no cache is defined, the attributes are empty + self.assertEqual(user.attributs, {}) + user_attr = models.UserAttributes.objects.create(username=settings.CAS_TEST_USER) + # if a cache is defined but without atrributes, also empty + self.assertEqual(user.attributs, {}) + user_attr.attributs = settings.CAS_TEST_ATTRIBUTES + user_attr.save() + # attributes are what is found in the cache + self.assertEqual(user.attributs, settings.CAS_TEST_ATTRIBUTES) + @override_settings(CAS_AUTH_CLASS='cas_server.auth.TestAuthUser') class TicketTestCase(TestCase, UserModels, BaseServicePattern): diff --git a/cas_server/views.py b/cas_server/views.py index c14e19f..b3d3a1e 100644 --- a/cas_server/views.py +++ b/cas_server/views.py @@ -506,6 +506,7 @@ class LoginView(View, LogoutMixin): username=self.request.session['username'], session_key=self.request.session.session_key )[0] + self.user.last_login = timezone.now() self.user.save() elif ret == self.USER_LOGIN_FAILURE: # bad user login if settings.CAS_FEDERATE: From b80947755ab0afaa3055bd3a8c661ad79be2def5 Mon Sep 17 00:00:00 2001 From: Valentin Samir Date: Fri, 7 Oct 2016 15:36:11 +0200 Subject: [PATCH 10/13] Add module tests.auth a docstring --- cas_server/tests/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cas_server/tests/auth.py b/cas_server/tests/auth.py index 436cbce..ff6374e 100644 --- a/cas_server/tests/auth.py +++ b/cas_server/tests/auth.py @@ -9,7 +9,7 @@ # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # (c) 2016 Valentin Samir - +"""Some test authentication classes for the CAS""" from cas_server import auth From 64d3901ec4df90800154cba7014bb4a158d792d4 Mon Sep 17 00:00:00 2001 From: Valentin Samir Date: Sun, 20 Nov 2016 16:50:49 +0100 Subject: [PATCH 11/13] Remove spaceless in forms, fix css class errors --- CHANGELOG.rst | 1 + cas_server/templates/cas_server/form.html | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1b1caee..daad0c4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -20,6 +20,7 @@ Fixed ----- * Allow both unicode and bytes dotted string in utils.import_attr * Fix some spelling and grammar on log messages. (thanks to Allie Micka) +* Fix froms css class error on success/error due to a scpaless block Changed ------- diff --git a/cas_server/templates/cas_server/form.html b/cas_server/templates/cas_server/form.html index 5571a7b..405dedd 100644 --- a/cas_server/templates/cas_server/form.html +++ b/cas_server/templates/cas_server/form.html @@ -6,13 +6,13 @@ {% endfor %} {% for field in form %}{% if not field|is_hidden %} -
{% spaceless %} +>{% spaceless %} {% if field|is_checkbox %}
{% else %} From 9db40bdbcb98d946cc57d858d311fc85b475f1d4 Mon Sep 17 00:00:00 2001 From: Valentin Samir Date: Sun, 20 Nov 2016 16:52:28 +0100 Subject: [PATCH 12/13] Disable pip cache then installing with make install --- CHANGELOG.rst | 1 + Makefile | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index daad0c4..09e69ba 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,6 +21,7 @@ Fixed * Allow both unicode and bytes dotted string in utils.import_attr * Fix some spelling and grammar on log messages. (thanks to Allie Micka) * Fix froms css class error on success/error due to a scpaless block +* Disable pip cache then installing with make install Changed ------- diff --git a/Makefile b/Makefile index a25dbf5..4025a3a 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ build: install: dist pip -V - pip install --no-deps --upgrade --force-reinstall --find-links ./dist/django-cas-server-${VERSION}.tar.gz django-cas-server + pip install --no-cache-dir --no-deps --upgrade --force-reinstall --find-links ./dist/django-cas-server-${VERSION}.tar.gz django-cas-server uninstall: pip uninstall django-cas-server || true From 00d47790e4ad6864f761cb73a02ff9d8cd961785 Mon Sep 17 00:00:00 2001 From: Valentin Samir Date: Wed, 8 Mar 2017 14:11:26 +0100 Subject: [PATCH 13/13] Update version to 0.8.0 --- CHANGELOG.rst | 4 ++-- cas_server/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 09e69ba..beff5bd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,8 +6,8 @@ All notable changes to this project will be documented in this file. .. contents:: Table of Contents :depth: 2 -Unreleased -========== +v0.8.0 - 2017-03-08 +=================== Added ----- diff --git a/cas_server/__init__.py b/cas_server/__init__.py index 04751e8..07db413 100644 --- a/cas_server/__init__.py +++ b/cas_server/__init__.py @@ -11,7 +11,7 @@ """A django CAS server application""" #: version of the application -VERSION = '0.7.4' +VERSION = '0.8.0' #: path the the application configuration class default_app_config = 'cas_server.apps.CasAppConfig'