diff --git a/passbook/saml_idp/models.py b/passbook/saml_idp/models.py index 55313e081..c10932306 100644 --- a/passbook/saml_idp/models.py +++ b/passbook/saml_idp/models.py @@ -42,7 +42,7 @@ class SAMLProvider(Provider): """Get link to download XML metadata for admin interface""" try: # pylint: disable=no-member - return reverse('passbook_saml_idp:metadata_xml', + return reverse('passbook_saml_idp:saml-metadata', kwargs={'application': self.application.slug}) except Provider.application.RelatedObjectDoesNotExist: return None diff --git a/passbook/saml_idp/templates/saml/idp/settings.html b/passbook/saml_idp/templates/saml/idp/settings.html index 812510fff..3e09645f5 100644 --- a/passbook/saml_idp/templates/saml/idp/settings.html +++ b/passbook/saml_idp/templates/saml/idp/settings.html @@ -39,7 +39,7 @@ diff --git a/passbook/saml_idp/urls.py b/passbook/saml_idp/urls.py index 81f99ca3a..33a67a66c 100644 --- a/passbook/saml_idp/urls.py +++ b/passbook/saml_idp/urls.py @@ -4,13 +4,14 @@ from django.urls import path from passbook.saml_idp import views urlpatterns = [ - path('login//', - views.LoginBeginView.as_view(), name="saml_login_begin"), - path('login//initiate/', - views.InitiateLoginView.as_view(), name="saml_login_init"), - path('login//process/', - views.LoginProcessView.as_view(), name='saml_login_process'), - path('logout/', views.LogoutView.as_view(), name="saml_logout"), - path('metadata//', - views.DescriptorDownloadView.as_view(), name='metadata_xml'), + path('/login/', + views.LoginBeginView.as_view(), name="saml-login"), + path('/login/initiate/', + views.InitiateLoginView.as_view(), name="saml-login-initiate"), + path('/login/process/', + views.LoginProcessView.as_view(), name='saml-login-process'), + path('/logout/', views.LogoutView.as_view(), name="saml-logout"), + path('/logout/slo/', views.SLOLogout.as_view(), name="saml-logout-slo"), + path('/metadata/', + views.DescriptorDownloadView.as_view(), name='saml-metadata'), ] diff --git a/passbook/saml_idp/views.py b/passbook/saml_idp/views.py index dae9a412a..244755d28 100644 --- a/passbook/saml_idp/views.py +++ b/passbook/saml_idp/views.py @@ -14,6 +14,7 @@ from django.views.decorators.csrf import csrf_exempt from signxml.util import strip_pem_header from passbook.core.models import Application +from passbook.core.policies import PolicyEngine from passbook.lib.config import CONFIG from passbook.lib.mixins import CSRFExemptMixin from passbook.lib.utils.template import render_to_string @@ -75,7 +76,7 @@ class LoginBeginView(LoginRequiredMixin, View): return HttpResponseBadRequest('the SAML request payload is missing') request.session['RelayState'] = source.get('RelayState', '') - return redirect(reverse('passbook_saml_idp:saml_login_process', kwargs={ + return redirect(reverse('passbook_saml_idp:saml-login-process', kwargs={ 'application': application })) @@ -94,17 +95,22 @@ class RedirectToSPView(LoginRequiredMixin, View): }) + class LoginProcessView(ProviderMixin, LoginRequiredMixin, View): """Processor-based login continuation. Presents a SAML 2.0 Assertion for POSTing back to the Service Provider.""" + def _has_access(self): + """Check if user has access to application""" + policy_engine = PolicyEngine(self.provider.application.policies.all()) + policy_engine.for_user(self.request.user) + return policy_engine.result + def get(self, request, application): """Handle get request, i.e. render form""" LOGGER.debug("Request: %s", request) # Check if user has access - access = True - # TODO: Check access here - if self.provider.application.skip_authorization and access: + if self.provider.application.skip_authorization and self._has_access(): ctx = self.provider.processor.generate_response() # TODO: AuditLog Skipped Authz return RedirectToSPView.as_view()( @@ -122,9 +128,7 @@ class LoginProcessView(ProviderMixin, LoginRequiredMixin, View): """Handle post request, return back to ACS""" LOGGER.debug("Request: %s", request) # Check if user has access - access = True - # TODO: Check access here - if request.POST.get('ACSUrl', None) and access: + if request.POST.get('ACSUrl', None) and self._has_access(): # User accepted request # TODO: AuditLog accepted return RedirectToSPView.as_view()( @@ -144,7 +148,7 @@ class LogoutView(CSRFExemptMixin, LoginRequiredMixin, View): returns a standard logged-out page. (SalesForce and others use this method, though it's technically not SAML 2.0).""" - def get(self, request): + def get(self, request, application): """Perform logout""" logout(request) @@ -164,11 +168,10 @@ class SLOLogout(CSRFExemptMixin, LoginRequiredMixin, View): """Receives a SAML 2.0 LogoutRequest from a Service Provider, logs out the user and returns a standard logged-out page.""" - def post(self, request): + def post(self, request, application): """Perform logout""" request.session['SAMLRequest'] = request.POST['SAMLRequest'] # TODO: Parse SAML LogoutRequest from POST data, similar to login_process(). - # TODO: Add a URL dispatch for this view. # TODO: Modify the base processor to handle logouts? # TODO: Combine this with login_process(), since they are so very similar? # TODO: Format a LogoutResponse and return it to the browser. @@ -183,8 +186,8 @@ class DescriptorDownloadView(ProviderMixin, View): def get(self, request, application): """Replies with the XML Metadata IDSSODescriptor.""" entity_id = CONFIG.y('saml_idp.issuer') - slo_url = request.build_absolute_uri(reverse('passbook_saml_idp:saml_logout')) - sso_url = request.build_absolute_uri(reverse('passbook_saml_idp:saml_login_begin', kwargs={ + slo_url = request.build_absolute_uri(reverse('passbook_saml_idp:saml-logout')) + sso_url = request.build_absolute_uri(reverse('passbook_saml_idp:saml-login', kwargs={ 'application': application })) pubkey = strip_pem_header(self.provider.signing_cert.replace('\r', '')).replace('\n', '')