Merge lp:~roadmr/canonical-identity-provider/metadata-with-custom-cert into lp:canonical-identity-provider/release

Proposed by Daniel Manrique
Status: Merged
Approved by: Daniel Manrique
Approved revision: no longer in the source branch.
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: lp:~roadmr/canonical-identity-provider/metadata-with-custom-cert
Merge into: lp:canonical-identity-provider/release
Prerequisite: lp:~roadmr/canonical-identity-provider/pass-custom-cert-to-django-saml2-idp
Diff against target: 119 lines (+54/-3)
3 files modified
src/ubuntu_sso_saml/tests/test_views.py (+44/-1)
src/ubuntu_sso_saml/urls.py (+1/-0)
src/ubuntu_sso_saml/views.py (+9/-2)
To merge this branch: bzr merge lp:~roadmr/canonical-identity-provider/metadata-with-custom-cert
Reviewer Review Type Date Requested Status
Ricardo Kirkner (community) Approve
Review via email: mp+334994@code.launchpad.net

Commit message

Add SP-specific metadata view.

This allows URLs such as /+saml/metadata/4. If the SP with id 4 has a
custom certificate, it will be used in the metadata. If not, valid metadata
with the default global cert is shown. If no SP with the given primary key exists,
a 404 is raised.

This avoids having to tell SPs "use this metadata URL but this certificate
because the one in the metadata is bad".

The intended flow would be:

1- create the SPConfig, even if with partial config.
2- Add a custom cert
3- We can now give the SP's support people a metadata link with nice certificate.

Description of the change

Add SP-specific metadata view.

This allows URLs such as /+saml/metadata/4. If the SP with id 4 has a
custom certificate, it will be used in the metadata. If not, valid metadata
with the default global cert is shown. If no SP with the given primary key exists,
a 404 is raised.

This avoids having to tell SPs "use this metadata URL but this certificate
because the one in the metadata is bad".

The intended flow would be:

1- create the SPConfig, even if with partial config.
2- Add a custom cert
3- We can now give the SP's support people a metadata link with nice certificate.

To post a comment you must log in.
Revision history for this message
Ricardo Kirkner (ricardokirkner) wrote :

Good idea and code lgtm, but what about using some other field (eg, acs_url) to identify the SP instead of exposing our internal PK?

Revision history for this message
Daniel Manrique (roadmr) wrote :

I wanted to specifically avoid changing the DB schema, and the PK seemed like the most innocuous, simple field to use. But I agree it's not nice to use it like that.

We could use the acs_url but since it's a url (and they can be quite ugly) I worry about tacking one url as a component to another url.

I'd be happy to implement any of these options:

1- Add a "slug" field so we can have URLs like /metadata/a-nice-provider (restrict to a-z0-9-). It's one more field to worry about, but it'd be stable for each remote.
2- Calculate a slug with those constraints using the existing "display name" field. Avoids adding a new field, but auto-slugs tend to be crappy and if the display name changes, the metadata location will also change.
3- Have a "sp identifier" field or something, which we auto-populate on record creation with a random value. Similar to 1, saves the operator from having to think of a slug value, but could yield ugly URLs (/metadata/a5Ewkhr4)
5- Use the acs_url. We'd probably need to come up with some cleaning/prettyfying.
6- Open to any other ideas

My preference would be option 1, it feels like the cleanest and is not hard to implement.

Revision history for this message
Ricardo Kirkner (ricardokirkner) wrote :

Ok, I think it's not that bad after all (after you explained it it didn't sound that aweful).
Maybe just changing the name of the field would be enough, eg instead of using pk for the variable, let's use sp_id (which is exactly the same really, but isn't forced to be the "primary" key; also, /metadata/pk would suggest it's the primary key for a metadata object).

Revision history for this message
Daniel Manrique (roadmr) wrote :

Done, I changed it in the URL and the view which are the most exposed places, though I needed to keep calling it pk in the places where I hit the database.

Let me know if that works!

Revision history for this message
Ricardo Kirkner (ricardokirkner) wrote :

LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/ubuntu_sso_saml/tests/test_views.py'
2--- src/ubuntu_sso_saml/tests/test_views.py 2017-05-11 15:22:36 +0000
3+++ src/ubuntu_sso_saml/tests/test_views.py 2018-01-05 23:23:10 +0000
4@@ -4,7 +4,11 @@
5 from saml2idp import saml2idp_metadata
6
7 from identityprovider.tests.utils import SSOBaseTestCase
8-from ubuntu_sso_saml.tests.helpers import find_cert_path
9+from ubuntu_sso_saml.models import SAMLConfig
10+from ubuntu_sso_saml.tests.helpers import cert_string_from_file, find_cert_path
11+
12+
13+CUSTOM_TEST_CERTIFICATE = "custom-test-certificate.pem"
14
15
16 class SAMLMetadataViewTestCase(SSOBaseTestCase):
17@@ -25,8 +29,47 @@
18 }
19 saml2idp_metadata.SAML2IDP_CONFIG.update(test_config)
20
21+ custom_cert_filename = find_cert_path(CUSTOM_TEST_CERTIFICATE)
22+ with open(custom_cert_filename) as custom_cert_file:
23+ custom_cert_string = custom_cert_file.read()
24+ self.custom_sp_config = SAMLConfig.objects.create(
25+ acs_url="https://custom.example.com",
26+ certificate=custom_cert_string)
27+ self.default_sp_config = SAMLConfig.objects.create(
28+ acs_url="https://default.example.com")
29+ self.custom_cert_string = cert_string_from_file(custom_cert_filename)
30+
31 def test_metadata_view(self):
32 response = self.client.get(reverse('saml-metadata'))
33 self.assertEqual(response['content-type'], 'application/xml')
34 self.assertContains(response, reverse('logout'))
35 self.assertContains(response, reverse('login_begin'))
36+
37+ def test_sp_specific_metadata_view_nonexistent_sp(self):
38+ # Can never have a record with sp_id of 0
39+ # (relying on the fact that the sp_id is actually the database ID for
40+ # the record).
41+ response = self.client.get(
42+ reverse('saml-metadata', args=(0,)))
43+ self.assertEqual(response.status_code, 404)
44+
45+ def test_sp_specific_metadata_view_default_cert(self):
46+ cert_filename = saml2idp_metadata.SAML2IDP_CONFIG['certificate_file']
47+
48+ response = self.client.get(
49+ reverse('saml-metadata', args=(self.default_sp_config.pk,)))
50+ self.assertEqual(response['content-type'], 'application/xml')
51+ self.assertContains(response, reverse('logout'))
52+ self.assertContains(response, reverse('login_begin'))
53+
54+ self.assertContains(
55+ response, cert_string_from_file(cert_filename))
56+
57+ def test_sp_specific_metadata_view_custom_cert(self):
58+ response = self.client.get(
59+ reverse('saml-metadata', args=(self.custom_sp_config.pk,)))
60+ self.assertEqual(response['content-type'], 'application/xml')
61+ self.assertContains(response, reverse('logout'))
62+ self.assertContains(response, reverse('login_begin'))
63+
64+ self.assertContains(response, self.custom_cert_string)
65
66=== modified file 'src/ubuntu_sso_saml/urls.py'
67--- src/ubuntu_sso_saml/urls.py 2017-05-12 15:39:10 +0000
68+++ src/ubuntu_sso_saml/urls.py 2018-01-05 23:23:10 +0000
69@@ -9,6 +9,7 @@
70 url(r'^$', 'saml_begin', name='login_begin'),
71 url(r'^/process$', 'saml_process', name='login_process'),
72 url(r'^/metadata$', 'saml_metadata', name='saml-metadata'),
73+ url(r'^/metadata/(?P<sp_id>\d+)$', 'saml_metadata', name='saml-metadata'),
74 )
75
76 # Automagically detect deep link URLs from settings:
77
78=== modified file 'src/ubuntu_sso_saml/views.py'
79--- src/ubuntu_sso_saml/views.py 2017-05-12 15:39:10 +0000
80+++ src/ubuntu_sso_saml/views.py 2018-01-05 23:23:10 +0000
81@@ -4,7 +4,7 @@
82 from urlparse import urlparse, urlunparse
83
84 from django.core.urlresolvers import reverse
85-from django.shortcuts import render, render_to_response
86+from django.shortcuts import get_object_or_404, render, render_to_response
87 from django.template import RequestContext
88 from django.utils.datastructures import MultiValueDictKeyError
89 from django.views.decorators.csrf import csrf_exempt
90@@ -20,6 +20,7 @@
91 # XXX: we should not import webui here
92 from webui.decorators import sso_login_required
93
94+from ubuntu_sso_saml.models import SAMLConfig
95 from ubuntu_sso_saml.processors import InfoProcessor
96 from ubuntu_sso_saml.utils import (
97 get_config_and_link_for_resource,
98@@ -99,7 +100,7 @@
99
100
101 # backported descriptor view (from saml2idp.views) as it needed fixing
102-def saml_metadata(request):
103+def saml_metadata(request, sp_id=None):
104 """
105 Replies with the XML Metadata IDSSODescriptor.
106 """
107@@ -108,6 +109,12 @@
108 slo_url = request.build_absolute_uri(reverse('logout'))
109 sso_url = request.build_absolute_uri(reverse('login_begin'))
110 pubkey = xml_signing.load_cert_data(config['certificate_file'])
111+ if sp_id:
112+ sp = get_object_or_404(SAMLConfig, pk=sp_id)
113+ if sp.certificate:
114+ pubkey = xml_signing.load_cert_data_from_string(
115+ str(sp.certificate))
116+
117 tv = {
118 'entity_id': entity_id,
119 'cert_public_key': pubkey,