From 4c2b12b710c762e2471a4ede54e59b54f9f06fe6 Mon Sep 17 00:00:00 2001 From: Jim Laney Date: Thu, 27 Aug 2020 09:12:05 -0700 Subject: [PATCH 1/3] adds settings for blti auth --- project/base_settings/auth_settings.py | 159 ++++++++++++++----------- tests/test_settings/test_auth.py | 60 ++++++++-- 2 files changed, 139 insertions(+), 80 deletions(-) diff --git a/project/base_settings/auth_settings.py b/project/base_settings/auth_settings.py index 3df7eb8..0e260d1 100644 --- a/project/base_settings/auth_settings.py +++ b/project/base_settings/auth_settings.py @@ -1,81 +1,96 @@ import os -from .common import INSTALLED_APPS, AUTHENTICATION_BACKENDS +import json +from .common import INSTALLED_APPS, MIDDLEWARE, AUTHENTICATION_BACKENDS from .setting_utils import parse_bool_from_str +for auth in os.getenv('AUTH', '').split(' '): + if auth.startswith('SAML') and 'uw_saml' not in INSTALLED_APPS: + INSTALLED_APPS += ['uw_saml'] + LOGIN_URL = '/saml/login' + LOGOUT_URL = '/saml/logout' + SAML_USER_ATTRIBUTE = os.getenv('SAML_USER_ATTRIBUTE', 'uwnetid') + SAML_FORCE_AUTHN = parse_bool_from_str(os.getenv('SAML_FORCE_AUTHN', 'False')) -if os.getenv('AUTH', '').startswith('SAML'): - INSTALLED_APPS += ['uw_saml'] - LOGIN_URL = '/saml/login' - LOGOUT_URL = '/saml/logout' - SAML_USER_ATTRIBUTE = os.getenv('SAML_USER_ATTRIBUTE', 'uwnetid') - SAML_FORCE_AUTHN = parse_bool_from_str(os.getenv('SAML_FORCE_AUTHN', 'False')) - - if os.getenv('AUTH', '') == 'SAML_MOCK' or os.getenv('AUTH', '') == 'SAML_DJANGO_LOGIN': - DEFAULT_SAML_ATTRIBUTES = { - 'uwnetid': ['javerage'], - 'affiliations': ['student', 'member', 'alum', 'staff', 'employee'], - 'eppn': ['javerage@washington.edu'], - 'scopedAffiliations': [ - 'student@washington.edu', 'member@washington.edu', - 'alum@washington.edu', 'staff@washington.edu', - 'employee@washington.edu'], - 'isMemberOf': ['u_test_group', 'u_test_another_group'] - } - if os.getenv('AUTH', '') == 'SAML_MOCK': - MOCK_SAML_ATTRIBUTES = DEFAULT_SAML_ATTRIBUTES - - else: - AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',) - DJANGO_LOGIN_MOCK_SAML = { - 'NAME_ID': 'mock-nameid', - 'SESSION_INDEX': 'mock-session', - 'SAML_USERS': [{ - 'username': os.getenv('DJANGO_LOGIN_USERNAME', 'javerage'), - 'password': os.getenv('DJANGO_LOGIN_PASSWORD', 'javerage'), - 'email': os.getenv('DJANGO_LOGIN_EMAIL', 'javerage@uw.edu'), - 'MOCK_ATTRIBUTES': DEFAULT_SAML_ATTRIBUTES, - }] + if auth == 'SAML_MOCK' or auth == 'SAML_DJANGO_LOGIN': + DEFAULT_SAML_ATTRIBUTES = { + 'uwnetid': ['javerage'], + 'affiliations': ['student', 'member', 'alum', 'staff', 'employee'], + 'eppn': ['javerage@washington.edu'], + 'scopedAffiliations': [ + 'student@washington.edu', 'member@washington.edu', + 'alum@washington.edu', 'staff@washington.edu', + 'employee@washington.edu'], + 'isMemberOf': ['u_test_group', 'u_test_another_group'] } + if auth == 'SAML_MOCK': + MOCK_SAML_ATTRIBUTES = DEFAULT_SAML_ATTRIBUTES - elif os.getenv('AUTH', '') == 'SAML': - CLUSTER_CNAME = os.getenv('CLUSTER_CNAME', 'localhost') - UW_SAML = { - 'strict': True, - 'debug': True, - 'sp': { - 'entityId': os.getenv('SAML_ENTITY_ID', 'https://' + CLUSTER_CNAME + '/saml'), - 'assertionConsumerService': { - 'url': 'https://' + CLUSTER_CNAME + '/saml/sso', - 'binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' - }, - 'singleLogoutService': { - 'url': 'https://' + CLUSTER_CNAME + LOGOUT_URL, - 'binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' - }, - 'NameIDFormat': 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', - 'x509cert': os.getenv('SP_CERT', ''), - 'privateKey': os.getenv('SP_PRIVATE_KEY', ''), - }, - 'idp': { - 'entityId': 'urn:mace:incommon:washington.edu', - 'singleSignOnService': { - 'url': 'https://idp.u.washington.edu/idp/profile/SAML2/Redirect/SSO', - 'binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' + else: + AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',) + DJANGO_LOGIN_MOCK_SAML = { + 'NAME_ID': 'mock-nameid', + 'SESSION_INDEX': 'mock-session', + 'SAML_USERS': [{ + 'username': os.getenv('DJANGO_LOGIN_USERNAME', 'javerage'), + 'password': os.getenv('DJANGO_LOGIN_PASSWORD', 'javerage'), + 'email': os.getenv('DJANGO_LOGIN_EMAIL', 'javerage@uw.edu'), + 'MOCK_ATTRIBUTES': DEFAULT_SAML_ATTRIBUTES, + }] + } + + elif auth == 'SAML': + CLUSTER_CNAME = os.getenv('CLUSTER_CNAME', 'localhost') + UW_SAML = { + 'strict': True, + 'debug': True, + 'sp': { + 'entityId': os.getenv('SAML_ENTITY_ID', 'https://' + CLUSTER_CNAME + '/saml'), + 'assertionConsumerService': { + 'url': 'https://' + CLUSTER_CNAME + '/saml/sso', + 'binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' + }, + 'singleLogoutService': { + 'url': 'https://' + CLUSTER_CNAME + LOGOUT_URL, + 'binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' + }, + 'NameIDFormat': 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', + 'x509cert': os.getenv('SP_CERT', ''), + 'privateKey': os.getenv('SP_PRIVATE_KEY', ''), }, - 'singleLogoutService': { - 'url': 'https://idp.u.washington.edu/idp/logout', - 'binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' + 'idp': { + 'entityId': 'urn:mace:incommon:washington.edu', + 'singleSignOnService': { + 'url': 'https://idp.u.washington.edu/idp/profile/SAML2/Redirect/SSO', + 'binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' + }, + 'singleLogoutService': { + 'url': 'https://idp.u.washington.edu/idp/logout', + 'binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' + }, + 'x509cert': os.getenv('IDP_CERT', ''), }, - 'x509cert': os.getenv('IDP_CERT', ''), - }, - 'security': { - 'authnRequestsSigned': parse_bool_from_str(os.getenv('SP_AUTHN_REQUESTS_SIGNED', 'False')), - 'wantMessagesSigned': parse_bool_from_str(os.getenv('SP_WANT_MESSAGES_SIGNED', 'True')), - 'wantAssertionsSigned': parse_bool_from_str(os.getenv('SP_WANT_ASSERTIONS_SIGNED', 'False')), - 'wantAssertionsEncrypted': parse_bool_from_str(os.getenv('SP_WANT_ASSERTIONS_ENCRYPTED', 'False')), - 'requestedAuthnContext': [ - 'urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken' - ] if parse_bool_from_str(os.getenv('SP_USE_2FA', 'False')) else False, - 'failOnAuthnContextMismatch': parse_bool_from_str(os.getenv('SP_USE_2FA', 'False')), + 'security': { + 'authnRequestsSigned': parse_bool_from_str(os.getenv('SP_AUTHN_REQUESTS_SIGNED', 'False')), + 'wantMessagesSigned': parse_bool_from_str(os.getenv('SP_WANT_MESSAGES_SIGNED', 'True')), + 'wantAssertionsSigned': parse_bool_from_str(os.getenv('SP_WANT_ASSERTIONS_SIGNED', 'False')), + 'wantAssertionsEncrypted': parse_bool_from_str(os.getenv('SP_WANT_ASSERTIONS_ENCRYPTED', 'False')), + 'requestedAuthnContext': [ + 'urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken' + ] if parse_bool_from_str(os.getenv('SP_USE_2FA', 'False')) else False, + 'failOnAuthnContextMismatch': parse_bool_from_str(os.getenv('SP_USE_2FA', 'False')), + } } - } + + if auth.startswith('BLTI') and 'blti' not in INSTALLED_APPS: + INSTALLED_APPS += ['blti'] + + MIDDLEWARE = ['blti.middleware.CSRFHeaderMiddleware', + 'blti.middleware.SessionHeaderMiddleware'] + MIDDLEWARE + + # BLTI consumer key:secret pairs in env as a serialized dict + LTI_CONSUMERS = json.loads(os.getenv('LTI_CONSUMERS', '{}')) + LTI_ENFORCE_SSL = parse_bool_from_str(os.getenv('LTI_ENFORCE_SSL', 'False')) + + # BLTI session object encryption values + BLTI_AES_KEY = bytes(os.getenv('BLTI_AES_KEY', ''), encoding='utf8') + BLTI_AES_IV = bytes(os.getenv('BLTI_AES_IV', ''), encoding='utf8') diff --git a/tests/test_settings/test_auth.py b/tests/test_settings/test_auth.py index a689652..31fc552 100644 --- a/tests/test_settings/test_auth.py +++ b/tests/test_settings/test_auth.py @@ -1,7 +1,8 @@ from unittest import TestCase from ..utils import SettingLoader -class BaseAuthTest: + +class SAMLBaseAuthTest: def test_common_attributes(self): with SettingLoader('project.base_settings', **self.mock_env) as base_settings: self.assertIn('uw_saml', base_settings.INSTALLED_APPS) @@ -20,7 +21,8 @@ def test_common_attributes(self): with SettingLoader('project.base_settings', **self.mock_env) as base_settings: self.assertFalse(base_settings.SAML_FORCE_AUTHN) -class TestNoAuthBackend(TestCase): + +class NoAuthBackendTest(TestCase): def test_common_attributes(self): with SettingLoader('project.base_settings') as base_settings: self.assertNotIn('uw_saml', base_settings.INSTALLED_APPS) @@ -29,7 +31,8 @@ def test_common_attributes(self): self.assertFalse(hasattr(base_settings, 'SAML_USER_ATTRIBUTE')) self.assertFalse(hasattr(base_settings, 'SAML_FORCE_AUTHN')) -class TestSAML(TestCase, BaseAuthTest): + +class SAMLTest(TestCase, SAMLBaseAuthTest): def setUp(self): self.mock_env = { 'AUTH': 'SAML', @@ -39,11 +42,12 @@ def setUp(self): def test_attributes(self): with SettingLoader('project.base_settings', **self.mock_env) as base_settings: self.assertEqual(self.mock_env['CLUSTER_CNAME'], base_settings.CLUSTER_CNAME) - + # Just tests that UW_SAML attributes exists self.assertIsNotNone(base_settings.UW_SAML) -class TestSAMLMOCK(TestCase, BaseAuthTest): + +class SAMLMockTest(TestCase, SAMLBaseAuthTest): def setUp(self): self.mock_env = { 'AUTH': 'SAML_MOCK', @@ -53,10 +57,11 @@ def test_attributes(self): with SettingLoader('project.base_settings', **self.mock_env) as base_settings: self.assertIsNotNone(base_settings.DEFAULT_SAML_ATTRIBUTES) self.assertIsNotNone(base_settings.MOCK_SAML_ATTRIBUTES) - + self.assertDictEqual(base_settings.DEFAULT_SAML_ATTRIBUTES, base_settings.MOCK_SAML_ATTRIBUTES) -class TestSAMLDJANGOLOGIN(TestCase, BaseAuthTest): + +class SAMLDjangoLoginTest(TestCase, SAMLBaseAuthTest): def setUp(self): self.mock_env = { 'AUTH': 'SAML_DJANGO_LOGIN', @@ -73,8 +78,47 @@ def test_attributes(self): def test_django_user_conf(self): with SettingLoader('project.base_settings', **self.mock_env) as base_settings: self.assertEqual(len(base_settings.DJANGO_LOGIN_MOCK_SAML['SAML_USERS']), 1) - + self.assertEqual(base_settings.DJANGO_LOGIN_MOCK_SAML['SAML_USERS'][0]['username'], self.mock_env['DJANGO_LOGIN_USERNAME']) self.assertEqual(base_settings.DJANGO_LOGIN_MOCK_SAML['SAML_USERS'][0]['password'], self.mock_env['DJANGO_LOGIN_PASSWORD']) self.assertEqual(base_settings.DJANGO_LOGIN_MOCK_SAML['SAML_USERS'][0]['email'], self.mock_env['DJANGO_LOGIN_EMAIL']) self.assertDictEqual(base_settings.DJANGO_LOGIN_MOCK_SAML['SAML_USERS'][0]['MOCK_ATTRIBUTES'], base_settings.DEFAULT_SAML_ATTRIBUTES) + + +class BLTITest(TestCase): + def setUp(self): + self.mock_env = { + 'AUTH': 'BLTI', + 'LTI_CONSUMERS': '{"0000-0000-0000": "01234567ABCDEF"}', + 'BLTI_AES_KEY': 'AE91AE1DF0E6FB44', + 'BLTI_AES_IV': '01C8837249AE8667', + } + + def test_attributes(self): + with SettingLoader('project.base_settings', **self.mock_env) as base_settings: + self.assertIn('blti', base_settings.INSTALLED_APPS) + self.assertIn('blti.middleware.SessionHeaderMiddleware', base_settings.MIDDLEWARE) + self.assertDictEqual({"0000-0000-0000": "01234567ABCDEF"}, base_settings.LTI_CONSUMERS) + + +class MultipleAuthTest(TestCase): + def test_valid_auth_list(self): + mock_env = { + 'AUTH': 'SAML BLTI', + } + + with SettingLoader('project.base_settings', **mock_env) as base_settings: + self.assertIn('uw_saml', base_settings.INSTALLED_APPS) + self.assertIn('blti', base_settings.INSTALLED_APPS) + + def test_invalid_auth_list(self): + mock_env = { + 'AUTH': 'SAML SAML_MOCK', + } + + with SettingLoader('project.base_settings', **mock_env) as base_settings: + self.assertIn('uw_saml', base_settings.INSTALLED_APPS) + + # uw_saml app must not appear in INSTALLED_APPS more then once + self.assertEqual(len(set(base_settings.INSTALLED_APPS)), + len(base_settings.INSTALLED_APPS)) From e395e1ac131065a45d12515d9c4fa0af4eee2175 Mon Sep 17 00:00:00 2001 From: Jim Laney Date: Thu, 27 Aug 2020 09:25:36 -0700 Subject: [PATCH 2/3] add to middleware without creating a new list --- project/base_settings/auth_settings.py | 4 ++-- tests/test_settings/test_auth.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/project/base_settings/auth_settings.py b/project/base_settings/auth_settings.py index 0e260d1..55198b8 100644 --- a/project/base_settings/auth_settings.py +++ b/project/base_settings/auth_settings.py @@ -84,8 +84,8 @@ if auth.startswith('BLTI') and 'blti' not in INSTALLED_APPS: INSTALLED_APPS += ['blti'] - MIDDLEWARE = ['blti.middleware.CSRFHeaderMiddleware', - 'blti.middleware.SessionHeaderMiddleware'] + MIDDLEWARE + MIDDLEWARE.insert(0, 'blti.middleware.SessionHeaderMiddleware') + MIDDLEWARE.insert(0, 'blti.middleware.CSRFHeaderMiddleware') # BLTI consumer key:secret pairs in env as a serialized dict LTI_CONSUMERS = json.loads(os.getenv('LTI_CONSUMERS', '{}')) diff --git a/tests/test_settings/test_auth.py b/tests/test_settings/test_auth.py index 31fc552..a1272f8 100644 --- a/tests/test_settings/test_auth.py +++ b/tests/test_settings/test_auth.py @@ -98,6 +98,7 @@ def test_attributes(self): with SettingLoader('project.base_settings', **self.mock_env) as base_settings: self.assertIn('blti', base_settings.INSTALLED_APPS) self.assertIn('blti.middleware.SessionHeaderMiddleware', base_settings.MIDDLEWARE) + self.assertIn('blti.middleware.CSRFHeaderMiddleware', base_settings.MIDDLEWARE) self.assertDictEqual({"0000-0000-0000": "01234567ABCDEF"}, base_settings.LTI_CONSUMERS) From e33e714a6c9759cc9399902d459b3001ead9fccc Mon Sep 17 00:00:00 2001 From: Jim Laney Date: Thu, 27 Aug 2020 09:38:37 -0700 Subject: [PATCH 3/3] use append instead of list addition --- project/base_settings/auth_settings.py | 5 +++-- project/base_settings/prometheus_settings.py | 8 +++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/project/base_settings/auth_settings.py b/project/base_settings/auth_settings.py index 55198b8..a61268d 100644 --- a/project/base_settings/auth_settings.py +++ b/project/base_settings/auth_settings.py @@ -5,7 +5,8 @@ for auth in os.getenv('AUTH', '').split(' '): if auth.startswith('SAML') and 'uw_saml' not in INSTALLED_APPS: - INSTALLED_APPS += ['uw_saml'] + INSTALLED_APPS.append('uw_saml') + LOGIN_URL = '/saml/login' LOGOUT_URL = '/saml/logout' SAML_USER_ATTRIBUTE = os.getenv('SAML_USER_ATTRIBUTE', 'uwnetid') @@ -82,7 +83,7 @@ } if auth.startswith('BLTI') and 'blti' not in INSTALLED_APPS: - INSTALLED_APPS += ['blti'] + INSTALLED_APPS.append('blti') MIDDLEWARE.insert(0, 'blti.middleware.SessionHeaderMiddleware') MIDDLEWARE.insert(0, 'blti.middleware.CSRFHeaderMiddleware') diff --git a/project/base_settings/prometheus_settings.py b/project/base_settings/prometheus_settings.py index ecf6478..19e924f 100644 --- a/project/base_settings/prometheus_settings.py +++ b/project/base_settings/prometheus_settings.py @@ -1,12 +1,10 @@ import os from .common import INSTALLED_APPS, MIDDLEWARE, DATABASES +INSTALLED_APPS.append('django_prometheus') -INSTALLED_APPS += ['django_prometheus'] - -MIDDLEWARE = ['django_prometheus.middleware.PrometheusBeforeMiddleware'] + \ - MIDDLEWARE + \ - ['django_prometheus.middleware.PrometheusAfterMiddleware'] +MIDDLEWARE.insert(0, 'django_prometheus.middleware.PrometheusBeforeMiddleware') +MIDDLEWARE.append('django_prometheus.middleware.PrometheusAfterMiddleware') if os.getenv('DB', 'sqlite3') == 'sqlite3': DATABASES['default']['ENGINE'] = 'django_prometheus.db.backends.sqlite3'