summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--asgard/settings.d/10-apps.conf1
-rw-r--r--asgard/settings.d/20-saml.conf84
-rw-r--r--asgard/venv.conf5
-rw-r--r--coip/apps/auth/__init__.py79
-rw-r--r--coip/apps/utils/__init__.py0
-rw-r--r--coip/apps/utils/saml.py160
6 files changed, 253 insertions, 76 deletions
diff --git a/asgard/settings.d/10-apps.conf b/asgard/settings.d/10-apps.conf
index e0ccd0f..5b5efc9 100644
--- a/asgard/settings.d/10-apps.conf
+++ b/asgard/settings.d/10-apps.conf
@@ -12,5 +12,4 @@ INSTALLED_APPS += [
'coip.apps.saml2',
'coip.apps.resource',
'coip.apps.scim',
- 'coip.apps.services'
]
diff --git a/asgard/settings.d/20-saml.conf b/asgard/settings.d/20-saml.conf
index 833c21f..3d02fa4 100644
--- a/asgard/settings.d/20-saml.conf
+++ b/asgard/settings.d/20-saml.conf
@@ -1,81 +1,17 @@
-
from django.conf import settings
from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT
-METADATA = 'http://md.swamid.se/md/swamid-1.0.xml'
+SAML_METADATA_FILE = "/var/run/swamid-idp-transitive.xml"
+SAML_CREATE_UNKNOWN_USER = True
+SAML_CONFIG_LOADER = "coip.apps.auth.asgard_sp_config"
AUTH_PROFILE_MODULE = 'userprofile.UserProfile'
-#SAML_KEY = "/etc/ssl/private/ssl-cert-snakeoil.key"
-#SAML_CERT = "/etc/ssl/certs/ssl-cert-snakeoil.pem"
-
-SAML_ATTRIBUTE_MAPPING = {
- 'eduPersonPrincipalName': 'username',
- 'mail': 'email',
- 'givenName': 'first_name',
- 'sn': 'last_name',
-}
-
LOGIN_URL = '/saml2/sp/login/'
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
-
-def asgard_sp_config(request=None):
- host = "localhost"
- if request != None:
- host = request.get_host().replace(":","-")
- return {
- # your entity id, usually your subdomain plus the url to the metadata view
- 'entityid': 'https://coip.app.nordu.net/saml2/sp/metadata',
- # directory with attribute mapping
- "attribute_map_dir" : "%s/saml2/attributemaps" % settings.BASE_DIR,
- # this block states what services we provide
- 'service': {
- # we are just a lonely SP
- 'sp' : {
- 'name': 'COIP',
- 'endpoints': {
- # url and binding to the assertion consumer service view
- # do not change the binding osettingsr service name
- 'assertion_consumer_service': [
- ('https://coip.app.nordu.net/saml2/sp/acs/',
- BINDING_HTTP_POST),
- ],
- # url and binding to the single logout service view
- # do not change the binding or service name
- 'single_logout_service': [
- ('https://coip.app.nordu.net/saml2/sp/ls/',
- BINDING_HTTP_REDIRECT),
- ],
- },
- # attributes that this project need to identify a user
- 'required_attributes': ['eduPersonPrincipalName','displayName'],
- # attributes that may be useful to have but not required
- 'optional_attributes': ['eduPersonAffiliation'],
- }
- },
-
- # where the remote metadata is stored
- 'metadata': { 'local': ['/tmp/swamid-idp.xml'] },
-
- # set to 1 to output debugging information
- 'debug': 1,
-
- # certificate
- "key_file" : "%s/%s.key" % (settings.SSL_KEY_DIR,host),
- "cert_file" : "%s/%s.crt" % (settings.SSL_CRT_DIR,host),
- # own metadata settings
- 'contact_person': [
- {'given_name': 'Leif',
- 'sur_name': 'Johansson',
- 'company': 'NORDUnet',
- 'email_address': 'leifj@nordu.net',
- 'contact_type': 'technical'},
- ],
- # you can set multilanguage information here
- 'organization': {
- 'name': [('NORDUNet', 'en')],
- 'display_name': [('NORDUnet A/S', 'en')],
- 'url': [('http://www.nordu.net', 'en')],
- },
- 'valid_for': 24, # how long is our metadata valid
+SAML_ATTRIBUTE_MAPPING = {
+ 'username': ['eduPersonPrincipalName'],
+ 'first_name': ['givenName'],
+ 'last_name': ['sn'],
+ 'display_name': ['displayName','cn']
}
-
-SAML_CONFIG_GENERATOR = asgard_sp_config \ No newline at end of file
+#AUTHENTICATION_BACKENDS += ['coip.apps.utils.saml.TargetedUsernameSamlBackend']
+AUTO_REMOTE_SUPERUSERS = ['leifj@nordu.net'] \ No newline at end of file
diff --git a/asgard/venv.conf b/asgard/venv.conf
index f62501f..112c997 100644
--- a/asgard/venv.conf
+++ b/asgard/venv.conf
@@ -1,4 +1,4 @@
-django==1.3.1
+django==1.4.1
Werkzeug==0.6.2
anyjson==0.3.1
celery==2.3.3
@@ -8,6 +8,7 @@ django-form-utils==0.2.0
git+git://github.com/leifj/django-oauth2-lite.git
https://launchpad.net/pysaml2/main/0.4.2/+download/pysaml2-0.4.2.tar.gz
django-tagging==0.3.1
+djangosaml2==0.6.1
httplib2==0.6.0
importlib==1.0.2
lxml==2.3
@@ -27,3 +28,5 @@ django-activity-stream==0.3.9
python-memcached
hg+https://bitbucket.org/leifj/djangosaml2
iso8601
+django-taggit==0.9.3
+django-tastypie==0.9.11 \ No newline at end of file
diff --git a/coip/apps/auth/__init__.py b/coip/apps/auth/__init__.py
index e69de29..06effdf 100644
--- a/coip/apps/auth/__init__.py
+++ b/coip/apps/auth/__init__.py
@@ -0,0 +1,79 @@
+__author__ = 'leifj'
+
+from django.conf import settings
+from saml2.config import SPConfig
+import copy
+from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT
+
+import logging
+logging.basicConfig()
+logger = logging.getLogger("djangosaml2")
+logger.setLevel(logging.DEBUG)
+
+def asgard_sp_config(request=None):
+ host = "localhost"
+ if request != None:
+ host = request.get_host().replace(":","-")
+ x= {
+ # your entity id, usually your subdomain plus the url to the metadata view
+ 'entityid': 'https://coip.app.nordu.net/saml2/sp/metadata',
+ # directory with attribute mapping
+ "attribute_map_dir" : "%s/saml2/attributemaps" % settings.BASE_DIR,
+ # this block states what services we provide
+ 'service': {
+ # we are just a lonely SP
+ 'sp' : {
+ 'name': 'COIP',
+ 'endpoints': {
+ # url and binding to the assertion consumer service view
+ # do not change the binding osettingsr service name
+ 'assertion_consumer_service': [
+ ('https://coip.app.nordu.net/saml2/sp/acs/',
+ BINDING_HTTP_POST),
+ ],
+ # url and binding to the single logout service view
+ # do not change the binding or service name
+ 'single_logout_service': [
+ ('https://coip.app.nordu.net/saml2/sp/ls/',
+ BINDING_HTTP_REDIRECT),
+ ],
+ },
+ # attributes that this project need to identify a user
+ 'required_attributes': ['eduPersonPrincipalName','displayName'],
+ }
+ },
+
+ # where the remote metadata is stored
+ #'metadata': { 'remote': [{'url':'http://md.swamid.se/md/swamid-idp.xml',
+ # 'cert':'%s/saml2/credentials/md-signer.crt' % settings.BASE_DIR}] },
+ 'metadata': {'local': [settings.SAML_METADATA_FILE]},
+
+ # set to 1 to output debugging information
+ 'debug': 1,
+
+ # certificate
+ "key_file" : "%s/%s.key" % (settings.SSL_KEY_DIR,host),
+ "cert_file" : "%s/%s.crt" % (settings.SSL_CRT_DIR,host),
+ # own metadata settings
+ 'contact_person': [
+ {'given_name': 'Leif',
+ 'sur_name': 'Johansson',
+ 'company': 'NORDUnet',
+ 'email_address': 'leifj@nordu.net',
+ 'contact_type': 'technical'},
+ {'given_name': 'Johan',
+ 'sur_name': 'Berggren',
+ 'company': 'NORDUnet',
+ 'email_address': 'jbn@nordu.net',
+ 'contact_type': 'technical'},
+ ],
+ # you can set multilanguage information here
+ 'organization': {
+ 'name': [('NORDUNet', 'en')],
+ 'display_name': [('NORDUnet A/S', 'en')],
+ 'url': [('http://www.nordu.net', 'en')],
+ }
+ }
+ c = SPConfig()
+ c.load(copy.deepcopy(x))
+ return c
diff --git a/coip/apps/utils/__init__.py b/coip/apps/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/coip/apps/utils/__init__.py
diff --git a/coip/apps/utils/saml.py b/coip/apps/utils/saml.py
new file mode 100644
index 0000000..bad3d60
--- /dev/null
+++ b/coip/apps/utils/saml.py
@@ -0,0 +1,160 @@
+from django.contrib.auth.backends import ModelBackend
+from django.contrib.auth.models import SiteProfileNotAvailable, User
+from django.core.exceptions import ObjectDoesNotExist
+import logging
+from saml2.saml import name_id_type__from_string
+
+logger = logging.getLogger('djangosaml2')
+
+__author__ = 'leifj'
+
+class Saml2Backend(ModelBackend):
+
+ """This backend is added automatically by the assertion_consumer_service
+ view.
+
+ Don't add it to settings.AUTHENTICATION_BACKENDS.
+ """
+
+ def _set(self,o,django_attr,saml_attrs,attributes):
+ for saml_attr in saml_attrs:
+ if attributes.has_key(saml_attr):
+ setattr(o, django_attr, attributes[saml_attr][0])
+ return True
+ return False
+
+ def get_saml_user(self,session_info,attributes,attribute_mapping):
+ for saml_attr, django_fields in attribute_mapping.items():
+ if 'username' in django_fields and saml_attr in attributes:
+ return attributes[saml_attr][0]
+ return None
+
+ def authenticate(self, session_info=None, attribute_mapping=None,
+ create_unknown_user=True):
+ if session_info is None or attribute_mapping is None:
+ logger.error('Session info or attribute mapping are None')
+ return None
+
+ if not 'ava' in session_info:
+ logger.error('"ava" key not found in session_info')
+ return None
+
+ print session_info
+
+ attributes = session_info['ava']
+ if not attributes:
+ logger.error('The attributes dictionary is empty')
+
+ saml_user = self.get_saml_user(session_info,attributes,attribute_mapping)
+
+ if saml_user is None:
+ logger.error('Could not find saml_user value')
+ return None
+
+ user = None
+ username = self.clean_username(saml_user)
+
+ # Note that this could be accomplished in one try-except clause, but
+ # instead we use get_or_create when creating unknown users since it has
+ # built-in safeguards for multiple threads.
+ if create_unknown_user:
+ logger.debug('Check if the user "%s" exists or create otherwise' % username)
+ user, created = User.objects.get_or_create(username=username)
+ if created:
+ logger.debug('New user created')
+ user = self.configure_user(user, attributes, attribute_mapping)
+ else:
+ logger.debug('User updated')
+ user = self.update_user(user, attributes, attribute_mapping)
+ else:
+ logger.debug('Retrieving existing user "%s"' % username)
+ try:
+ user = User.objects.get(username=username)
+ user = self.update_user(user, attributes, attribute_mapping)
+ except User.DoesNotExist:
+ logger.error('The user "%s" does not exist' % username)
+ pass
+
+ return user
+
+ def clean_username(self, username):
+ """Performs any cleaning on the "username" prior to using it to get or
+ create the user object. Returns the cleaned username.
+
+ By default, returns the username unchanged.
+ """
+ return username
+
+ def configure_user(self, user, attributes, attribute_mapping):
+ """Configures a user after creation and returns the updated user.
+
+ By default, returns the user with his attributes updated.
+ """
+ user.set_unusable_password()
+ return self.update_user(user, attributes, attribute_mapping,
+ force_save=True)
+
+ def update_user(self, user, attributes, attribute_mapping, force_save=False):
+ """Update a user with a set of attributes and returns the updated user.
+
+ By default it uses a mapping defined in the settings constant
+ SAML_ATTRIBUTE_MAPPING. For each attribute, if the user object has
+ that field defined it will be set, otherwise it will try to set
+ it in the profile object.
+ """
+ if not attribute_mapping:
+ return user
+
+ try:
+ profile = user.get_profile()
+ except ObjectDoesNotExist:
+ profile = None
+ except SiteProfileNotAvailable:
+ profile = None
+
+ user_modified = False
+ profile_modified = False
+ for django_attr,saml_attrs in attribute_mapping.items():
+ try:
+ if hasattr(user, django_attr):
+ user_modified = self._set(user,django_attr,saml_attrs,attributes)
+
+ elif profile is not None and hasattr(profile, django_attr):
+ profile_modified = self._set(profile,django_attr,saml_attrs,attributes)
+
+ except KeyError:
+ # the saml attribute is missing
+ pass
+
+ if user_modified or force_save:
+ user.save()
+
+ if profile_modified or force_save:
+ profile.save()
+
+ return user
+
+class TargetedUsernameSamlBackend(Saml2Backend):
+ def get_saml_user(self,session_info,attributes,attribute_mapping):
+
+ eptid = attributes.get('eduPersonTargetedID',None)
+ if eptid is not None:
+ try:
+ name_id_o = name_id_type__from_string(eptid)
+ return "%s!%s!%s" % (name_id_o.name_qualifier,name_id_o.sp_name_qualifier,name_id_o.text)
+ except Exception,ex:
+ logger.error(ex)
+ pass
+
+ username = None
+ print attribute_mapping
+ if attribute_mapping.has_key('username'):
+ for saml_attr in attribute_mapping['username']:
+ if attributes.has_key(saml_attr):
+ username = attributes[saml_attr][0]
+
+ if username is None:
+ return None
+
+ return username
+ #return "%s!%s!%s" % (session_info['issuer'],session_info.get('entity_id',""),username) \ No newline at end of file