From c676429509d83b9a2d89a85260eb8968fe06a926 Mon Sep 17 00:00:00 2001 From: Johan Berggren Date: Wed, 19 Sep 2012 16:27:23 +0200 Subject: Make pysaml work in asgard --- asgard/settings.d/10-apps.conf | 1 - asgard/settings.d/20-saml.conf | 84 +++------------------- asgard/venv.conf | 5 +- coip/apps/auth/__init__.py | 79 ++++++++++++++++++++ coip/apps/utils/__init__.py | 0 coip/apps/utils/saml.py | 160 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 253 insertions(+), 76 deletions(-) create mode 100644 coip/apps/utils/__init__.py create mode 100644 coip/apps/utils/saml.py 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 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 -- cgit v1.1