diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bec80a2..ef5cd80 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ I welcome all contributions; issues/bug reports, adding tests, adding or request ## About the project -This package started off on a need-to-have basis for use in the company I work for. I did not spend time to build an all-encompassing feature set; so what we did not use and need was not implemented. Several features since then have been added by contributors with their own needs in mind. Therefore, if something is missing that you need, feel free to ask about it and I'm happy to take a look. +This package started off on a need-to-have basis to use in the company I work for. I did not spend time to build an all-encompassing solution; so what we did not use or need was not implemented. Several features since then have been added by contributors with their own needs in mind. Therefore, if something is missing that you need, feel free to ask about it and I'm happy to take a look. While I created this package due to a need in the company for something like this, I maintain it privately. I am not paid for it nor work on it during 'company time', it is a private side project. My response time will therefore be dependent on my available time and is a best-effort matter. Please keep this in mind and my apologies in advance if a response runs a bit late. We all have lives to lead :) @@ -16,7 +16,7 @@ Some notes for when you want to start adding code to the project. ### Tests -- Unfortunately there are not a lot of formal tests (there's an idea for a contribution ;)) Please make sure that the ones who are present, succeed by running the test suite (it uses `pytest`, instructions are in the [README.rst](README.rst)) +- Instructions are in the [README.rst](README.rst)). Please make sure the tests succeed, they will be run when you create a PR in the repo. Also try to provide some formal testing of new code you are adding. We have spent some time to make sure there is a high test coverage of the code, and I would love to keep it like that. ### Codestyle @@ -25,8 +25,5 @@ Some notes for when you want to start adding code to the project. ```bash pylama --options pytest.ini ``` - in the root of the project. It only outputs violations, so if you don't get any output, you did great. Please ensure pylama succeeds when creating a PR. + in the root of the project. It only outputs violations, so if you don't get any output, you did great. Please ensure pylama succeeds when creating a PR (it will be run automatically via CI). - When you create a PR for functionality or bugfix, don't mix in unrelated formatting changes along with it. - - -The `pytest.ini` file contains sections for both PEP8 and pylama settings, which should be used for this project. \ No newline at end of file diff --git a/README.rst b/README.rst index 3c8b3a5..18889c1 100644 --- a/README.rst +++ b/README.rst @@ -36,8 +36,6 @@ djangosaml2idp djangosaml2idp implements the Identity Provider side of the SAML2 protocol for Django. It builds on top of `PySAML2 `_, and is production-ready. -Package version 0.3.3 was the last Python 2 / Django 1.8-1.11 compatible release. Versions starting from 0.4.0 are for Python 3 and Django 2.x. - Any contributions, feature requests, proposals, ideas ... are welcome! See the `CONTRIBUTING document `_ for some info. Installation @@ -49,7 +47,6 @@ you will need to set the full path to it in the configuration stage. XmlSec is a Now you can install the djangosaml2idp package using pip. This will also install PySAML2 and its dependencies automatically:: - .. code-block:: shell pip install djangosaml2idp @@ -58,7 +55,6 @@ Configuration & Usage The first thing you need to do is add ``djangosaml2idp`` to the list of installed apps:: - .. code-block:: python INSTALLED_APPS = ( 'django.contrib.admin', 'djangosaml2idp', @@ -67,7 +63,6 @@ The first thing you need to do is add ``djangosaml2idp`` to the list of installe Now include ``djangosaml2idp`` in your project by adding it in the url config:: - .. code-block:: python from django.conf.urls import url, include from django.contrib import admin @@ -77,9 +72,10 @@ Now include ``djangosaml2idp`` in your project by adding it in the url config:: ... ] +Run the migrations for the app. + In your Django settings, configure your IdP. Configuration follows the `PySAML2 configuration `_. The IdP from the example project looks like this:: - .. code-block:: python import saml2 from saml2.saml import NAMEID_FORMAT_EMAILADDRESS, NAMEID_FORMAT_UNSPECIFIED from saml2.sigver import get_xmlsec_binary @@ -98,19 +94,21 @@ In your Django settings, configure your IdP. Configuration follows the `PySAML2 'name': 'Django localhost IdP', 'endpoints': { 'single_sign_on_service': [ - ('%s/sso/post' % BASE_URL, saml2.BINDING_HTTP_POST), - ('%s/sso/redirect' % BASE_URL, saml2.BINDING_HTTP_REDIRECT), + ('http://localhost:9000/idp/sso/post/', saml2.BINDING_HTTP_POST), + ('http://localhost:9000/idp/sso/redirect/', saml2.BINDING_HTTP_REDIRECT), + ], + "single_logout_service": [ + ("http://localhost:9000/idp/slo/post/", saml2.BINDING_HTTP_POST), + ("http://localhost:9000/idp/slo/redirect/", saml2.BINDING_HTTP_REDIRECT) ], }, 'name_id_format': [NAMEID_FORMAT_EMAILADDRESS, NAMEID_FORMAT_UNSPECIFIED], 'sign_response': True, 'sign_assertion': True, + 'want_authn_requests_signed': True, }, }, - 'metadata': { - 'local': [os.path.join(BASE_DIR, 'idp', 'saml2_config', 'sp_metadata.xml')], - }, # Signing 'key_file': BASE_DIR + '/certificates/private.key', 'cert_file': BASE_DIR + '/certificates/public.cert', @@ -125,18 +123,12 @@ In your Django settings, configure your IdP. Configuration follows the `PySAML2 Notice the configuration requires a private key and public certificate to be available on the filesystem in order to sign and encrypt messages. -Next the Service Providers need to be added, this is done via the Django admin interface. Add the necessary configuration for each SP. -Upload a copy of the local metadata, or set a remote metadata url. Add an attribute mapping for user attributes to SAML fields in the configuration, as a dict of strings (with double quotes for the strings): +Next the Service Providers and their configuration need to be added, this is done via the Django admin interface. Add an entry for each SP which speaks to thie IdP. +Add a copy of the local metadata xml, or set a remote metadata url. Add an attribute mapping for user attributes to SAML fields or leave the default mapping which will be prefilled. - attribute_mapping = {'email': 'email', 'first_name': 'first_name', 'last_name': 'last_name', 'is_staff': 'is_staff', 'is_superuser': 'is_superuser'} - - -Several attributes can be overriden per SP, but will fall back the defaults set in the application settings if not overriden explicitly, and if they haven't been set some sane settings are used. -The resulting configuration of a SP, with merged settings of its own and the defaults, is shown in the admin. - -The last step is configuring metadata. -Download a copy of the IdP's metadata from /idp/metadata (assuming that's how you set up your urls.py). Use it to configure your SPs as required by them. -Obtain a copy of the metadata for each of your SPs, and upload them where you indicated in ``SAML_IDP_CONFIG['metadata]`` +Several attributes can be overriden per SP. If they aren't overridden explicitly, they will use the 'global' settings which can be configured for your Django installation. +If those aren't set, some defaults will be used, as indicated in the admin when you configre a SP. +The resulting configuration of a SP, with merged settings of its own and the instance settings and defaults, is shown in the admin as a summary. Further optional configuration options ====================================== @@ -152,7 +144,10 @@ Use this metadata xml to configure your SP. Place the metadata xml from that SP Without custom setting, users will be identified by the ``USERNAME_FIELD`` property on the user Model you use. By Django defaults this will be the username. You can customize which field is used for the identifier by adding ``SAML_IDP_DJANGO_USERNAME_FIELD`` to your settings with as value the attribute to use on your user instance. -Other settings you can set as defaults to be used if not overriden by an SP are `SAML_AUTHN_SIGN_ALG`, `SAML_AUTHN_DIGEST_ALG`, and `SAML_ENCRYPT_AUTHN_RESPONSE`. +Other settings you can set as defaults to be used if not overriden by an SP are `SAML_AUTHN_SIGN_ALG`, `SAML_AUTHN_DIGEST_ALG`, and `SAML_ENCRYPT_AUTHN_RESPONSE`. They can be set if desired in the django settings, in which case they will be used for all ServiceProviders configuration on this instance if they don't override it. E.g.: + + SAML_AUTHN_SIGN_ALG = saml2.xmldsig.SIG_RSA_SHA256 + SAML_AUTHN_DIGEST_ALG = saml2.xmldsig.DIGEST_SHA256 Customizing error handling ========================== diff --git a/djangosaml2idp/migrations/0001_initial.py b/djangosaml2idp/migrations/0001_initial.py index a083cc9..a34f7c4 100644 --- a/djangosaml2idp/migrations/0001_initial.py +++ b/djangosaml2idp/migrations/0001_initial.py @@ -17,7 +17,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('dt_created', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), ('dt_updated', models.DateTimeField(auto_now=True, null=True, verbose_name='Updated at')), - ('entity_id', models.CharField(max_length=256, unique=True, verbose_name='Entity ID')), + ('entity_id', models.CharField(max_length=255, unique=True, verbose_name='Entity ID')), ('pretty_name', models.CharField(blank=True, help_text='For display purposes, can be empty', max_length=256, verbose_name='Pretty Name')), ('description', models.TextField(blank=True, verbose_name='Description')), ('metadata_expiration_dt', models.DateTimeField(verbose_name='Metadata valid until')), diff --git a/djangosaml2idp/models.py b/djangosaml2idp/models.py index f0354fb..9a690bb 100644 --- a/djangosaml2idp/models.py +++ b/djangosaml2idp/models.py @@ -42,8 +42,8 @@ class ServiceProvider(models.Model): dt_updated = models.DateTimeField(verbose_name='Updated at', auto_now=True, null=True, blank=True) # Identification - entity_id = models.CharField(verbose_name='Entity ID', max_length=256, unique=True) - pretty_name = models.CharField(verbose_name='Pretty Name', blank=True, max_length=256, help_text='For display purposes, can be empty') + entity_id = models.CharField(verbose_name='Entity ID', max_length=255, unique=True) + pretty_name = models.CharField(verbose_name='Pretty Name', blank=True, max_length=255, help_text='For display purposes, can be empty') description = models.TextField(verbose_name='Description', blank=True) # Metadata