|
17 | 17 | import datetime
|
18 | 18 | import re
|
19 | 19 | import sys
|
20 |
| - |
| 20 | +from importlib import import_module |
21 | 21 | from unittest import mock, skip
|
| 22 | +from urllib.parse import parse_qs, urlparse |
22 | 23 |
|
23 | 24 | from django.conf import settings
|
24 | 25 | from django.contrib.auth import SESSION_KEY, get_user_model
|
25 | 26 | from django.contrib.auth.models import AnonymousUser
|
| 27 | +from django.core.exceptions import ImproperlyConfigured |
26 | 28 | from django.http.request import HttpRequest
|
27 | 29 | from django.template import Context, Template
|
28 |
| -from django.test import TestCase, Client |
| 30 | +from django.test import Client, TestCase |
29 | 31 | from django.test.client import RequestFactory
|
| 32 | +from django.urls import reverse |
| 33 | +from django.utils.encoding import force_text |
30 | 34 | from djangosaml2 import views
|
31 | 35 | from djangosaml2.cache import OutstandingQueriesCache
|
32 | 36 | from djangosaml2.conf import get_config
|
33 | 37 | from djangosaml2.middleware import SamlSessionMiddleware
|
34 | 38 | from djangosaml2.signals import post_authenticated, pre_user_save
|
35 | 39 | from djangosaml2.tests import conf
|
36 |
| -from djangosaml2.tests.utils import SAMLPostFormParser |
37 | 40 | from djangosaml2.tests.auth_response import auth_response
|
38 |
| -from djangosaml2.views import finish_logout |
39 |
| -from djangosaml2.utils import (saml2_from_httpredirect_request, |
| 41 | +from djangosaml2.tests.utils import SAMLPostFormParser |
| 42 | +from djangosaml2.utils import (get_idp_sso_supported_bindings, |
40 | 43 | get_session_id_from_saml2,
|
41 |
| - get_subject_id_from_saml2) |
42 |
| -from importlib import import_module |
| 44 | + get_subject_id_from_saml2, |
| 45 | + saml2_from_httpredirect_request) |
| 46 | +from djangosaml2.views import finish_logout |
43 | 47 | from saml2.config import SPConfig
|
44 | 48 | from saml2.s_utils import decode_base64_and_inflate, deflate_and_base64_encode
|
45 | 49 |
|
46 |
| -try: |
47 |
| - from django.urls import reverse |
48 |
| -except ImportError: |
49 |
| - from django.core.urlresolvers import reverse |
50 |
| -try: |
51 |
| - from django.utils.encoding import force_text |
52 |
| -except ImportError: |
53 |
| - from django.utils.text import force_text |
54 |
| -try: |
55 |
| - from django.utils.six.moves.urllib.parse import urlparse, parse_qs |
56 |
| -except ImportError: |
57 |
| - from urllib.parse import urlparse, parse_qs |
58 |
| - |
59 |
| - |
60 | 50 | User = get_user_model()
|
61 | 51 |
|
62 | 52 | PY_VERSION = sys.version_info[:2]
|
63 | 53 |
|
64 | 54 |
|
| 55 | +def dummy_loader(request): |
| 56 | + return 'dummy_loader' |
| 57 | + |
| 58 | + |
| 59 | +non_callable = 'just a string' |
| 60 | + |
| 61 | + |
| 62 | +class UtilsTests(TestCase): |
| 63 | + def test_get_config_valid_path(self): |
| 64 | + self.assertEqual(get_config('djangosaml2.tests.dummy_loader'), 'dummy_loader') |
| 65 | + |
| 66 | + def test_get_config_wrongly_formatted_path(self): |
| 67 | + with self.assertRaisesMessage(ImproperlyConfigured, 'SAML config loader must be a callable object.'): |
| 68 | + get_config('djangosaml2.tests.non_callable') |
| 69 | + |
| 70 | + def test_get_config_nonsense_path(self): |
| 71 | + with self.assertRaisesMessage(ImproperlyConfigured, 'Error importing SAML config loader lalala.nonexisting.blabla: "No module named \'lalala\'"'): |
| 72 | + get_config('lalala.nonexisting.blabla') |
| 73 | + |
| 74 | + def test_get_config_missing_function(self): |
| 75 | + with self.assertRaisesMessage(ImproperlyConfigured, 'Module "djangosaml2.tests" does not define a "nonexisting_function" config loader'): |
| 76 | + get_config('djangosaml2.tests.nonexisting_function') |
| 77 | + |
| 78 | + |
65 | 79 | class SAML2Tests(TestCase):
|
66 | 80 |
|
67 | 81 | urls = 'djangosaml2.tests.urls'
|
@@ -117,6 +131,32 @@ def render_template(self, text):
|
117 | 131 | def b64_for_post(self, xml_text, encoding='utf-8'):
|
118 | 132 | return base64.b64encode(xml_text.encode(encoding)).decode('ascii')
|
119 | 133 |
|
| 134 | + def test_get_idp_sso_supported_bindings_noargs(self): |
| 135 | + settings.SAML_CONFIG = conf.create_conf( |
| 136 | + sp_host='sp.example.com', |
| 137 | + idp_hosts=['idp.example.com'], |
| 138 | + metadata_file='remote_metadata_one_idp.xml', |
| 139 | + ) |
| 140 | + idp_id = 'https://idp.example.com/simplesaml/saml2/idp/metadata.php' |
| 141 | + self.assertEqual(list(get_idp_sso_supported_bindings())[0], list(settings.SAML_CONFIG['service']['sp']['idp'][idp_id]['single_sign_on_service'].keys())[0]) |
| 142 | + |
| 143 | + def test_get_idp_sso_supported_bindings_unknown_idp(self): |
| 144 | + settings.SAML_CONFIG = conf.create_conf( |
| 145 | + sp_host='sp.example.com', |
| 146 | + idp_hosts=['idp.example.com'], |
| 147 | + metadata_file='remote_metadata_one_idp.xml', |
| 148 | + ) |
| 149 | + self.assertEqual(get_idp_sso_supported_bindings(idp_entity_id='random'), []) |
| 150 | + |
| 151 | + def test_get_idp_sso_supported_bindings_no_idps(self): |
| 152 | + settings.SAML_CONFIG = conf.create_conf( |
| 153 | + sp_host='sp.example.com', |
| 154 | + idp_hosts=[], |
| 155 | + metadata_file='remote_metadata_no_idp.xml', |
| 156 | + ) |
| 157 | + with self.assertRaisesMessage(ImproperlyConfigured, "No IdP configured!"): |
| 158 | + get_idp_sso_supported_bindings() |
| 159 | + |
120 | 160 | def test_unsigned_post_authn_request(self):
|
121 | 161 | """
|
122 | 162 | Test that unsigned authentication requests via POST binding
|
@@ -161,6 +201,24 @@ def test_login_evil_redirect(self):
|
161 | 201 |
|
162 | 202 | self.assertEqual(params['RelayState'], [settings.LOGIN_REDIRECT_URL, ])
|
163 | 203 |
|
| 204 | + def test_no_redirect(self): |
| 205 | + """ |
| 206 | + Make sure that if we give an empty path as the next parameter, |
| 207 | + it is replaced with the default LOGIN_REDIRECT_URL. |
| 208 | + """ |
| 209 | + |
| 210 | + # monkey patch SAML configuration |
| 211 | + settings.SAML_CONFIG = conf.create_conf( |
| 212 | + sp_host='sp.example.com', |
| 213 | + idp_hosts=['idp.example.com'], |
| 214 | + metadata_file='remote_metadata_one_idp.xml', |
| 215 | + ) |
| 216 | + response = self.client.get(reverse('saml2_login') + '?next=') |
| 217 | + url = urlparse(response['Location']) |
| 218 | + params = parse_qs(url.query) |
| 219 | + |
| 220 | + self.assertEqual(params['RelayState'], [settings.LOGIN_REDIRECT_URL, ]) |
| 221 | + |
164 | 222 | def test_login_one_idp(self):
|
165 | 223 | # monkey patch SAML configuration
|
166 | 224 | settings.SAML_CONFIG = conf.create_conf(
|
@@ -236,7 +294,6 @@ def test_login_several_idps(self):
|
236 | 294 | if 'AuthnRequest xmlns' not in decode_base64_and_inflate(saml_request).decode('utf-8'):
|
237 | 295 | raise Exception('Not a valid AuthnRequest')
|
238 | 296 |
|
239 |
| - |
240 | 297 | def test_assertion_consumer_service(self):
|
241 | 298 | # Get initial number of users
|
242 | 299 | initial_user_count = User.objects.count()
|
@@ -293,6 +350,27 @@ def test_assertion_consumer_service(self):
|
293 | 350 | self.assertEqual(url.path, settings.LOGIN_REDIRECT_URL)
|
294 | 351 | self.assertEqual(force_text(new_user.id), client.session[SESSION_KEY])
|
295 | 352 |
|
| 353 | + def test_assertion_consumer_service_already_logged_in_allowed(self): |
| 354 | + self.client.force_login(User.objects.create(username='user', password='pass')) |
| 355 | + |
| 356 | + settings.SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN = True |
| 357 | + |
| 358 | + came_from = '/dummy-url/' |
| 359 | + response = self.client.get(reverse('saml2_login') + f'?next={came_from}') |
| 360 | + self.assertEqual(response.status_code, 302) |
| 361 | + url = urlparse(response['Location']) |
| 362 | + self.assertEqual(url.path, came_from) |
| 363 | + |
| 364 | + def test_assertion_consumer_service_already_logged_in_error(self): |
| 365 | + self.client.force_login(User.objects.create(username='user', password='pass')) |
| 366 | + |
| 367 | + settings.SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN = False |
| 368 | + |
| 369 | + came_from = '/dummy-url/' |
| 370 | + response = self.client.get(reverse('saml2_login') + f'?next={came_from}') |
| 371 | + self.assertEqual(response.status_code, 200) |
| 372 | + self.assertInHTML("<p>You are already logged in and you are trying to go to the login page again.</p>", response.content.decode()) |
| 373 | + |
296 | 374 | def test_assertion_consumer_service_no_session(self):
|
297 | 375 | settings.SAML_CONFIG = conf.create_conf(
|
298 | 376 | sp_host='sp.example.com',
|
|
0 commit comments