Commit 902dd103 authored by Valentin Samir's avatar Valentin Samir Committed by GitHub

Merge pull request #12 from nitmir/dev

Update version to 0.7.0
parents 282e3a83 5b2795ae
This diff is collapsed.
include tox.ini
include LICENSE
include README.rst
include CHANGELOG.rst
include .coveragerc
include Makefile
include pytest.ini
......@@ -15,6 +16,7 @@ include docs/conf.py
include docs/index.rst
include docs/Makefile
include docs/README.rst
include docs/CHANGELOG.rst
recursive-include docs/_ext *
recursive-include docs/package *
recursive-include docs/_static *
......
......@@ -7,7 +7,7 @@ CAS Server is a Django application implementing the `CAS Protocol 3.0 Specificat
<https://apereo.github.io/cas/4.2.x/protocol/CAS-Protocol-Specification.html>`_.
By default, the authentication process use django internal users but you can easily
use any sources (see auth classes in the auth.py file)
use any sources (see the `Authentication backend`_ section and auth classes in the auth.py file)
.. contents:: Table of Contents
......@@ -38,7 +38,7 @@ Dependencies
Minimal version of packages dependancy are just indicative and meens that ``django-cas-server`` has
been tested with it. Previous versions of dependencies may or may not work.
Additionally, denpending of the authentication backend you plan to use, you may need the following
Additionally, denpending of the `Authentication backend`_ you plan to use, you may need the following
python packages:
* ldap3
......@@ -174,13 +174,11 @@ Quick start
inactive since more than ``SESSION_COOKIE_AGE``. The default value for is ``1209600``
seconds (2 weeks). You probably should reduce it to something like ``86400`` seconds (1 day).
You could for example do as bellow :
You could for example do as bellow::
.. code-block::
0 0 * * * cas-user /path/to/project/manage.py clearsessions
*/5 * * * * cas-user /path/to/project/manage.py cas_clean_tickets
5 0 * * * cas-user /path/to/project/manage.py cas_clean_sessions
0 0 * * * cas-user /path/to/project/manage.py clearsessions
*/5 * * * * cas-user /path/to/project/manage.py cas_clean_tickets
5 0 * * * cas-user /path/to/project/manage.py cas_clean_sessions
5. Run ``python manage.py createsuperuser`` to create an administrator user.
......@@ -208,7 +206,7 @@ Template settings
Default is a key icon. Set it to ``False`` to disable it.
* ``CAS_SHOW_POWERED``: Set it to ``False`` to hide the powered by footer. The default is ``True``.
* ``CAS_COMPONENT_URLS``: URLs to css and javascript external components. It is a dictionnary
and it must have the five following keys: ``"bootstrap3_css"``, ``"bootstrap3_js"``,
having the five following keys: ``"bootstrap3_css"``, ``"bootstrap3_js"``,
``"html5shiv"``, ``"respond"``, ``"jquery"``. The default is::
{
......@@ -219,6 +217,32 @@ Template settings
"jquery": "//code.jquery.com/jquery.min.js",
}
if you omit some keys of the dictionnary, the default value for these keys is used.
* ``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:
* ``message``: A unicode message to display, potentially wrapped around ugettex_lazy
* ``discardable``: A boolean, specify if the users can close the message info-box
* ``type``: One of info, success, info, warning, danger. The type of the info-box.
``CAS_INFO_MESSAGES`` contains by default one message, ``cas_explained``, which explain
roughly the purpose of a CAS. The default is::
{
"cas_explained": {
"message":_(
u"The Central Authentication Service grants you access to most of our websites by "
u"authenticating only once, so you don't need to type your credentials again unless "
u"your session expires or you logout."
),
"discardable": True,
"type": "info", # one of info, success, info, warning, danger
},
}
* ``CAS_INFO_MESSAGES_ORDER``: A list of message names. Order in which info-box messages are
displayed. Use an empty list to disable messages display. The default is ``[]``.
* ``CAS_LOGIN_TEMPLATE``: Path to the template showed on ``/login`` then the user
is not autenticated. The default is ``"cas_server/login.html"``.
* ``CAS_WARN_TEMPLATE``: Path to the template showed on ``/login?service=...`` then
......@@ -228,7 +252,7 @@ Template settings
authenticated. The default is ``"cas_server/logged.html"``.
* ``CAS_LOGOUT_TEMPLATE``: Path to the template showed on ``/logout`` then to user
is being disconnected. The default is ``"cas_server/logout.html"``
* ``CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT``: Should we redirect users to `/login` after they
* ``CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT``: Should we redirect users to ``/login`` after they
logged out instead of displaying ``CAS_LOGOUT_TEMPLATE``. The default is ``False``.
......@@ -271,7 +295,7 @@ New version warnings settings
* ``CAS_NEW_VERSION_HTML_WARNING``: A boolean for diplaying a warning on html pages then a new
version of the application is avaible. Once closed by a user, it is not displayed to this user
until the next new version. The default is ``True``.
* ``CAS_NEW_VERSION_EMAIL_WARNING``: A bolean sot sending a email to ``settings.ADMINS`` when a new
* ``CAS_NEW_VERSION_EMAIL_WARNING``: A boolean for sending a email to ``settings.ADMINS`` when a new
version is available. The default is ``True``.
......@@ -545,10 +569,10 @@ A service pattern has 4 associated models:
an email address to connect to it. To do so, put ``email`` in ``Attribute`` and ``.*`` in ``pattern``.
Then a user ask a ticket for a service, the service URL is compare against each service patterns
sorted by `position`. The first service pattern that matches the service URL is chosen.
Hence, you should give low `position` to very specific patterns like
``^https://www\.example\.com(/.*)?$`` and higher `position` to generic patterns like ``^https://.*``.
So the service URL `https://www.examle.com` will use the service pattern for
sorted by ``position``. The first service pattern that matches the service URL is chosen.
Hence, you should give low ``position`` to very specific patterns like
``^https://www\.example\.com(/.*)?$`` and higher ``position`` to generic patterns like ``^https://.*``.
So the service URL ``https://www.examle.com`` will use the service pattern for
``^https://www\.example\.com(/.*)?$`` and not the one for ``^https://.*``.
......@@ -572,7 +596,7 @@ An identity provider comes with 5 fields:
* ``Suffix``: the suffix that will be append to the username returned by the identity provider.
It must be unique.
* ``Server url``: the URL to the identity provider CAS. For instance, if you are using
``https://cas.example.org/login`` to authenticate on the CAS, the `server url` is
``https://cas.example.org/login`` to authenticate on the CAS, the ``server url`` is
``https://cas.example.org``
* ``CAS protocol version``: the version of the CAS protocol to use to contact the identity provider.
The default is version 3.
......@@ -593,11 +617,9 @@ Then using federate mode, you should add one command to a daily crontab: ``cas_c
This command clean the local cache of federated user from old unused users.
You could for example do as bellow :
.. code-block::
You could for example do as bellow::
10 0 * * * cas-user /path/to/project/manage.py cas_clean_federate
10 0 * * * cas-user /path/to/project/manage.py cas_clean_federate
......
......@@ -11,7 +11,7 @@
"""A django CAS server application"""
#: version of the application
VERSION = '0.6.4'
VERSION = '0.7.0'
#: path the the application configuration class
default_app_config = 'cas_server.apps.CasAppConfig'
......@@ -12,6 +12,7 @@
"""Default values for the app's settings"""
from django.conf import settings
from django.contrib.staticfiles.templatetags.staticfiles import static
from django.utils.translation import ugettext_lazy as _
from importlib import import_module
......@@ -180,13 +181,45 @@ 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"
#: 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`.
#: A message :class:`dict` has 3 keys:
#: * ``message``: A :class:`unicode`, the message to display, potentially wrapped around
#: ugettex_lazy
#: * ``discardable``: A :class:`bool`, specify if the users can close the message info-box
#: * ``type``: One of info, success, info, warning, danger. The type of the info-box.
#: ``CAS_INFO_MESSAGES`` contains by default one message, ``cas_explained``, which explain
#: roughly the purpose of a CAS.
CAS_INFO_MESSAGES = {
"cas_explained": {
"message": _(
u"The Central Authentication Service grants you access to most of our websites by "
u"authenticating only once, so you don't need to type your credentials again unless "
u"your session expires or you logout."
),
"discardable": True,
"type": "info", # one of info, success, info, warning, danger
},
}
#: :class:`list` of message names. Order in which info-box messages are displayed.
#: Let the list empty to disable messages display.
CAS_INFO_MESSAGES_ORDER = []
GLOBALS = globals().copy()
for name, default_value in GLOBALS.items():
# get the current setting value, falling back to default_value
value = getattr(settings, name, default_value)
# set the setting value to its value if defined, ellse to the default_value.
setattr(settings, name, value)
# only care about parameter begining by CAS_
if name.startswith("CAS_"):
# get the current setting value, falling back to default_value
value = getattr(settings, name, default_value)
# set the setting value to its value if defined, ellse to the default_value.
setattr(settings, name, value)
# Allow the user defined CAS_COMPONENT_URLS to omit not changed values
MERGED_CAS_COMPONENT_URLS = CAS_COMPONENT_URLS.copy()
MERGED_CAS_COMPONENT_URLS.update(settings.CAS_COMPONENT_URLS)
settings.CAS_COMPONENT_URLS = MERGED_CAS_COMPONENT_URLS
# if the federated mode is enabled, we must use the :class`cas_server.auth.CASFederateAuth` auth
# backend.
......
This diff is collapsed.
......@@ -19,7 +19,7 @@ from ... import models
class Command(BaseCommand):
"""Clean old trickets"""
args = ''
help = _(u"Clean old trickets")
help = _(u"Clean old tickets")
def handle(self, *args, **options):
models.User.clean_old_entries()
......
......@@ -466,7 +466,8 @@ class ServicePattern(models.Model):
"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 '\\'."
)
),
validators=[utils.regexpr_validator]
)
#: Name of the attribute to transmit as username, if empty the user login is used
user_field = models.CharField(
......@@ -625,7 +626,7 @@ class ReplaceAttributName(models.Model):
max_length=255,
blank=True,
verbose_name=_(u"replace"),
help_text=_(u"name under which the attribute will be show"
help_text=_(u"name under which the attribute will be show "
u"to the service. empty = default name of the attribut")
)
#: ForeignKey to a :class:`ServicePattern`. :class:`ReplaceAttributName` instances for a
......@@ -660,7 +661,8 @@ class FilterAttributValue(models.Model):
pattern = models.CharField(
max_length=255,
verbose_name=_(u"pattern"),
help_text=_(u"a regular expression")
help_text=_(u"a regular expression"),
validators=[utils.regexpr_validator]
)
#: ForeignKey to a :class:`ServicePattern`. :class:`FilterAttributValue` instances for a
#: :class:`ServicePattern` are accessible thought its :attr:`ServicePattern.filters`
......@@ -689,7 +691,8 @@ class ReplaceAttributValue(models.Model):
pattern = models.CharField(
max_length=255,
verbose_name=_(u"pattern"),
help_text=_(u"An regular expression maching whats need to be replaced")
help_text=_(u"An regular expression maching whats need to be replaced"),
validators=[utils.regexpr_validator]
)
#: The replacement to what is mached by :attr:`pattern`. groups are capture by \\1, \\2 …
replace = models.CharField(
......
......@@ -31,14 +31,14 @@ function eraseCookie(name) {
createCookie(name,"",-1);
}
function alert_version(last_version){
function discard_and_remember(id, cookie_name, token, days=10*365){
jQuery(function( $ ){
$("#alert-version").click(function( e ){
$(id).click(function( e ){
e.preventDefault();
createCookie("cas-alert-version", last_version, 10*365);
createCookie(cookie_name, token, days);
});
if(readCookie("cas-alert-version") === last_version){
$("#alert-version").parent().hide();
if(readCookie(cookie_name) === token){
$(id).parent().hide();
}
});
}
......@@ -31,10 +31,16 @@
<div class="col-lg-3 col-md-3 col-sm-2 col-xs-12"></div>
<div class="col-lg-6 col-md-6 col-sm-8 col-xs-12">
{% if auto_submit %}<noscript>{% endif %}
{% for msg in CAS_INFO_RENDER %}
<div class="alert alert-{{msg.type}}{% if msg.discardable %} alert-dismissable{% endif %}">
{% if msg.discardable %}<button type="button" class="close" data-dismiss="alert" aria-hidden="true" id="info-{{msg.name}}">&#215;</button>{% endif %}
<p>{{msg.message}}</p>
</div>
{% endfor %}
{% if settings.CAS_NEW_VERSION_HTML_WARNING and upgrade_available %}
<div class="alert alert-info alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true" id="alert-version">&#215;</button>
{% blocktrans %}A new version of the application is available. This instance runs {{VERSION}} and the last version is {{LAST_VERSION}}. Please consider upgrading.{% endblocktrans %}
<p>{% blocktrans %}A new version of the application is available. This instance runs {{VERSION}} and the last version is {{LAST_VERSION}}. Please consider upgrading.{% endblocktrans %}</p>
</div>
{% endif %}
{% block ante_messages %}{% endblock %}
......@@ -52,7 +58,7 @@
class="alert alert-danger"
{% endif %}
{% endspaceless %}>
{{message|safe}}
<p>{{message|safe}}</p>
</div>
{% endfor %}
{% if auto_submit %}</noscript>{% endif %}
......@@ -71,9 +77,17 @@
<script src="{{settings.CAS_COMPONENT_URLS.jquery}}"></script>
<script src="{{settings.CAS_COMPONENT_URLS.bootstrap3_js}}"></script>
<script src="{% static "cas_server/functions.js" %}"></script>
{% if settings.CAS_NEW_VERSION_HTML_WARNING and upgrade_available %}
<script type="text/javascript">alert_version("{{LAST_VERSION}}")</script>
{% endif %}
<script type="text/javascript">
{% if settings.CAS_NEW_VERSION_HTML_WARNING and upgrade_available %}
discard_and_remember("#alert-version", "cas-alert-version", "{{LAST_VERSION}}");
{% endif %}
{% for msg in CAS_INFO_RENDER %}
{% if msg.discardable %}
discard_and_remember("#info-{{msg.name}}", "cas-info-{{msg.name}}", "{{msg.hash}}");
{% endif %}
{% endfor %}
{% block javascript_inline %}{% endblock %}
</script>
{% block javascript %}{% endblock %}
</body>
</html>
......
......@@ -15,7 +15,7 @@
{% if auto_submit %}</noscript>{% endif %}
</form>
{% endblock %}
{% block javascript %}<script type="text/javascript">
{% block javascript_inline %}
jQuery(function( $ ){
$("#id_warn").click(function(e){
if($("#id_warn").is(':checked')){
......@@ -26,5 +26,5 @@ jQuery(function( $ ){
});
});{% if auto_submit %}
document.getElementById('login_form').submit(); // SUBMIT FORM{% endif %}
</script>{% endblock %}
{% endblock %}
......@@ -255,3 +255,9 @@ class UtilsTestCase(TestCase):
self.assertIsInstance(result, dict)
self.assertIn('applied', result)
self.assertIsInstance(result['applied'], datetime.datetime)
def test_regexpr_validator(self):
"""test the function regexpr_validator"""
utils.regexpr_validator("^a$")
with self.assertRaises(utils.ValidationError):
utils.regexpr_validator("[")
......@@ -75,6 +75,45 @@ class LoginTestCase(TestCase, BaseServicePattern, CanLogin):
response = client.get("/login")
self.assertNotIn(b"A new version of the application is available", response.content)
@override_settings(CAS_INFO_MESSAGES_ORDER=["cas_explained"])
def test_messages_info_box_enabled(self):
"""test that the message info-box is displayed then enabled"""
client = Client()
response = client.get("/login")
self.assertIn(
b"The Central Authentication Service grants you access to most of our websites by ",
response.content
)
@override_settings(CAS_INFO_MESSAGES_ORDER=[])
def test_messages_info_box_disabled(self):
"""test that the message info-box is not displayed then disabled"""
client = Client()
response = client.get("/login")
self.assertNotIn(
b"The Central Authentication Service grants you access to most of our websites by ",
response.content
)
# test1 and test2 are malformed and should be ignored, test3 is ok, test5 do not
# exists and should be ignored
@override_settings(CAS_INFO_MESSAGES_ORDER=["test1", "test2", "test3", "test5"])
@override_settings(CAS_INFO_MESSAGES={
"test1": "test", # not a dict, should be ignored
"test2": {"type": "success"}, # not "message" key, should be ignored
"test3": {"message": "test3"},
"test4": {"message": "test4"},
})
def test_messages_info_box_bad_messages(self):
"""test that mal formated messages dict are ignored"""
client = Client()
# not errors should be raises
response = client.get("/login")
# test3 is ok est should be there
self.assertIn(b"test3", response.content)
# test4 is not in CAS_INFO_MESSAGES_ORDER and should not be there
self.assertNotIn(b"test4", response.content)
def test_login_view_post_goodpass_goodlt(self):
"""Test a successul login"""
# we get a client who fetch a frist time the login page and the login form default
......@@ -234,7 +273,7 @@ class LoginTestCase(TestCase, BaseServicePattern, CanLogin):
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.assertTrue(b"Service https://www.example.net non allowed" in response.content)
self.assertTrue(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"""
......@@ -280,7 +319,7 @@ class LoginTestCase(TestCase, BaseServicePattern, CanLogin):
self.assertEqual(response.status_code, 200)
# we warn the user that https://www.example.net is not an allowed service url
# NO ticket are created
self.assertTrue(b"Service https://www.example.org non allowed" in response.content)
self.assertTrue(b"Service https://www.example.org not allowed" in response.content)
def test_user_logged_not_in_db(self):
"""If the user is logged but has been delete from the database, it should be logged out"""
......@@ -314,7 +353,7 @@ class LoginTestCase(TestCase, BaseServicePattern, CanLogin):
response = client.get("/login", {'service': self.service_restrict_user_fail})
self.assertEqual(response.status_code, 200)
# the ticket is not created and a warning is displayed to the user
self.assertTrue(b"Username non allowed" in response.content)
self.assertTrue(b"Username not allowed" in response.content)
# same but with the tes user username being one of the allowed usernames
response = client.get("/login", {'service': self.service_restrict_user_success})
......@@ -337,7 +376,7 @@ class LoginTestCase(TestCase, BaseServicePattern, CanLogin):
response = client.get("/login", {'service': service})
# the ticket is not created and a warning is displayed to the user
self.assertEqual(response.status_code, 200)
self.assertTrue(b"User characteristics non allowed" in response.content)
self.assertTrue(b"User characteristics not allowed" in response.content)
# same but with rectriction that a valid upon the test user attributes
response = client.get("/login", {'service': self.service_filter_success})
......@@ -546,7 +585,7 @@ class LoginTestCase(TestCase, BaseServicePattern, CanLogin):
self.assertEqual(data["messages"][0]["level"], "error")
self.assertEqual(
data["messages"][0]["message"],
"Service https://www.example.org non allowed."
"Service https://www.example.org not allowed."
)
@override_settings(CAS_ENABLE_AJAX_AUTH=True)
......
......@@ -18,7 +18,10 @@ from django.contrib import messages
from django.contrib.messages import constants as DEFAULT_MESSAGE_LEVELS
from django.core.serializers.json import DjangoJSONEncoder
from django.utils import timezone
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
import re
import random
import string
import json
......@@ -61,6 +64,7 @@ def context(params):
"""
params["settings"] = settings
params["message_levels"] = DEFAULT_MESSAGE_LEVELS
if settings.CAS_NEW_VERSION_HTML_WARNING:
LAST_VERSION = last_version()
params["VERSION"] = VERSION
......@@ -69,6 +73,27 @@ def context(params):
params["upgrade_available"] = decode_version(VERSION) < decode_version(LAST_VERSION)
else:
params["upgrade_available"] = False
if settings.CAS_INFO_MESSAGES_ORDER:
params["CAS_INFO_RENDER"] = []
for msg_name in settings.CAS_INFO_MESSAGES_ORDER:
if msg_name in settings.CAS_INFO_MESSAGES:
if not isinstance(settings.CAS_INFO_MESSAGES[msg_name], dict):
continue
msg = settings.CAS_INFO_MESSAGES[msg_name].copy()
if "message" in msg:
msg["name"] = msg_name
# use info as default infox type
msg["type"] = msg.get("type", "info")
# make box discardable by default
msg["discardable"] = msg.get("discardable", True)
msg_hash = (
six.text_type(msg["message"]).encode("utf-8") +
msg["type"].encode("utf-8")
)
# hash depend of the rendering language
msg["hash"] = hashlib.md5(msg_hash).hexdigest()
params["CAS_INFO_RENDER"].append(msg)
return params
......@@ -700,3 +725,19 @@ def logout_request(ticket):
'datetime': timezone.now().isoformat(),
'ticket': ticket
}
def regexpr_validator(value):
"""
Test that ``value`` is a valid regular expression
:param unicode value: A regular expression to test
:raises ValidationError: if ``value`` is not a valid regular expression
"""
try:
re.compile(value)
except re.error:
raise ValidationError(
_('"%(value)s" is not a valid regular expression'),
params={'value': value}
)
......@@ -727,21 +727,21 @@ class LoginView(View, LogoutMixin):
messages.add_message(
self.request,
messages.ERROR,
_(u'Service %(url)s non allowed.') % {'url': self.service}
_(u'Service %(url)s not allowed.') % {'url': self.service}
)
except models.BadUsername:
error = 2
messages.add_message(
self.request,
messages.ERROR,
_(u"Username non allowed")
_(u"Username not allowed")
)
except models.BadFilter:
error = 3
messages.add_message(
self.request,
messages.ERROR,
_(u"User characteristics non allowed")
_(u"User characteristics not allowed")
)
except models.UserFieldNotDefined:
error = 4
......@@ -852,7 +852,7 @@ class LoginView(View, LogoutMixin):
messages.add_message(
self.request,
messages.ERROR,
_(u'Service %s non allowed') % self.service
_(u'Service %s not allowed') % self.service
)
if self.ajax:
data = {
......
.. include:: ../CHANGELOG.rst
......@@ -14,6 +14,11 @@ Contents:
README
package/cas_server
.. toctree::
:maxdepth: 2
CHANGELOG
Indices and tables
==================
......
......@@ -101,6 +101,7 @@ deps=
skip_install=True
commands=
rst2html.py --strict {toxinidir}/README.rst /dev/null
rst2html.py --halt=warning {toxinidir}/CHANGELOG.rst /dev/null
{[post_cmd]commands}
whitelist_externals={[post_cmd]whitelist_externals}
......
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