admin: only add link if function returns not None

This commit is contained in:
Jens Langhammer 2018-12-26 21:55:14 +01:00
parent 4d5f688a44
commit 0c9a00acbe
No known key found for this signature in database
GPG Key ID: BEBC05297D92821B
3 changed files with 88 additions and 77 deletions

View File

@ -18,9 +18,14 @@ def get_links(model_instance):
LOGGER.warning("Model %s is not instance of Model", model_instance) LOGGER.warning("Model %s is not instance of Model", model_instance)
return links return links
for name, method in inspect.getmembers(model_instance, predicate=inspect.ismethod): try:
if name.startswith(prefix): for name, method in inspect.getmembers(model_instance, predicate=inspect.ismethod):
human_name = name.replace(prefix, '').replace('_', ' ').capitalize() if name.startswith(prefix):
links[human_name] = method() human_name = name.replace(prefix, '').replace('_', ' ').capitalize()
link = method()
if link:
links[human_name] = link
except NotImplementedError:
pass
return links return links

View File

@ -2,3 +2,4 @@ beautifulsoup4>=4.6.0
lxml>=3.8.0 lxml>=3.8.0
signxml signxml
defusedxml defusedxml
PyCryptodome

View File

@ -1,6 +1,7 @@
"""passbook SAML IDP Views""" """passbook SAML IDP Views"""
from logging import getLogger from logging import getLogger
from django.conf import settings
from django.contrib.auth import logout from django.contrib.auth import logout
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -9,33 +10,34 @@ from django.http import HttpResponse, HttpResponseBadRequest
from django.shortcuts import get_object_or_404, redirect, render, reverse from django.shortcuts import get_object_or_404, redirect, render, reverse
from django.utils.datastructures import MultiValueDictKeyError from django.utils.datastructures import MultiValueDictKeyError
from django.views import View from django.views import View
from saml2 import BINDING_HTTP_POST
from saml2.authn_context import PASSWORD, AuthnBroker, authn_context_class_ref
from saml2.config import IdPConfig
from saml2.ident import NameID
from saml2.metadata import entity_descriptor
from saml2.s_utils import UnknownPrincipal, UnsupportedBinding
from saml2.saml import NAMEID_FORMAT_EMAILADDRESS, NAMEID_FORMAT_UNSPECIFIED
from saml2.server import Server
from signxml.util import strip_pem_header
from passbook.core.models import Application
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
# from django.utils.html import escape
# from django.utils.translation import ugettext as _
from passbook.lib.mixins import CSRFExemptMixin from passbook.lib.mixins import CSRFExemptMixin
# from passbook.core.models import Event, Setting, UserAcquirableRelationship
from passbook.lib.utils.template import render_to_string from passbook.lib.utils.template import render_to_string
# from passbook.core.views.common import ErrorResponseView from passbook.saml_idp import exceptions
# from passbook.core.views.settings import GenericSettingView
from passbook.saml_idp import exceptions, registry
from passbook.saml_idp.models import SAMLProvider from passbook.saml_idp.models import SAMLProvider
# from OpenSSL.crypto import FILETYPE_PEM
# from OpenSSL.crypto import Error as CryptoError
# from OpenSSL.crypto import load_certificate
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
URL_VALIDATOR = URLValidator(schemes=('http', 'https')) URL_VALIDATOR = URLValidator(schemes=('http', 'https'))
def _generate_response(request, processor, remote): def _generate_response(request, provider: SAMLProvider):
"""Generate a SAML response using processor and return it in the proper Django """Generate a SAML response using processor_instance and return it in the proper Django
response.""" response."""
try: try:
ctx = processor.generate_response() ctx = provider.processor.generate_response()
ctx['remote'] = remote ctx['remote'] = provider
ctx['is_login'] = True
except exceptions.UserNotAuthorized: except exceptions.UserNotAuthorized:
return render(request, 'saml/idp/invalid_user.html') return render(request, 'saml/idp/invalid_user.html')
@ -47,11 +49,23 @@ def render_xml(request, template, ctx):
return render(request, template, context=ctx, content_type="application/xml") return render(request, template, context=ctx, content_type="application/xml")
class ProviderMixin:
_provider = None
@property
def provider(self):
if not self._provider:
application = get_object_or_404(Application, slug=self.kwargs['application'])
self._provider = get_object_or_404(SAMLProvider, pk=application.provider_id)
return self._provider
class LoginBeginView(CSRFExemptMixin, View): class LoginBeginView(CSRFExemptMixin, View):
"""Receives a SAML 2.0 AuthnRequest from a Service Provider and """Receives a SAML 2.0 AuthnRequest from a Service Provider and
stores it in the session prior to enforcing login.""" stores it in the session prior to enforcing login."""
def dispatch(self, request): def dispatch(self, request, application):
if request.method == 'POST': if request.method == 'POST':
source = request.POST source = request.POST
else: else:
@ -64,7 +78,9 @@ class LoginBeginView(CSRFExemptMixin, View):
return HttpResponseBadRequest('the SAML request payload is missing') return HttpResponseBadRequest('the SAML request payload is missing')
request.session['RelayState'] = source.get('RelayState', '') request.session['RelayState'] = source.get('RelayState', '')
return redirect(reverse('passbook_saml_idp:saml_login_process')) return redirect(reverse('passbook_saml_idp:saml_login_process'), kwargs={
'application': application
})
class RedirectToSPView(View): class RedirectToSPView(View):
@ -81,35 +97,18 @@ class RedirectToSPView(View):
}) })
class LoginProcessView(View): class LoginProcessView(ProviderMixin, View):
"""Processor-based login continuation. """Processor-based login continuation.
Presents a SAML 2.0 Assertion for POSTing back to the Service Provider.""" Presents a SAML 2.0 Assertion for POSTing back to the Service Provider."""
def dispatch(self, request): def dispatch(self, request, application):
LOGGER.debug("Request: %s", request) LOGGER.debug("Request: %s", request)
proc, provider = registry.find_processor(request)
# Check if user has access # Check if user has access
access = True access = True
# if provider.productextensionsaml2_set.exists() and \ # TODO: Check access here
# provider.productextensionsaml2_set.first().product_set.exists(): if self.provider.skip_authorization and access:
# # Only check if there is a connection from OAuth2 Application to product ctx = self.provider.processor.generate_response()
# product = provider.productextensionsaml2_set.first().product_set.first() # TODO: AuditLog Skipped Authz
# relationship = UserAcquirableRelationship.objects.
# filter(user=request.user, model=product)
# # Product is invitation_only = True and no relation with user exists
# if product.invitation_only and not relationship.exists():
# access = False
# Check if we should just autosubmit
if provider.skip_authorization and access:
# full_res = _generate_response(request, proc, provider)
ctx = proc.generate_response()
# User accepted request
# Event.create(
# user=request.user,
# message=_('You authenticated %s (via SAML) (skipped Authz)' % provider.name),
# request=request,
# current=False,
# hidden=True)
return RedirectToSPView.as_view()( return RedirectToSPView.as_view()(
request=request, request=request,
acs_url=ctx['acs_url'], acs_url=ctx['acs_url'],
@ -117,27 +116,17 @@ class LoginProcessView(View):
relay_state=ctx['relay_state']) relay_state=ctx['relay_state'])
if request.method == 'POST' and request.POST.get('ACSUrl', None) and access: if request.method == 'POST' and request.POST.get('ACSUrl', None) and access:
# User accepted request # User accepted request
# Event.create( # TODO: AuditLog accepted
# user=request.user,
# message=_('You authenticated %s (via SAML)' % provider.name),
# request=request,
# current=False,
# hidden=True)
return RedirectToSPView.as_view()( return RedirectToSPView.as_view()(
request=request, request=request,
acs_url=request.POST.get('ACSUrl'), acs_url=request.POST.get('ACSUrl'),
saml_response=request.POST.get('SAMLResponse'), saml_response=request.POST.get('SAMLResponse'),
relay_state=request.POST.get('RelayState')) relay_state=request.POST.get('RelayState'))
try: try:
full_res = _generate_response(request, proc, provider) full_res = _generate_response(request, provider)
# if not access:
# LOGGER.warning("User '%s' has no invitation to '%s'", request.user, product)
# messages.error(request, "You have no access to '%s'" % product.name)
# raise Http404
return full_res return full_res
except exceptions.CannotHandleAssertion as exc: except exceptions.CannotHandleAssertion as exc:
LOGGER.debug(exc) LOGGER.debug(exc)
# return ErrorResponseView.as_view()(request, str(exc))
class LogoutView(CSRFExemptMixin, View): class LogoutView(CSRFExemptMixin, View):
@ -177,17 +166,34 @@ class SLOLogout(CSRFExemptMixin, LoginRequiredMixin, View):
logout(request) logout(request)
return render(request, 'saml/idp/logged_out.html') return render(request, 'saml/idp/logged_out.html')
class IdPMixin(ProviderMixin):
class DescriptorDownloadView(View): provider = None
def dispatch(self, request, application):
def get_identity(self, provider, user):
""" Create Identity dict (using SP-specific mapping)
"""
sp_mapping = {'username': 'username'}
# return provider.processor.create_identity(user, sp_mapping)
return {
out_attr: getattr(user, user_attr)
for user_attr, out_attr in sp_mapping.items()
if hasattr(user, user_attr)
}
class DescriptorDownloadView(ProviderMixin, View):
"""Replies with the XML Metadata IDSSODescriptor.""" """Replies with the XML Metadata IDSSODescriptor."""
def get(self, request, application_id): def get(self, request, application):
"""Replies with the XML Metadata IDSSODescriptor.""" """Replies with the XML Metadata IDSSODescriptor."""
application = get_object_or_404(SAMLProvider, pk=application_id) super().dispatch(request, application)
entity_id = CONFIG.y('saml_idp.issuer') entity_id = CONFIG.y('saml_idp.issuer')
slo_url = request.build_absolute_uri(reverse('passbook_saml_idp:saml_logout')) 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')) sso_url = request.build_absolute_uri(reverse('passbook_saml_idp:saml_login_begin'))
pubkey = application.signing_cert pubkey = strip_pem_header(self.provider.signing_cert.replace('\r', '')).replace('\n', '')
ctx = { ctx = {
'entity_id': entity_id, 'entity_id': entity_id,
'cert_public_key': pubkey, 'cert_public_key': pubkey,
@ -196,25 +202,24 @@ class DescriptorDownloadView(View):
} }
metadata = render_to_string('saml/xml/metadata.xml', ctx) metadata = render_to_string('saml/xml/metadata.xml', ctx)
response = HttpResponse(metadata, content_type='application/xml') response = HttpResponse(metadata, content_type='application/xml')
response['Content-Disposition'] = 'attachment; filename="passbook_metadata.xml' response['Content-Disposition'] = ('attachment; filename="'
'%s_passbook_meta.xml"' % self.provider.name)
return response return response
# class IDPSettingsView(GenericSettingView): class LoginInitView(IdPMixin, LoginRequiredMixin, View):
# """IDP Settings"""
# form = IDPSettingsForm def dispatch(self, request, application):
# template_name = 'saml/idp/settings.html' """Initiates an IdP-initiated link to a simple SP resource/target URL."""
super().dispatch(request, application)
# def dispatch(self, request, *args, **kwargs): # # linkdict = dict(metadata.get_links(sp_config))
# self.extra_data['metadata'] = escape(descriptor(request).content.decode('utf-8')) # # pattern = linkdict[resource]
# # is_simple_link = ('/' not in resource)
# # Show the certificate fingerprint # # if is_simple_link:
# sha1_fingerprint = _('<failed to parse certificate>') # # simple_target = kwargs['target']
# try: # # url = pattern % simple_target
# cert = load_certificate(FILETYPE_PEM, CONFIG.y('saml_idp.certificate')) # # else:
# sha1_fingerprint = cert.digest("sha1") # # url = pattern % kwargs
# except CryptoError: # provider.processor.init_deep_link(request, 'deep url')
# pass # return _generate_response(request, provider)
# self.extra_data['fingerprint'] = sha1_fingerprint
# return super().dispatch(request, *args, **kwargs)