Skip to content

Commit 768f794

Browse files
authored
Merge pull request #1 from knaperek/master
merge in latest upstream.
2 parents 2bb7ab4 + 49b5b4f commit 768f794

File tree

16 files changed

+411
-250
lines changed

16 files changed

+411
-250
lines changed

.travis.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
language: python
2+
3+
python:
4+
- "2.7"
5+
6+
sudo: false
7+
8+
env:
9+
- TOX_ENV=py27-django18
10+
- TOX_ENV=py27-django19
11+
- TOX_ENV=py27-django110
12+
- TOX_ENV=py27-djangomaster
13+
14+
matrix:
15+
fast_finish: true
16+
allow_failures:
17+
- env: TOX_ENV=py27-djangomaster
18+
19+
addons:
20+
apt:
21+
packages:
22+
- xmlsec1
23+
24+
install:
25+
- pip install tox virtualenv
26+
27+
script:
28+
- tox -e $TOX_ENV
29+
30+
after_success:
31+
- pip install codecov
32+
- codecov -e TOX_ENV
33+
34+
notifications:
35+
email: false

CHANGES

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
Changes
22
=======
3+
0.14.5 (2016-09-19)
4+
-------------------
5+
- Django 1.10 support. Thanks to inducer.
6+
- Various fixes and minor improvements. Thanks to ajsmilutin, ganiserb, inducer, grunichev, liquidpele and darbula
7+
38
0.14.4 (2016-03-29)
49
-------------------
510
- Fix compatibility issue with pysaml2-4.0.3+. Thanks to jimr and astoltz.

README.rst

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
1-
.. contents::
2-
31
===========
42
djangosaml2
53
===========
64

5+
.. image:: https://travis-ci.org/knaperek/djangosaml2.svg?branch=master
6+
:target: https://travis-ci.org/knaperek/djangosaml2
7+
:align: left
8+
9+
710
djangosaml2 is a Django application that integrates the PySAML2 library
811
into your project. This mean that you can protect your Django based project
912
with a service provider based on PySAML. This way it will talk SAML2 with
1013
your Identity Provider allowing you to use this authentication mechanism.
1114
This document will guide you through a few simple steps to accomplish
1215
such goal.
1316

17+
.. contents::
1418

1519
Installation
1620
============
@@ -209,9 +213,15 @@ We will see a typical configuration for protecting a Django project::
209213
# set to 1 to output debugging information
210214
'debug': 1,
211215

212-
# certificate
216+
# Signing
213217
'key_file': path.join(BASEDIR, 'mycert.key'), # private part
214218
'cert_file': path.join(BASEDIR, 'mycert.pem'), # public part
219+
220+
# Encryption
221+
'encryption_keypairs': [{
222+
'key_file': path.join(BASEDIR, 'my_encryption_key.key'), # private part
223+
'cert_file': path.join(BASEDIR, 'my_encryption_cert.pem'), # public part
224+
}],
215225

216226
# own metadata settings
217227
'contact_person': [
@@ -307,6 +317,14 @@ Please, use an unique attribute when setting this option. Otherwise
307317
the authentication process will fail because djangosaml2 does not know
308318
which Django user it should pick.
309319

320+
If your main attribute is something inherently case-inensitive (such as
321+
an email address), you may set::
322+
323+
SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP = '__iexact'
324+
325+
(This is simply appended to the main attribute name to form a Django
326+
query. Your main attribute must be unique even given this lookup.)
327+
310328
Another option is to use the SAML2 name id as the username by setting::
311329

312330
SAML_USE_NAME_ID_AS_USERNAME = True
@@ -347,7 +365,42 @@ If you are using Django user profile objects to store extra attributes
347365
about your user you can add those attributes to the SAML_ATTRIBUTE_MAPPING
348366
dictionary. For each (key, value) pair, djangosaml2 will try to store the
349367
attribute in the User model if there is a matching field in that model.
350-
Otherwise it will try to do the same with your profile custom model.
368+
Otherwise it will try to do the same with your profile custom model. For
369+
multi-valued attributes only the first value is assigned to the destination field.
370+
371+
Alternatively, custom processing of attributes can be achieved by setting the
372+
value(s) in the SAML_ATTRIBUTE_MAPPING, to name(s) of method(s) defined on a
373+
custom django User object. In this case, each method is called by djangosaml2,
374+
passing the full list of attribute values extracted from the <saml:AttributeValue>
375+
elements of the <saml:Attribute>. Among other uses, this is a useful way to process
376+
multi-valued attributes such as lists of user group names.
377+
378+
For example::
379+
380+
Saml assertion snippet::
381+
382+
<saml:Attribute Name="groups" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
383+
<saml:AttributeValue>group1</saml:AttributeValue>
384+
<saml:AttributeValue>group2</saml:AttributeValue>
385+
<saml:AttributeValue>group3</saml:AttributeValue>
386+
</saml:Attribute>
387+
388+
Custom User object::
389+
390+
from django.contrib.auth.models import AbstractUser
391+
392+
class User(AbstractUser):
393+
394+
def process_groups(self, groups):
395+
// process list of group names in argument 'groups'
396+
pass;
397+
398+
settings.py::
399+
400+
SAML_ATTRIBUTE_MAPPING = {
401+
'groups': ('process_groups', ),
402+
}
403+
351404

352405
Learn more about Django profile models at:
353406

@@ -362,7 +415,7 @@ following code to your app::
362415

363416
from djangosaml2.signals import pre_user_save
364417

365-
def custom_update_user(sender=user, attributes=attributes, user_modified=user_modified)
418+
def custom_update_user(sender=User, instance, attributes, user_modified, **kargs)
366419
...
367420
return True # I modified the user object
368421

djangosaml2/backends.py

Lines changed: 78 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323

2424
from djangosaml2.signals import pre_user_save
2525

26+
from . import settings as saml_settings
27+
2628
try:
2729
from django.contrib.auth.models import SiteProfileNotAvailable
2830
except ImportError:
@@ -83,8 +85,8 @@ def authenticate(self, session_info=None, attribute_mapping=None,
8385
use_name_id_as_username = getattr(
8486
settings, 'SAML_USE_NAME_ID_AS_USERNAME', False)
8587

86-
django_user_main_attribute = getattr(
87-
settings, 'SAML_DJANGO_USER_MAIN_ATTRIBUTE', 'username')
88+
django_user_main_attribute = saml_settings.SAML_DJANGO_USER_MAIN_ATTRIBUTE
89+
django_user_main_attribute_lookup = saml_settings.SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP
8890

8991
logger.debug('attributes: %s', attributes)
9092
saml_user = None
@@ -95,11 +97,7 @@ def authenticate(self, session_info=None, attribute_mapping=None,
9597
else:
9698
logger.error('The nameid is not available. Cannot find user without a nameid.')
9799
else:
98-
logger.debug('attribute_mapping: %s', attribute_mapping)
99-
for saml_attr, django_fields in attribute_mapping.items():
100-
if (django_user_main_attribute in django_fields
101-
and saml_attr in attributes):
102-
saml_user = attributes[saml_attr][0]
100+
saml_user = self.get_attribute_value(django_user_main_attribute, attributes, attribute_mapping)
103101

104102
if saml_user is None:
105103
logger.error('Could not find saml_user value')
@@ -108,46 +106,21 @@ def authenticate(self, session_info=None, attribute_mapping=None,
108106
if not self.is_authorized(attributes, attribute_mapping):
109107
return None
110108

111-
user = None
112-
113109
main_attribute = self.clean_user_main_attribute(saml_user)
114110

115-
user_query_args = {django_user_main_attribute: main_attribute}
116-
117111
# Note that this could be accomplished in one try-except clause, but
118112
# instead we use get_or_create when creating unknown users since it has
119113
# built-in safeguards for multiple threads.
120-
User = get_saml_user_model()
121-
if create_unknown_user:
122-
logger.debug('Check if the user "%s" exists or create otherwise',
123-
main_attribute)
124-
try:
125-
user, created = User.objects.get_or_create(**user_query_args)
126-
except MultipleObjectsReturned:
127-
logger.error("There are more than one user with %s = %s",
128-
django_user_main_attribute, main_attribute)
129-
return None
130-
131-
if created:
132-
logger.debug('New user created')
133-
user = self.configure_user(user, attributes, attribute_mapping)
134-
else:
135-
logger.debug('User updated')
136-
user = self.update_user(user, attributes, attribute_mapping)
137-
else:
138-
logger.debug('Retrieving existing user "%s"', main_attribute)
139-
try:
140-
user = User.objects.get(**user_query_args)
141-
user = self.update_user(user, attributes, attribute_mapping)
142-
except User.DoesNotExist:
143-
logger.error('The user "%s" does not exist', main_attribute)
144-
return None
145-
except MultipleObjectsReturned:
146-
logger.error("There are more than one user with %s = %s",
147-
django_user_main_attribute, main_attribute)
148-
return None
114+
return self.get_saml2_user(
115+
create_unknown_user, main_attribute, attributes, attribute_mapping)
149116

150-
return user
117+
def get_attribute_value(self, django_field, attributes, attribute_mapping):
118+
saml_user = None
119+
logger.debug('attribute_mapping: %s', attribute_mapping)
120+
for saml_attr, django_fields in attribute_mapping.items():
121+
if django_field in django_fields and saml_attr in attributes:
122+
saml_user = attributes[saml_attr][0]
123+
return saml_user
151124

152125
def is_authorized(self, attributes, attribute_mapping):
153126
"""Hook to allow custom authorization policies based on
@@ -164,6 +137,65 @@ def clean_user_main_attribute(self, main_attribute):
164137
"""
165138
return main_attribute
166139

140+
def get_user_query_args(self, main_attribute):
141+
django_user_main_attribute = getattr(
142+
settings, 'SAML_DJANGO_USER_MAIN_ATTRIBUTE', 'username')
143+
django_user_main_attribute_lookup = getattr(
144+
settings, 'SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP', '')
145+
146+
return {
147+
django_user_main_attribute + django_user_main_attribute_lookup: main_attribute
148+
}
149+
150+
def get_saml2_user(self, create, main_attribute, attributes, attribute_mapping):
151+
if create:
152+
return self._get_or_create_saml2_user(main_attribute, attributes, attribute_mapping)
153+
154+
return self._get_saml2_user(main_attribute, attributes, attribute_mapping)
155+
156+
def _get_or_create_saml2_user(self, main_attribute, attributes, attribute_mapping):
157+
logger.debug('Check if the user "%s" exists or create otherwise',
158+
main_attribute)
159+
django_user_main_attribute = saml_settings.SAML_DJANGO_USER_MAIN_ATTRIBUTE
160+
django_user_main_attribute_lookup = saml_settings.SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP
161+
user_query_args = self.get_user_query_args(main_attribute)
162+
user_create_defaults = {django_user_main_attribute: main_attribute}
163+
164+
User = get_saml_user_model()
165+
try:
166+
user, created = User.objects.get_or_create(
167+
defaults=user_create_defaults, **user_query_args)
168+
except MultipleObjectsReturned:
169+
logger.error("There are more than one user with %s = %s",
170+
django_user_main_attribute, main_attribute)
171+
return None
172+
173+
if created:
174+
logger.debug('New user created')
175+
user = self.configure_user(user, attributes, attribute_mapping)
176+
else:
177+
logger.debug('User updated')
178+
user = self.update_user(user, attributes, attribute_mapping)
179+
return user
180+
181+
def _get_saml2_user(self, main_attribute, attributes, attribute_mapping):
182+
User = get_saml_user_model()
183+
django_user_main_attribute = saml_settings.SAML_DJANGO_USER_MAIN_ATTRIBUTE
184+
user_query_args = self.get_user_query_args(main_attribute)
185+
186+
logger.debug('Retrieving existing user "%s"', main_attribute)
187+
try:
188+
user = User.objects.get(**user_query_args)
189+
user = self.update_user(user, attributes, attribute_mapping)
190+
except User.DoesNotExist:
191+
logger.error('The user "%s" does not exist', main_attribute)
192+
return None
193+
except MultipleObjectsReturned:
194+
logger.error("There are more than one user with %s = %s",
195+
django_user_main_attribute, main_attribute)
196+
return None
197+
return user
198+
167199
def configure_user(self, user, attributes, attribute_mapping):
168200
"""Configures a user after creation and returns the updated user.
169201
@@ -217,7 +249,8 @@ def update_user(self, user, attributes, attribute_mapping,
217249
logger.debug('Sending the pre_save signal')
218250
signal_modified = any(
219251
[response for receiver, response
220-
in pre_user_save.send_robust(sender=user,
252+
in pre_user_save.send_robust(sender=user.__class__,
253+
instance=user,
221254
attributes=attributes,
222255
user_modified=user_modified)]
223256
)
@@ -236,9 +269,9 @@ def _set_attribute(self, obj, attr, value):
236269
237270
Return True if the attribute was changed and False otherwise.
238271
"""
239-
field = obj._meta.get_field_by_name(attr)
240-
if len(value) > field[0].max_length:
241-
cleaned_value = value[:field[0].max_length]
272+
field = obj._meta.get_field(attr)
273+
if len(value) > field.max_length:
274+
cleaned_value = value[:field.max_length]
242275
logger.warn('The attribute "%s" was trimmed from "%s" to "%s"',
243276
attr, value, cleaned_value)
244277
else:

djangosaml2/settings.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.conf import settings
2+
3+
SAML_DJANGO_USER_MAIN_ATTRIBUTE = getattr(
4+
settings, 'SAML_DJANGO_USER_MAIN_ATTRIBUTE', 'username')
5+
SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP = getattr(
6+
settings, 'SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP', '')
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
4+
<head>
5+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
6+
</head>
7+
<body>
8+
<h1>Authentication Error.</h1>
9+
10+
<h2>Access Denied.</h2>
11+
12+
</body>
13+
</html>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
4+
<head>
5+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
6+
</head>
7+
<body>
8+
<h1>Permission Denied.</h1>
9+
10+
</body>
11+
</html>

djangosaml2/templates/djangosaml2/wayf.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ <h1>Where are you from?</h1>
99
<p>Please select your <strong>Identity Provider</strong> from the following list:</p>
1010
<ul>
1111
{% for url, name in available_idps %}
12-
<li><a href="{% url 'djangosaml2.views.login' %}?idp={{ url }}{% if came_from %}&next={{ came_from }}{% endif %}">{{ name }}</a></li>
12+
<li><a href="{% url 'saml2_login' %}?idp={{ url }}{% if came_from %}&next={{ came_from }}{% endif %}">{{ name }}</a></li>
1313
{% endfor %}
1414
</ul>
1515
</body>

0 commit comments

Comments
 (0)