Unverified Commit 4229f871 authored by Valentin Samir's avatar Valentin Samir Committed by GitHub

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
parents 85eae7d6 5811d643
Pipeline #619 failed with stage
......@@ -6,6 +6,29 @@ All notable changes to this project will be documented in this file.
.. contents:: Table of Contents
: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
===================
......
......@@ -218,7 +218,8 @@ Template settings
}
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.
It is a dictionnary mapping message name to a message dict. A message dict has 3 keys:
......
......@@ -11,7 +11,7 @@
"""A django CAS server application"""
#: version of the application
VERSION = '0.8.0'
VERSION = '0.9.0'
#: path the the application configuration class
default_app_config = 'cas_server.apps.CasAppConfig'
......@@ -27,6 +27,7 @@ except ImportError:
try: # pragma: no cover
import ldap3
import ldap3.core.exceptions
except ImportError:
ldap3 = None
......@@ -297,6 +298,16 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover
settings.CAS_LDAP_USER_QUERY % ldap3.utils.conv.escape_bytes(username),
attributes=ldap3.ALL_ATTRIBUTES
) and len(conn.entries) == 1:
# try the new ldap3>=2 API
try:
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()
......@@ -308,7 +319,7 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover
else:
super(LdapAuthUser, self).__init__(username)
break
except ldap3.LDAPCommunicationError:
except ldap3.core.exceptions.LDAPCommunicationError:
if retry_nb == 2:
raise
......@@ -321,7 +332,7 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover
correct, ``False`` otherwise.
:rtype: bool
"""
if settings.CAS_LDAP_PASSWORD_CHECK == "bind":
if self.user and settings.CAS_LDAP_PASSWORD_CHECK == "bind":
try:
conn = ldap3.Connection(
settings.CAS_LDAP_SERVER,
......@@ -336,6 +347,16 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover
settings.CAS_LDAP_USER_QUERY % ldap3.utils.conv.escape_bytes(self.username),
attributes=ldap3.ALL_ATTRIBUTES
) and len(conn.entries) == 1:
# try the ldap3>=2 API
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
......@@ -346,7 +367,10 @@ class LdapAuthUser(DBAuthUser): # pragma: no cover
finally:
conn.unbind()
return True
except (ldap3.LDAPBindError, ldap3.LDAPCommunicationError):
except (
ldap3.core.exceptions.LDAPBindError,
ldap3.core.exceptions.LDAPCommunicationError
):
return False
elif self.user and self.user.get(settings.CAS_LDAP_PASSWORD_ATTR):
return check_password(
......
......@@ -185,6 +185,8 @@ CAS_NEW_VERSION_EMAIL_WARNING = True
#: You should not change it.
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.
#: ``CAS_INFO_MESSAGES`` is a :class:`dict` mapping message name to a message :class:`dict`.
......
This diff is collapsed.
# -*- 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'),
),
]
# -*- 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),
),
]
......@@ -273,7 +273,7 @@ class User(models.Model):
#: The session key of the current authenticated user
session_key = models.CharField(max_length=40, blank=True, null=True)
#: 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…)
date = models.DateTimeField(auto_now=True)
#: last time the user logged
......
......@@ -58,7 +58,7 @@
class="alert alert-danger"
{% endif %}
{% endspaceless %}>
<p>{{message|safe}}</p>
<p>{{message}}</p>
</div>
{% endfor %}
{% if auto_submit %}</noscript>{% endif %}
......
......@@ -2,6 +2,6 @@
{% load staticfiles %}
{% load i18n %}
{% block content %}
<div class="alert alert-success" role="alert">{{logout_msg|safe}}</div>
<div class="alert alert-success" role="alert">{{logout_msg}}</div>
{% endblock %}
......@@ -295,6 +295,24 @@ class LoginTestCase(TestCase, BaseServicePattern, CanLogin):
) 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):
"""Request a ticket for an denied service by an unauthenticated 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
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):
"""Request a ticket for an allowed service by an authenticated client"""
# 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
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)
def test_ajax_login_required(self):
"""
......
......@@ -23,6 +23,7 @@ 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
from django.utils.safestring import mark_safe
import re
import logging
......@@ -181,24 +182,24 @@ class LogoutView(View, LogoutMixin):
else:
# build logout message depending of the number of sessions the user logs out
if session_nb == 1:
logout_msg = _(
logout_msg = mark_safe(_(
"<h3>Logout successful</h3>"
"You have successfully logged out from the Central Authentication Service. "
"For security reasons, close your web browser."
)
))
elif session_nb > 1:
logout_msg = _(
logout_msg = mark_safe(_(
"<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. "
"For security reasons, close your web browser."
) % session_nb
) % session_nb)
else:
logout_msg = _(
logout_msg = mark_safe(_(
"<h3>Logout successful</h3>"
"You were already logged out from the Central Authentication Service. "
"For security reasons, close your web browser."
)
))
# 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.
......@@ -835,6 +836,8 @@ class LoginView(View, LogoutMixin):
# clean messages before leaving django
list(messages.get_messages(self.request))
return HttpResponseRedirect(self.service)
if settings.CAS_SHOW_SERVICE_MESSAGES:
if self.request.session.get("authenticated") and self.renew:
messages.add_message(
self.request,
......@@ -850,6 +853,7 @@ class LoginView(View, LogoutMixin):
{'name': service_pattern.name, 'url': self.service}
)
except ServicePattern.DoesNotExist:
if settings.CAS_SHOW_SERVICE_MESSAGES:
messages.add_message(
self.request,
messages.ERROR,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment