Skip to content

Commit

Permalink
Add permission group mapping (#39)
Browse files Browse the repository at this point in the history
* Add permission group mapping

* Fixed typos in documentation
  • Loading branch information
soerenbe authored and bcail committed May 25, 2016
1 parent a80529c commit d655d9c
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 9 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Expand Up @@ -29,3 +29,6 @@ pip-log.txt

#Mr Developer
.mr.developer.cfg

# PyCharm
.idea
12 changes: 9 additions & 3 deletions README.md
Expand Up @@ -40,7 +40,7 @@ Installation and configuration
* Map Shibboleth attributes to Django User models. The attributes must be stated in the form they have in the HTTP headers.
Use this to populate the Django User object from Shibboleth attributes.

The first element of the tuple states if the attribute is required or not. If a reqired element is not found in the parsed
The first element of the tuple states if the attribute is required or not. If a reqired element is not found in the parsed
Shibboleth headers, an exception will be raised.
(True, "required_attribute")
(False, "optional_attribute).
Expand Down Expand Up @@ -92,8 +92,8 @@ If you would like to verify that everything is configured correctly, follow the

At this point, the django-shibboleth-remoteuser middleware should be complete.

##Optional
###Template tags
## Optional
### Template tags
* Template tags are included which will allow you to place {{ login_link }} or {{ logout_link }} in your templates for routing users to the login or logout page. These are available as a convenience and not required. To activate add the following to settings.py.

```python
Expand All @@ -103,4 +103,10 @@ At this point, the django-shibboleth-remoteuser middleware should be complete.
)
```

### Permission group mapping
* It is possible to map a list of attributes to Django permission groups. ```django-shibboleth-remoteuser``` will generate the groups from the semicolon-separated values of these attributes. They will be available in the Django admin interface and you can assign your application permissions to them.

```python
SHIBBOLETH_GROUP_ATTRIBUTES = ['Shibboleth-affiliation', 'Shibboleth-isMemberOf']
```
By default this value is empty and will not affect your group settings. But when you add attributes to ```SHIBBOLETH_GROUP_ATTRIBUTES``` the user will only associated with those groups. Be aware that the user will be removed from groups not defined in ```SHIBBOLETH_GROUP_ATTRIBUTES```, if you enable this setting. Some installations may create a lot of groups. You may check your group attributes at [https://your_domain.edu/Shibboleth.sso/Session]() before activating this feature.
3 changes: 3 additions & 0 deletions shibboleth/app_settings.py
Expand Up @@ -16,6 +16,9 @@
if not LOGIN_URL:
raise ImproperlyConfigured("A LOGIN_URL is required. Specify in settings.py")

# This list of attributes will map to Django permission groups
GROUP_ATTRIBUTES = getattr(settings, 'SHIBBOLETH_GROUP_ATTRIBUTES', [])

#Optional logout parameters
#This should look like: https://sso.school.edu/idp/logout.jsp?return=%s
#The return url variable will be replaced in the LogoutView.
Expand Down
28 changes: 27 additions & 1 deletion shibboleth/middleware.py
@@ -1,8 +1,9 @@
from django.contrib.auth.middleware import RemoteUserMiddleware
from django.contrib.auth.models import Group
from django.contrib import auth
from django.core.exceptions import ImproperlyConfigured

from shibboleth.app_settings import SHIB_ATTRIBUTE_MAP, LOGOUT_SESSION_KEY
from shibboleth.app_settings import SHIB_ATTRIBUTE_MAP, GROUP_ATTRIBUTES, LOGOUT_SESSION_KEY


class ShibbolethRemoteUserMiddleware(RemoteUserMiddleware):
Expand Down Expand Up @@ -61,6 +62,10 @@ def process_request(self, request):
auth.login(request, user)
user.set_unusable_password()
user.save()
# Upgrade user groups if configured in the settings.py
# If activated, the user will be associated with those groups.
if GROUP_ATTRIBUTES:
self.update_user_groups(request, user)
# call make profile.
self.make_profile(user, shib_meta)
# setup session.
Expand All @@ -81,6 +86,17 @@ def setup_session(self, request):
"""
return

def update_user_groups(self, request, user):
groups = self.parse_group_attributes(request)
# Remove the user from all groups that are not specified in the shibboleth metadata
for group in user.groups.all():
if group.name not in groups:
group.user_set.remove(user)
# Add the user to all groups in the shibboleth metadata
for g in groups:
group, created = Group.objects.get_or_create(name=g)
group.user_set.add(user)

@staticmethod
def parse_attributes(request):
"""
Expand All @@ -100,6 +116,16 @@ def parse_attributes(request):
error = True
return shib_attrs, error

@staticmethod
def parse_group_attributes(request):
"""
Parse the Shibboleth attributes for the GROUP_ATTRIBUTES and generate a list of them.
"""
groups = []
for attr in GROUP_ATTRIBUTES:
groups += filter(bool, request.META.get(attr, '').split(';'))
return groups


class ShibbolethValidationError(Exception):
pass
68 changes: 63 additions & 5 deletions shibboleth/tests/test_shib.py
Expand Up @@ -3,7 +3,7 @@

from django.conf import settings
from django.contrib import auth
from django.contrib.auth.models import User
from django.contrib.auth.models import User, Group
from django.test import TestCase, RequestFactory


Expand All @@ -28,7 +28,7 @@
"Shibboleth-persistent-id": "https://sso.college.edu/idp/shibboleth!https://server.college.edu/shibboleth-sp!sk1Z9qKruvXY7JXvsq4GRb8GCUk=",
"Shibboleth-sn": "Developer",
"Shibboleth-title": "Library Developer",
"Shibboleth-unscoped-affiliation": "member;staff"
"Shibboleth-unscoped-affiliation": "member;staff",
}

settings.SHIBBOLETH_ATTRIBUTE_MAP = {
Expand Down Expand Up @@ -58,7 +58,8 @@
settings.SHIBBOLETH_LOGOUT_REDIRECT_URL = 'http://school.edu/'

# MUST be imported after the settings above
from shibboleth.middleware import ShibbolethRemoteUserMiddleware
from shibboleth import app_settings
from shibboleth import middleware
from shibboleth import backends


Expand Down Expand Up @@ -97,7 +98,7 @@ def _get_valid_shib_meta(self, location='/'):
request_factory = RequestFactory()
test_request = request_factory.get(location)
test_request.META.update(**SAMPLE_HEADERS)
shib_meta, error = ShibbolethRemoteUserMiddleware.parse_attributes(test_request)
shib_meta, error = middleware.ShibbolethRemoteUserMiddleware.parse_attributes(test_request)
self.assertFalse(error, 'Generating shibboleth attribute mapping contains errors')
return shib_meta

Expand Down Expand Up @@ -134,10 +135,67 @@ def test_ensure_user_attributes(self):
self.assertEqual(user2.email, 'Sample_Developer@school.edu')


class TestShibbolethGroupAssignment(TestCase):

def test_unconfigured_group(self):
# by default SHIBBOLETH_GROUP_ATTRIBUTES = [] - so no groups will be touched
with self.settings(SHIBBOLETH_GROUP_ATTRIBUTES=[]):
reload(app_settings)
reload(middleware)
# After login the user will be created
self.client.get('/', **SAMPLE_HEADERS)
query = User.objects.filter(username='sampledeveloper@school.edu')
# Ensure the user was created
self.assertEqual(len(query), 1)
user = User.objects.get(username='sampledeveloper@school.edu')
# The user should have no groups
self.assertEqual(len(user.groups.all()), 0)
# Create a group and add the user
g = Group(name='Testgroup')
g.save()
# Now we should have exactly one group
self.assertEqual(len(Group.objects.all()), 1)
g.user_set.add(user)
# Now the user should be in exactly one group
self.assertEqual(len(user.groups.all()), 1)
self.client.get('/', **SAMPLE_HEADERS)
# After a request the user should still be in the group.
self.assertEqual(len(user.groups.all()), 1)

def test_group_creation(self):
# Test for group creation
with self.settings(SHIBBOLETH_GROUP_ATTRIBUTES=['Shibboleth-affiliation']):
reload(app_settings)
reload(middleware)
self.client.get('/', **SAMPLE_HEADERS)
user = User.objects.get(username='sampledeveloper@school.edu')
self.assertEqual(len(Group.objects.all()), 2)
self.assertEqual(len(user.groups.all()), 2)

def test_group_creation_list(self):
# Test for group creation from a list of group attributes
with self.settings(SHIBBOLETH_GROUP_ATTRIBUTES=['Shibboleth-affiliation', 'Shibboleth-isMemberOf']):
reload(app_settings)
reload(middleware)
self.client.get('/', **SAMPLE_HEADERS)
user = User.objects.get(username='sampledeveloper@school.edu')
self.assertEqual(len(Group.objects.all()), 6)
self.assertEqual(len(user.groups.all()), 6)

def test_empty_group_attribute(self):
# Test everthing is working even if the group attribute is missing in the shibboleth data
with self.settings(SHIBBOLETH_GROUP_ATTRIBUTES=['Shibboleth-not-existing-attribute']):
reload(app_settings)
reload(middleware)
self.client.get('/', **SAMPLE_HEADERS)
user = User.objects.get(username='sampledeveloper@school.edu')
self.assertEqual(len(Group.objects.all()), 0)
self.assertEqual(len(user.groups.all()), 0)


class LogoutTest(TestCase):

def test_logout(self):
from shibboleth import app_settings
# Login
login = self.client.get('/', **SAMPLE_HEADERS)
self.assertEqual(login.status_code, 200)
Expand Down

0 comments on commit d655d9c

Please sign in to comment.