From ea884b9f0579f112d561be41d70de662f5a7186c Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Wed, 29 Jul 2015 18:22:22 -0500 Subject: [PATCH] Towards SAML2 support --- .gitignore | 3 ++ doc/misc.rst | 10 ++++ local_settings.py.example | 4 ++ relate/settings.py | 22 +++++++++ relate/urls.py | 8 ++++ requirements.txt | 4 ++ saml_config.py.example | 98 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 149 insertions(+) create mode 100644 saml_config.py.example diff --git a/.gitignore b/.gitignore index 72cffcc6..150a854f 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ components build *.pyz + +# This is guaranteed to be sensitive--never check it in. +saml-config diff --git a/doc/misc.rst b/doc/misc.rst index 222f65a6..febfb735 100644 --- a/doc/misc.rst +++ b/doc/misc.rst @@ -80,6 +80,16 @@ a customized version of This is needed about once every few hundred course update cycles, so relatively infrequently. +Setting up SAML2 / Shibboleth +----------------------------- + +- Install ``xmlsec1``. + +- Flip ``RELATE_ENABLE_SAML2`` to ``True``. + +- Edit :file:`samle_config.py` using :file:`saml_config.py.example` + as a guide. + How to translate RELATE ----------------------- diff --git a/local_settings.py.example b/local_settings.py.example index 9b75b3a1..016cc15e 100644 --- a/local_settings.py.example +++ b/local_settings.py.example @@ -61,6 +61,10 @@ ADMINS = ( TIME_ZONE = "America/Chicago" +RELATE_ENABLE_SAML2 = False +# If you enable this, you must also have saml_config.py in this directory. +# See saml_config.py.example for help. + # Uncomment one of these to choose the default sign-in method for students STUDENT_SIGN_IN_VIEW = "relate-sign_in_by_email" #STUDENT_SIGN_IN_VIEW = "relate-sign_in_by_user_pw" diff --git a/relate/settings.py b/relate/settings.py index 27e06931..04d802ed 100644 --- a/relate/settings.py +++ b/relate/settings.py @@ -46,6 +46,9 @@ INSTALLED_APPS = ( "djangobower", ) +if local_settings["RELATE_ENABLE_SAML2"]: + INSTALLED_APPS = INSTALLED_APPS + ("djangosaml2",) + MIDDLEWARE_CLASSES = ( "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.locale.LocaleMiddleware", @@ -64,6 +67,10 @@ AUTHENTICATION_BACKENDS = ( "django.contrib.auth.backends.ModelBackend", ) +if local_settings["RELATE_ENABLE_SAML2"]: + AUTHENTICATION_BACKENDS = AUTHENTICATION_BACKENDS + ( + 'djangosaml2.backends.Saml2Backend', + ) RELATE_EXTRA_CONTEXT_PROCESSORS = ( "relate.utils.settings_context_processor", @@ -178,3 +185,18 @@ TEMPLATES = [ LOCALE_PATHS = ( BASE_DIR+ '/locale', ) + +# This makes SAML2 logins compatible with (and usable at the same time as) +# email-based logins. +SAML_DJANGO_USER_MAIN_ATTRIBUTE = 'email' + +SAML_CREATE_UNKNOWN_USER = True + +SAML_ATTRIBUTE_MAPPING = { + 'uid': ('username', ), + 'mail': ('email', ), + 'cn': ('first_name', ), + 'sn': ('last_name', ), +} + +SAML_CONFIG = join(BASE_DIR, "saml_config.py") diff --git a/relate/urls.py b/relate/urls.py index 3fbbb0b8..c8d82724 100644 --- a/relate/urls.py +++ b/relate/urls.py @@ -387,4 +387,12 @@ if settings.RELATE_MAINTENANCE_MODE: url(r'^.*$', 'course.views.maintenance'), ] +if settings.RELATE_ENABLE_SAML2: + urlpatterns.extend([ + (r'^saml2/', include('djangosaml2.urls')), + + # Keep commented unless debugging SAML2. + (r'^saml2-test/', 'djangosaml2.views.echo_attributes'), + ]) + # vim: fdm=marker diff --git a/requirements.txt b/requirements.txt index fad2b6ff..866f837b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -62,3 +62,7 @@ ipaddr # For localized datetime format babel + +# For interoperation with SAML2/Shibboleth +pysaml2 +djangosaml2 diff --git a/saml_config.py.example b/saml_config.py.example new file mode 100644 index 00000000..cd396ce5 --- /dev/null +++ b/saml_config.py.example @@ -0,0 +1,98 @@ +from os import path +import saml2 +BASEDIR = path.dirname(path.abspath(__file__)) + +_BASE_URL = 'https://relate.cs.illinois.edu' + +SAML_CONFIG = { + # full path to the xmlsec1 binary programm + 'xmlsec_binary': '/usr/bin/xmlsec1', + + # your entity id, usually your subdomain plus the url to the metadata view + 'entityid': _BASE_URL + '/saml2/metadata/', + + # directory with attribute mapping + 'attribute_map_dir': path.join(BASEDIR, 'attribute-maps'), + + # this block states what services we provide + 'service': { + # we are just a lonely SP + 'sp' : { + 'name': 'RELATE SAML2 SP', + 'name_id_format': saml2.saml.NAMEID_FORMAT_PERSISTENT, + 'endpoints': { + # url and binding to the assertion consumer service view + # do not change the binding or service name + 'assertion_consumer_service': [ + (_BASE_URL + '/saml2/acs/', + saml2.BINDING_HTTP_POST), + ], + # url and binding to the single logout service view + # do not change the binding or service name + 'single_logout_service': [ + (_BASE_URL + '/saml2/ls/', + saml2.BINDING_HTTP_REDIRECT), + ], + (_BASE_URL + '/saml2/ls/post', + saml2.BINDING_HTTP_POST), + ], + }, + + # attributes that this project needs to identify a user + 'required_attributes': ['uid'], + + # attributes that may be useful to have but not required + 'optional_attributes': ['eduPersonAffiliation'], + + # in this section the list of IdPs we talk to are defined + 'idp': { + # we do not need a WAYF service since there is + # only an IdP defined here. This IdP should be + # present in our metadata + + # the keys of this dictionary are entity ids + 'https://localhost/simplesaml/saml2/idp/metadata.php': { + 'single_sign_on_service': { + saml2.BINDING_HTTP_REDIRECT: 'https://localhost/simplesaml/saml2/idp/SSOService.php', + }, + 'single_logout_service': { + saml2.BINDING_HTTP_REDIRECT: 'https://localhost/simplesaml/saml2/idp/SingleLogoutService.php', + }, + }, + }, + }, + }, + + # where the remote metadata is stored + 'metadata': { + 'local': [path.join(BASEDIR, 'saml-config', 'remote_metadata.xml')], + }, + + # set to 1 to output debugging information + 'debug': 1, + + # certificate + 'key_file': path.join(BASEDIR, 'saml-config', 'relate.key'), # private part + 'cert_file': path.join(BASEDIR, 'saml-config', 'relate.crt'), # public part + + # own metadata settings + 'contact_person': [ + {'given_name': 'Andreas', + 'sur_name': 'Kloeckner', + 'company': 'CS - University of Illinois', + 'email_address': 'andreask@illinois.edu', + 'contact_type': 'technical'}, + {'given_name': 'Andreas', + 'sur_name': 'Kloeckner', + 'company': 'CS - University of Illinois', + 'email_address': 'andreask@illinois.edu', + 'contact_type': 'administrative'}, + ], + # you can set multilanguage information here + 'organization': { + 'name': [('RELATE', 'en')], + 'display_name': [('RELATE', 'en')], + 'url': [(_BASE_URL, 'en')], + }, + 'valid_for': 24, # how long is our metadata valid + } -- GitLab