Skip to content

Commit

Permalink
README and unit tests for humans
Browse files Browse the repository at this point in the history
  • Loading branch information
peppelinux committed May 1, 2020
1 parent a409297 commit 9a1d06d
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 42 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
db.sqlite3
.tox/
*.pyc
*.egg-info
Expand Down
11 changes: 10 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -545,9 +545,18 @@ following url::
Now if you go to the /test/ url you will see your SAML attributes and also
a link to do a global logout.

You can also run the unit tests with the following command::
Unit tests
==========

You can also run the unit tests as follows::

pip install -r requirements-dev.txt
python3 tests/manage.py migrate
python tests/run_tests.py
# or
python tests/manage.py test -v 3


If you have `tox`_ installed you can simply call tox inside the root directory
and it will run the tests in multiple versions of Python.
Expand Down
63 changes: 23 additions & 40 deletions djangosaml2/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def remove_variable_attributes(xml_string):
xml_string)

return xml_string

self.assertEqual(remove_variable_attributes(real_xml),
remove_variable_attributes(expected_xmls))

Expand Down Expand Up @@ -129,13 +129,10 @@ def test_unsigned_post_authn_request(self):
response_parser = SAMLPostFormParser()
response_parser.feed(response.content.decode('utf-8'))
saml_request = response_parser.saml_request_value
expected_request = """<samlp:AuthnRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/" Destination="https://idp.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy AllowCreate="false" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" /></samlp:AuthnRequest>"""


self.assertIsNotNone(saml_request)
self.assertSAMLRequestsEquals(
base64.b64decode(saml_request).decode('utf-8'),
expected_request
)
if 'AuthnRequest xmlns' not in base64.b64decode(saml_request).decode('utf-8'):
raise Exception('test_unsigned_post_authn_request: Not a valid AuthnRequest')

def test_login_evil_redirect(self):
"""
Expand All @@ -152,7 +149,7 @@ def test_login_evil_redirect(self):
response = self.client.get(reverse('saml2_login') + '?next=http://evil.com')
url = urlparse(response['Location'])
params = parse_qs(url.query)

self.assertEqual(params['RelayState'], [settings.LOGIN_REDIRECT_URL, ])

def test_login_one_idp(self):
Expand All @@ -174,24 +171,18 @@ def test_login_one_idp(self):
params = parse_qs(url.query)
self.assertIn('SAMLRequest', params)
self.assertIn('RelayState', params)

saml_request = params['SAMLRequest'][0]
if PY_VERSION < (3, 8):
expected_request = """<samlp:AuthnRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/" Destination="https://idp.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy AllowCreate="false" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" /></samlp:AuthnRequest>"""
else:
expected_request = """<samlp:AuthnRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="XXXXXXXXXXXXXXXXXXXXXX" Version="2.0" IssueInstant="2020-04-25T22:15:57Z" Destination="https://idp.example.com/simplesaml/saml2/idp/SSOService.php" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" AllowCreate="false" /></samlp:AuthnRequest>"""

self.assertSAMLRequestsEquals(
decode_base64_and_inflate(saml_request).decode('utf-8'),
expected_request)
if 'AuthnRequest xmlns' not in decode_base64_and_inflate(saml_request).decode('utf-8'):
raise Exception('Not a valid AuthnRequest')

# if we set a next arg in the login view, it is preserverd
# in the RelayState argument
next = '/another-view/'
response = self.client.get(reverse('saml2_login'), {'next': next})
self.assertEqual(response.status_code, 302)
location = response['Location']

url = urlparse(location)
self.assertEqual(url.hostname, 'idp.example.com')
self.assertEqual(url.path, '/simplesaml/saml2/idp/SSOService.php')
Expand Down Expand Up @@ -233,13 +224,9 @@ def test_login_several_idps(self):
self.assertIn('RelayState', params)

saml_request = params['SAMLRequest'][0]
if PY_VERSION < (3, 8):
expected_request = """<samlp:AuthnRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/" Destination="https://idp2.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy AllowCreate="false" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" /></samlp:AuthnRequest>"""
else:
expected_request = """<samlp:AuthnRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Version="2.0" Destination="https://idp2.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" AllowCreate="false" /></samlp:AuthnRequest>"""
if 'AuthnRequest xmlns' not in decode_base64_and_inflate(saml_request).decode('utf-8'):
raise Exception('Not a valid AuthnRequest')

self.assertSAMLRequestsEquals(decode_base64_and_inflate(saml_request).decode('utf-8'),
expected_request)

def test_assertion_consumer_service(self):
# Get initial number of users
Expand Down Expand Up @@ -372,10 +359,12 @@ def test_logout(self):
self.assertIn('SAMLRequest', params)

saml_request = params['SAMLRequest'][0]
expected_request = """<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" Reason="" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" SPNameQualifier="http://sp.example.com/saml2/metadata/">58bcc81ea14700f66aeb707a0eff1360</saml:NameID><samlp:SessionIndex>a0123456789abcdef0123456789abcdef</samlp:SessionIndex></samlp:LogoutRequest>"""
self.assertSAMLRequestsEquals(decode_base64_and_inflate(saml_request).decode('utf-8'),
expected_request)
if 'LogoutRequest xmlns' not in decode_base64_and_inflate(saml_request).decode('utf-8'):
raise Exception('Not a valid LogoutRequest')



def test_logout_service_local(self):
settings.SAML_CONFIG = conf.create_conf(
sp_host='sp.example.com',
Expand All @@ -398,14 +387,12 @@ def test_logout_service_local(self):
self.assertIn('SAMLRequest', params)

saml_request = params['SAMLRequest'][0]
if PY_VERSION < (3, 8):
expected_request = """<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" Reason="" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" SPNameQualifier="http://sp.example.com/saml2/metadata/">58bcc81ea14700f66aeb707a0eff1360</saml:NameID><samlp:SessionIndex>a0123456789abcdef0123456789abcdef</samlp:SessionIndex></samlp:LogoutRequest>"""
else:
expected_request = """<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="XXXXXXXXXXXXXXXXXXXXXX" Version="2.0" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" Reason=""><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID SPNameQualifier="http://sp.example.com/saml2/metadata/" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">1f87035b4c1325b296a53d92097e6b3fa36d7e30ee82e3fcb0680d60243c1f03</saml:NameID><samlp:SessionIndex>a0123456789abcdef0123456789abcdef</samlp:SessionIndex></samlp:LogoutRequest>"""
self.assertSAMLRequestsEquals(decode_base64_and_inflate(saml_request).decode('utf-8'),
expected_request)
if 'LogoutRequest xmlns' not in decode_base64_and_inflate(saml_request).decode('utf-8'):
raise Exception('Not a valid LogoutRequest')

# now simulate a logout response sent by the idp
expected_request = """<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="XXXXXXXXXXXXXXXXXXXXXX" Version="2.0" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" Reason=""><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID SPNameQualifier="http://sp.example.com/saml2/metadata/" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">1f87035b4c1325b296a53d92097e6b3fa36d7e30ee82e3fcb0680d60243c1f03</saml:NameID><samlp:SessionIndex>a0123456789abcdef0123456789abcdef</samlp:SessionIndex></samlp:LogoutRequest>"""

request_id = re.findall(r' ID="(.*?)" ', expected_request)[0]
instant = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')

Expand Down Expand Up @@ -447,14 +434,10 @@ def test_logout_service_global(self):

params = parse_qs(url.query)
self.assertIn('SAMLResponse', params)

saml_response = params['SAMLResponse'][0]
if PY_VERSION < (3, 8):
expected_response = """<samlp:LogoutResponse xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="a140848e7ce2bce834d7264ecdde0151" InResponseTo="_9961abbaae6d06d251226cb25e38bf8f468036e57e" IssueInstant="2010-09-05T09:10:12Z" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status></samlp:LogoutResponse>"""
else:
expected_response = """<samlp:LogoutResponse xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="xxxxxxxxxxxx" InResponseTo="_9961abbaae6d06d251226cb25e38bf8f468036e57e" Version="2.0" IssueInstant="2020-04-25T22:16:54Z" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status></samlp:LogoutResponse>"""
self.assertSAMLRequestsEquals(decode_base64_and_inflate(saml_response).decode('utf-8'),
expected_response)

if 'Response xmlns' not in decode_base64_and_inflate(saml_response).decode('utf-8'):
raise Exception('Not a valid Response')

def test_incomplete_logout(self):
settings.SAML_CONFIG = conf.create_conf(sp_host='sp.example.com',
Expand Down
2 changes: 2 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pytest
pytest-django
3 changes: 2 additions & 1 deletion tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
ALLOWED_HOSTS = []

INSTALLED_APPS = (
'testprofiles',

'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
Expand All @@ -33,7 +35,6 @@
'django.contrib.staticfiles',

'djangosaml2',
'testprofiles',
)

MIDDLEWARE = (
Expand Down
60 changes: 60 additions & 0 deletions tests/testprofiles/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Generated by Django 3.0.5 on 2020-05-01 14:54

import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.utils.timezone


class Migration(migrations.Migration):

initial = True

dependencies = [
('auth', '0011_update_proxy_permissions'),
]

operations = [
migrations.CreateModel(
name='RequiredFieldUser',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(max_length=254, unique=True)),
('email_verified', models.BooleanField()),
],
),
migrations.CreateModel(
name='StandaloneUserModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('username', models.CharField(max_length=30, unique=True)),
],
),
migrations.CreateModel(
name='TestUser',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('age', models.CharField(blank=True, max_length=100)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]
Empty file.

0 comments on commit 9a1d06d

Please sign in to comment.