Merge pull request #34 from nitmir/dev

Update version to 0.9.0

v0.9.0 - 2017-11-17
===================

Added
-----
* Dutch translation
* Protuguese translation (brazilian variant)
* Support for ldap3 version 2 or more (changes in the API)
  All exception are now in ldap3.core.exceptions, methodes for fetching attritutes and
  dn are renamed.
* Possibility to disable service message boxes on the login pages

Fixed
-----
* Then using the LDAP auth backend with ``bind`` method for password check, do not try to bind
  if the user dn was not found. This was causing the exception
  ``'NoneType' object has no attribute 'getitem'`` describe in #21
* Increase the max size of usernames (30 chars to 250)
* Fix XSS js injection
This commit is contained in:
Valentin Samir 2017-11-17 15:47:00 +01:00 committed by GitHub
commit 4229f871c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 605 additions and 39 deletions

View file

@ -6,6 +6,29 @@ All notable changes to this project will be documented in this file.
.. contents:: Table of Contents .. contents:: Table of Contents
:depth: 2 :depth: 2
v0.9.0 - 2017-11-17
===================
Added
-----
* Dutch translation
* Protuguese translation (brazilian variant)
* Support for ldap3 version 2 or more (changes in the API)
All exception are now in ldap3.core.exceptions, methodes for fetching attritutes and
dn are renamed.
* Possibility to disable service message boxes on the login pages
Fixed
-----
* Then using the LDAP auth backend with ``bind`` method for password check, do not try to bind
if the user dn was not found. This was causing the exception
``'NoneType' object has no attribute 'getitem'`` describe in #21
* Increase the max size of usernames (30 chars to 250)
* Fix XSS js injection
v0.8.0 - 2017-03-08 v0.8.0 - 2017-03-08
=================== ===================

View file

@ -218,7 +218,8 @@ Template settings
} }
if you omit some keys of the dictionnary, the default value for these keys is used. if you omit some keys of the dictionnary, the default value for these keys is used.
* ``CAS_SHOW_SERVICE_MESSAGES``: Messages displayed about the state of the service on the login page.
The default is ``True``.
* ``CAS_INFO_MESSAGES``: Messages displayed in info-boxes on the html pages of the default templates. * ``CAS_INFO_MESSAGES``: Messages displayed in info-boxes on the html pages of the default templates.
It is a dictionnary mapping message name to a message dict. A message dict has 3 keys: It is a dictionnary mapping message name to a message dict. A message dict has 3 keys:

View file

@ -11,7 +11,7 @@
"""A django CAS server application""" """A django CAS server application"""
#: version of the application #: version of the application
VERSION = '0.8.0' VERSION = '0.9.0'
#: path the the application configuration class #: path the the application configuration class
default_app_config = 'cas_server.apps.CasAppConfig' default_app_config = 'cas_server.apps.CasAppConfig'

View file

@ -27,6 +27,7 @@ except ImportError:
try: # pragma: no cover try: # pragma: no cover
import ldap3 import ldap3
import ldap3.core.exceptions
except ImportError: except ImportError:
ldap3 = None ldap3 = None
@ -297,9 +298,19 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover
settings.CAS_LDAP_USER_QUERY % ldap3.utils.conv.escape_bytes(username), settings.CAS_LDAP_USER_QUERY % ldap3.utils.conv.escape_bytes(username),
attributes=ldap3.ALL_ATTRIBUTES attributes=ldap3.ALL_ATTRIBUTES
) and len(conn.entries) == 1: ) and len(conn.entries) == 1:
user = conn.entries[0].entry_get_attributes_dict() # try the new ldap3>=2 API
# store the user dn try:
user["dn"] = conn.entries[0].entry_get_dn() user = conn.entries[0].entry_attributes_as_dict
# store the user dn
user["dn"] = conn.entries[0].entry_dn
# fallback to ldap3<2 API
except (
ldap3.core.exceptions.LDAPKeyError, # ldap3<1 exception
ldap3.core.exceptions.LDAPAttributeError # ldap3<2 exception
):
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): if user.get(settings.CAS_LDAP_USERNAME_ATTR):
self.user = user self.user = user
super(LdapAuthUser, self).__init__(user[settings.CAS_LDAP_USERNAME_ATTR][0]) super(LdapAuthUser, self).__init__(user[settings.CAS_LDAP_USERNAME_ATTR][0])
@ -308,7 +319,7 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover
else: else:
super(LdapAuthUser, self).__init__(username) super(LdapAuthUser, self).__init__(username)
break break
except ldap3.LDAPCommunicationError: except ldap3.core.exceptions.LDAPCommunicationError:
if retry_nb == 2: if retry_nb == 2:
raise raise
@ -321,7 +332,7 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover
correct, ``False`` otherwise. correct, ``False`` otherwise.
:rtype: bool :rtype: bool
""" """
if settings.CAS_LDAP_PASSWORD_CHECK == "bind": if self.user and settings.CAS_LDAP_PASSWORD_CHECK == "bind":
try: try:
conn = ldap3.Connection( conn = ldap3.Connection(
settings.CAS_LDAP_SERVER, settings.CAS_LDAP_SERVER,
@ -336,8 +347,18 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover
settings.CAS_LDAP_USER_QUERY % ldap3.utils.conv.escape_bytes(self.username), settings.CAS_LDAP_USER_QUERY % ldap3.utils.conv.escape_bytes(self.username),
attributes=ldap3.ALL_ATTRIBUTES attributes=ldap3.ALL_ATTRIBUTES
) and len(conn.entries) == 1: ) and len(conn.entries) == 1:
attributes = conn.entries[0].entry_get_attributes_dict() # try the ldap3>=2 API
attributes["dn"] = conn.entries[0].entry_get_dn() try:
attributes = conn.entries[0].entry_attributes_as_dict
# store the user dn
attributes["dn"] = conn.entries[0].entry_dn
# fallback to ldap<2 API
except (
ldap3.core.exceptions.LDAPKeyError, # ldap3<1 exception
ldap3.core.exceptions.LDAPAttributeError # ldap3<2 exception
):
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 # cache the attributes locally as we wont have access to the user password
# later. # later.
user = UserAttributes.objects.get_or_create(username=self.username)[0] user = UserAttributes.objects.get_or_create(username=self.username)[0]
@ -346,7 +367,10 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover
finally: finally:
conn.unbind() conn.unbind()
return True return True
except (ldap3.LDAPBindError, ldap3.LDAPCommunicationError): except (
ldap3.core.exceptions.LDAPBindError,
ldap3.core.exceptions.LDAPCommunicationError
):
return False return False
elif self.user and self.user.get(settings.CAS_LDAP_PASSWORD_ATTR): elif self.user and self.user.get(settings.CAS_LDAP_PASSWORD_ATTR):
return check_password( return check_password(

View file

@ -185,6 +185,8 @@ CAS_NEW_VERSION_EMAIL_WARNING = True
#: You should not change it. #: You should not change it.
CAS_NEW_VERSION_JSON_URL = "https://pypi.python.org/pypi/django-cas-server/json" CAS_NEW_VERSION_JSON_URL = "https://pypi.python.org/pypi/django-cas-server/json"
#: If the service message should be displayed on the login page
CAS_SHOW_SERVICE_MESSAGES = True
#: Messages displayed in a info-box on the html pages of the default templates. #: Messages displayed in a info-box on the html pages of the default templates.
#: ``CAS_INFO_MESSAGES`` is a :class:`dict` mapping message name to a message :class:`dict`. #: ``CAS_INFO_MESSAGES`` is a :class:`dict` mapping message name to a message :class:`dict`.

Binary file not shown.

View file

@ -0,0 +1,398 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-08-22 08:18-0300\n"
"PO-Revision-Date: 2017-08-29 18:09+0200\n"
"Language-Team: Roberto Morati <robertomorati@gmail.com>\n"
"Language: pt_BR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"Last-Translator: Valentin Samir <valentin.samir@crans.org>\n"
"X-Generator: Poedit 1.8.11\n"
#: cas_server/apps.py:25 cas_server/templates/cas_server/base.html:7
#: cas_server/templates/cas_server/base.html:26
msgid "Central Authentication Service"
msgstr "Central de Autenticação de Serviços"
#: cas_server/default_settings.py:201
msgid ""
"The Central Authentication Service grants you access to most of our websites by "
"authenticating only once, so you don't need to type your credentials again unless your "
"session expires or you logout."
msgstr ""
"A Central de Autenticação de Serviços garante seu acesso à maioria dos nossos sitespor "
"meio de uma única autenticação, então você não precisa digitar suas "
"credenciaisnovamente, ao menos que sua sessão expire ou seu logout."
#: cas_server/forms.py:85
msgid "Identity provider"
msgstr "Provedor de identidade"
#: cas_server/forms.py:89 cas_server/forms.py:111
msgid "Warn me before logging me into other sites."
msgstr "Avise-me antes de me registrar em outros sites"
#: cas_server/forms.py:93
msgid "Remember the identity provider"
msgstr "Relembrar o provedor de identidade"
#: cas_server/forms.py:104 cas_server/models.py:638
msgid "username"
msgstr "usuário"
#: cas_server/forms.py:108
msgid "password"
msgstr "senha"
#: cas_server/forms.py:131
msgid "The credentials you provided cannot be determined to be authentic."
msgstr "As credenciais que você forneceu não podem ser determinadas como autênticas."
#: cas_server/forms.py:183
msgid "User not found in the temporary database, please try to reconnect"
msgstr "Usuário não encontrado na base de dados temporária, por favor, tente se reconectar"
#: cas_server/forms.py:197
msgid "service"
msgstr ""
#: cas_server/management/commands/cas_clean_federate.py:20
msgid "Clean old federated users"
msgstr ""
#: cas_server/management/commands/cas_clean_sessions.py:22
msgid "Clean deleted sessions"
msgstr ""
#: cas_server/management/commands/cas_clean_tickets.py:22
msgid "Clean old tickets"
msgstr ""
#: cas_server/models.py:71
msgid "identity provider"
msgstr "provedor de identidade"
#: cas_server/models.py:72
msgid "identity providers"
msgstr "provedores de identidade"
#: cas_server/models.py:78
msgid "suffix"
msgstr ""
#: cas_server/models.py:80
msgid "Suffix append to backend CAS returned username: ``returned_username`` @ ``suffix``."
msgstr ""
#: cas_server/models.py:87
msgid "server url"
msgstr ""
#: cas_server/models.py:97
msgid "CAS protocol version"
msgstr ""
#: cas_server/models.py:99
msgid "Version of the CAS protocol to use when sending requests the the backend CAS."
msgstr ""
#: cas_server/models.py:106
msgid "verbose name"
msgstr ""
#: cas_server/models.py:107
msgid "Name for this identity provider displayed on the login page."
msgstr "Nome para exibir o provedor de identidade na página de login."
#: cas_server/models.py:113 cas_server/models.py:490
msgid "position"
msgstr ""
#: cas_server/models.py:127
msgid "display"
msgstr ""
#: cas_server/models.py:128
msgid "Display the provider on the login page."
msgstr ""
#: cas_server/models.py:166
msgid "Federated user"
msgstr ""
#: cas_server/models.py:167
msgid "Federated users"
msgstr ""
#: cas_server/models.py:246
msgid "User attributes cache"
msgstr ""
#: cas_server/models.py:247
msgid "User attributes caches"
msgstr ""
#: cas_server/models.py:271
msgid "User"
msgstr ""
#: cas_server/models.py:272
msgid "Users"
msgstr ""
#: cas_server/models.py:364
#, python-format
msgid "Error during service logout %s"
msgstr ""
#: cas_server/models.py:484
msgid "Service pattern"
msgstr ""
#: cas_server/models.py:485
msgid "Services patterns"
msgstr ""
#: cas_server/models.py:491
msgid "service patterns are sorted using the position attribute"
msgstr ""
#: cas_server/models.py:499 cas_server/models.py:664
msgid "name"
msgstr ""
#: cas_server/models.py:500
msgid "A name for the service"
msgstr ""
#: cas_server/models.py:508 cas_server/models.py:707 cas_server/models.py:737
msgid "pattern"
msgstr ""
#: cas_server/models.py:510
msgid ""
"A regular expression matching services. Will usually looks like '^https://some\\.server"
"\\.com/path/.*$'.As it is a regular expression, special character must be escaped with a "
"'\\'."
msgstr ""
#: cas_server/models.py:521
msgid "user field"
msgstr ""
#: cas_server/models.py:522
msgid "Name of the attribute to transmit as username, empty = login"
msgstr ""
#: cas_server/models.py:527
msgid "restrict username"
msgstr ""
#: cas_server/models.py:528
msgid "Limit username allowed to connect to the list provided bellow"
msgstr ""
#: cas_server/models.py:533
msgid "proxy"
msgstr ""
#: cas_server/models.py:534
msgid "Proxy tickets can be delivered to the service"
msgstr ""
#: cas_server/models.py:540
msgid "proxy callback"
msgstr ""
#: cas_server/models.py:541
msgid "can be used as a proxy callback to deliver PGT"
msgstr ""
#: cas_server/models.py:548
msgid "single log out"
msgstr ""
#: cas_server/models.py:549
msgid "Enable SLO for the service"
msgstr ""
#: cas_server/models.py:558
msgid ""
"URL where the SLO request will be POST. empty = service url\n"
"This is usefull for non HTTP proxied services."
msgstr ""
#: cas_server/models.py:639
msgid "username allowed to connect to the service"
msgstr ""
#: cas_server/models.py:665
msgid "name of an attribute to send to the service, use * for all attributes"
msgstr ""
#: cas_server/models.py:672 cas_server/models.py:745
msgid "replace"
msgstr ""
#: cas_server/models.py:673
msgid ""
"name under which the attribute will be show to the service. empty = default name of the "
"attribut"
msgstr ""
#: cas_server/models.py:700 cas_server/models.py:731
msgid "attribute"
msgstr ""
#: cas_server/models.py:701
msgid "Name of the attribute which must verify pattern"
msgstr ""
#: cas_server/models.py:708
msgid "a regular expression"
msgstr ""
#: cas_server/models.py:732
msgid "Name of the attribute for which the value must be replace"
msgstr ""
#: cas_server/models.py:738
msgid "An regular expression maching whats need to be replaced"
msgstr ""
#: cas_server/models.py:746
msgid "replace expression, groups are capture by \\1, \\2 …"
msgstr ""
#: cas_server/templates/cas_server/base.html:43
#, python-format
msgid ""
"A new version of the application is available. This instance runs %(VERSION)s and the "
"last version is %(LAST_VERSION)s. Please consider upgrading."
msgstr ""
"Uma nova versão da aplicação está disponível. Está instância usa a versão %(VERSION)s e "
"a última versão é %(LAST_VERSION)s. Por favor, considere a atualização."
#: cas_server/templates/cas_server/logged.html:4
msgid ""
"<h3>Log In Successful</h3>You have successfully logged into the Central Authentication "
"Service.<br/>For security reasons, please Log Out and Exit your web browser when you are "
"done accessing services that require authentication!"
msgstr ""
"<h3>Log In realizado com sucesso</h3>Você foi conectado com sucesso a Central de "
"Autenticação de Serviços.<br/>Por razões de segurança, faça o Log Out e saia do seu "
"navegador quando você terminar de acessar os serviços que exigem auntenticação!"
#: cas_server/templates/cas_server/logged.html:8
msgid "Log me out from all my sessions"
msgstr "Desconecte-me de todas as sessões"
#: cas_server/templates/cas_server/logged.html:14
msgid "Forget the identity provider"
msgstr "Esquecer o provedor de identidade"
#: cas_server/templates/cas_server/logged.html:18
msgid "Logout"
msgstr ""
#: cas_server/templates/cas_server/login.html:6
msgid "Please log in"
msgstr "Por favor, faça log in"
#: cas_server/templates/cas_server/login.html:14
msgid "Login"
msgstr ""
#: cas_server/templates/cas_server/warn.html:9
msgid "Connect to the service"
msgstr ""
#: cas_server/utils.py:744
#, python-format
msgid "\"%(value)s\" is not a valid regular expression"
msgstr ""
#: cas_server/views.py:185
msgid ""
"<h3>Logout successful</h3>You have successfully logged out from the Central "
"Authentication Service. For security reasons, close your web browser."
msgstr ""
"<h3>Logout realizado com sucesso</h3>Você foi desconectado com sucesso da Central de "
"Autenticação de Serviços. Por razões de segurança, feche seu navegador."
#: cas_server/views.py:191
#, python-format
msgid ""
"<h3>Logout successful</h3>You have successfully logged out from %s sessions of the "
"Central Authentication Service. For security reasons, close your web browser."
msgstr ""
"<h3>Logout realizado com sucesso</h3>Você foi desconectado com sucesso da %s sessão da "
"Centralde Autenticação de Serviços. Por razões de segurança, feche seu navegador."
#: cas_server/views.py:198
msgid ""
"<h3>Logout successful</h3>You were already logged out from the Central Authentication "
"Service. For security reasons, close your web browser."
msgstr ""
"<h3>Logout realizado com sucesso</h3>Você já está desconectado da Central de "
"Autenticação de Serviços. Por razões de segurança, feche seu navegador."
#: cas_server/views.py:378
#, python-format
msgid ""
"Invalid response from your identity provider CAS upon ticket %(ticket)s validation: "
"%(error)r"
msgstr ""
"Resposta inválida do provedor de identidade CAS sobre o ticket %(ticket)svalidação: "
"%(error)r"
#: cas_server/views.py:500
msgid "Invalid login ticket, please try to log in again"
msgstr "Ticket de login inválido, por favor tente novamente"
#: cas_server/views.py:693
#, python-format
msgid "Authentication has been required by service %(name)s (%(url)s)"
msgstr "Autenticação requerida pelo serviço %(name)s (%(url)s)"
#: cas_server/views.py:731
#, python-format
msgid "Service %(url)s not allowed."
msgstr "Serviço %(url)s não permitido"
#: cas_server/views.py:738
msgid "Username not allowed"
msgstr "Usuário não permitido"
#: cas_server/views.py:745
msgid "User characteristics not allowed"
msgstr "Características de usuário não permitida"
#: cas_server/views.py:752
#, python-format
msgid "The attribute %(field)s is needed to use that service"
msgstr "O atributo %(field)s é necessário para usar o serviço"
#: cas_server/views.py:842
#, python-format
msgid "Authentication renewal required by service %(name)s (%(url)s)."
msgstr "Renovação da autenticação requerida pelo serviço %(name)s (%(url)s)."
#: cas_server/views.py:849
#, python-format
msgid "Authentication required by service %(name)s (%(url)s)."
msgstr "Autenticação requerida pelo serviço %(name)s (%(url)s)."
#: cas_server/views.py:856
#, python-format
msgid "Service %s not allowed"
msgstr "Serviço %s não permitido"

View file

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-03-28 14:10
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cas_server', '0011_auto_20161007_1258'),
]
operations = [
migrations.AlterField(
model_name='federatediendityprovider',
name='cas_protocol_version',
field=models.CharField(choices=[('1', 'CAS 1.0'), ('2', 'CAS 2.0'), ('3', 'CAS 3.0'), ('CAS_2_SAML_1_0', 'SAML 1.1')], default='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'),
),
migrations.AlterField(
model_name='servicepattern',
name='single_log_out_callback',
field=models.CharField(blank=True, default='', help_text='URL where the SLO request will be POST. empty = service url\nThis is usefull for non HTTP proxied services.', max_length=255, verbose_name='single log out callback'),
),
migrations.AlterField(
model_name='servicepattern',
name='user_field',
field=models.CharField(blank=True, default='', help_text='Name of the attribute to transmit as username, empty = login', max_length=255, verbose_name='user field'),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-03-29 15:48
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cas_server', '0012_auto_20170328_1610'),
]
operations = [
migrations.AlterField(
model_name='user',
name='username',
field=models.CharField(max_length=250),
),
]

View file

@ -273,7 +273,7 @@ class User(models.Model):
#: The session key of the current authenticated user #: The session key of the current authenticated user
session_key = models.CharField(max_length=40, blank=True, null=True) session_key = models.CharField(max_length=40, blank=True, null=True)
#: The username of the current authenticated user #: The username of the current authenticated user
username = models.CharField(max_length=30) username = models.CharField(max_length=250)
#: Last time the authenticated user has do something (auth, fetch ticket, etc…) #: Last time the authenticated user has do something (auth, fetch ticket, etc…)
date = models.DateTimeField(auto_now=True) date = models.DateTimeField(auto_now=True)
#: last time the user logged #: last time the user logged

View file

@ -58,7 +58,7 @@
class="alert alert-danger" class="alert alert-danger"
{% endif %} {% endif %}
{% endspaceless %}> {% endspaceless %}>
<p>{{message|safe}}</p> <p>{{message}}</p>
</div> </div>
{% endfor %} {% endfor %}
{% if auto_submit %}</noscript>{% endif %} {% if auto_submit %}</noscript>{% endif %}

View file

@ -2,6 +2,6 @@
{% load staticfiles %} {% load staticfiles %}
{% load i18n %} {% load i18n %}
{% block content %} {% block content %}
<div class="alert alert-success" role="alert">{{logout_msg|safe}}</div> <div class="alert alert-success" role="alert">{{logout_msg}}</div>
{% endblock %} {% endblock %}

View file

@ -295,6 +295,24 @@ class LoginTestCase(TestCase, BaseServicePattern, CanLogin):
) in response.content ) in response.content
) )
@override_settings(CAS_SHOW_SERVICE_MESSAGES=False)
def test_view_login_get_allowed_service_no_message(self):
"""Request a ticket for an allowed service by an unauthenticated client"""
# get a bare new http client
client = Client()
# we are not authenticated and are asking for a ticket for https://www.example.com
# which is a valid service matched by self.service_pattern
response = client.get("/login?service=https://www.example.com")
# the login page should be displayed
self.assertEqual(response.status_code, 200)
# we warn the user why it need to authenticated
self.assertFalse(
(
b"Authentication required by service "
b"example (https://www.example.com)"
) in response.content
)
def test_view_login_get_denied_service(self): def test_view_login_get_denied_service(self):
"""Request a ticket for an denied service by an unauthenticated client""" """Request a ticket for an denied service by an unauthenticated client"""
# get a bare new http client # get a bare new http client
@ -306,6 +324,18 @@ class LoginTestCase(TestCase, BaseServicePattern, CanLogin):
# we warn the user that https://www.example.net is not an allowed service url # we warn the user that https://www.example.net is not an allowed service url
self.assertTrue(b"Service https://www.example.net not allowed" in response.content) self.assertTrue(b"Service https://www.example.net not allowed" in response.content)
@override_settings(CAS_SHOW_SERVICE_MESSAGES=False)
def test_view_login_get_denied_service_no_message(self):
"""Request a ticket for an denied service by an unauthenticated client"""
# get a bare new http client
client = Client()
# we are not authenticated and are asking for a ticket for https://www.example.net
# which is NOT a valid service
response = client.get("/login?service=https://www.example.net")
self.assertEqual(response.status_code, 200)
# we warn the user that https://www.example.net is not an allowed service url
self.assertFalse(b"Service https://www.example.net not allowed" in response.content)
def test_view_login_get_auth_allowed_service(self): def test_view_login_get_auth_allowed_service(self):
"""Request a ticket for an allowed service by an authenticated client""" """Request a ticket for an allowed service by an authenticated client"""
# get a client that is already authenticated # get a client that is already authenticated
@ -505,6 +535,40 @@ class LoginTestCase(TestCase, BaseServicePattern, CanLogin):
# renewing authentication is done in the validate and serviceValidate views tests # renewing authentication is done in the validate and serviceValidate views tests
self.assertEqual(ticket.renew, True) self.assertEqual(ticket.renew, True)
@override_settings(CAS_SHOW_SERVICE_MESSAGES=False)
def test_renew_message_disabled(self):
"""test the authentication renewal request from a service"""
# use the default test service
service = "https://www.example.com"
# get a client that is already authenticated
client = get_auth_client()
# ask for a ticket for the service but aks for authentication renewal
response = client.get("/login", {'service': service, 'renew': 'on'})
# we are ask to reauthenticate and tell the user why
self.assertEqual(response.status_code, 200)
self.assertFalse(
(
b"Authentication renewal required by "
b"service example (https://www.example.com)"
) in response.content
)
# get the form default parameter
params = copy_form(response.context["form"])
# set valid username/password
params["username"] = settings.CAS_TEST_USER
params["password"] = settings.CAS_TEST_PASSWORD
# the renew parameter from the form should be True
self.assertEqual(params["renew"], True)
# post the authentication request
response = client.post("/login", params)
# the request succed, a ticket is created and we are redirected to the service url
self.assertEqual(response.status_code, 302)
ticket_value = response['Location'].split('ticket=')[-1]
ticket = models.ServiceTicket.objects.get(value=ticket_value)
# the created ticket is marked has being gottent after a renew. Futher testing about
# renewing authentication is done in the validate and serviceValidate views tests
self.assertEqual(ticket.renew, True)
@override_settings(CAS_ENABLE_AJAX_AUTH=True) @override_settings(CAS_ENABLE_AJAX_AUTH=True)
def test_ajax_login_required(self): def test_ajax_login_required(self):
""" """

View file

@ -23,6 +23,7 @@ from django.views.decorators.csrf import csrf_exempt
from django.middleware.csrf import CsrfViewMiddleware from django.middleware.csrf import CsrfViewMiddleware
from django.views.generic import View from django.views.generic import View
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from django.utils.safestring import mark_safe
import re import re
import logging import logging
@ -181,24 +182,24 @@ class LogoutView(View, LogoutMixin):
else: else:
# build logout message depending of the number of sessions the user logs out # build logout message depending of the number of sessions the user logs out
if session_nb == 1: if session_nb == 1:
logout_msg = _( logout_msg = mark_safe(_(
"<h3>Logout successful</h3>" "<h3>Logout successful</h3>"
"You have successfully logged out from the Central Authentication Service. " "You have successfully logged out from the Central Authentication Service. "
"For security reasons, close your web browser." "For security reasons, close your web browser."
) ))
elif session_nb > 1: elif session_nb > 1:
logout_msg = _( logout_msg = mark_safe(_(
"<h3>Logout successful</h3>" "<h3>Logout successful</h3>"
"You have successfully logged out from %s sessions of the Central " "You have successfully logged out from %d sessions of the Central "
"Authentication Service. " "Authentication Service. "
"For security reasons, close your web browser." "For security reasons, close your web browser."
) % session_nb ) % session_nb)
else: else:
logout_msg = _( logout_msg = mark_safe(_(
"<h3>Logout successful</h3>" "<h3>Logout successful</h3>"
"You were already logged out from the Central Authentication Service. " "You were already logged out from the Central Authentication Service. "
"For security reasons, close your web browser." "For security reasons, close your web browser."
) ))
# depending of settings, redirect to the login page with a logout message or display # depending of settings, redirect to the login page with a logout message or display
# the logout page. The default is to display tge logout page. # the logout page. The default is to display tge logout page.
@ -835,26 +836,29 @@ class LoginView(View, LogoutMixin):
# clean messages before leaving django # clean messages before leaving django
list(messages.get_messages(self.request)) list(messages.get_messages(self.request))
return HttpResponseRedirect(self.service) return HttpResponseRedirect(self.service)
if self.request.session.get("authenticated") and self.renew:
messages.add_message( if settings.CAS_SHOW_SERVICE_MESSAGES:
self.request, if self.request.session.get("authenticated") and self.renew:
messages.WARNING, messages.add_message(
_(u"Authentication renewal required by service %(name)s (%(url)s).") % self.request,
{'name': service_pattern.name, 'url': self.service} messages.WARNING,
) _(u"Authentication renewal required by service %(name)s (%(url)s).") %
else: {'name': service_pattern.name, 'url': self.service}
messages.add_message( )
self.request, else:
messages.WARNING, messages.add_message(
_(u"Authentication required by service %(name)s (%(url)s).") % self.request,
{'name': service_pattern.name, 'url': self.service} messages.WARNING,
) _(u"Authentication required by service %(name)s (%(url)s).") %
{'name': service_pattern.name, 'url': self.service}
)
except ServicePattern.DoesNotExist: except ServicePattern.DoesNotExist:
messages.add_message( if settings.CAS_SHOW_SERVICE_MESSAGES:
self.request, messages.add_message(
messages.ERROR, self.request,
_(u'Service %s not allowed') % self.service messages.ERROR,
) _(u'Service %s not allowed') % self.service
)
if self.ajax: if self.ajax:
data = { data = {
"status": "error", "status": "error",