diff --git a/mellon/utils.py b/mellon/utils.py index ad1117f..6ae5fb7 100644 --- a/mellon/utils.py +++ b/mellon/utils.py @@ -35,6 +35,10 @@ from . import app_settings logger = logging.getLogger(__name__) +class CreateServerError(Exception): + pass + + def create_metadata(request): entity_id = reverse('mellon_metadata') login_url = reverse(app_settings.LOGIN_URL) @@ -84,6 +88,8 @@ def create_server(request): server = lasso.Server.newFromBuffers( metadata, private_key_content=private_key, private_key_password=private_key_password ) + if not server: + raise CreateServerError if app_settings.SIGNATURE_METHOD: symbol_name = 'SIGNATURE_METHOD_' + app_settings.SIGNATURE_METHOD.replace('-', '_').upper() if hasattr(lasso, symbol_name): diff --git a/mellon/views.py b/mellon/views.py index 7f27b37..8920e22 100644 --- a/mellon/views.py +++ b/mellon/views.py @@ -132,6 +132,45 @@ class ProfileMixin: args.append(idp_message) self.log.warning(*args) + def dispatch(self, request, *args, **kwargs): + try: + return super().dispatch(request, *args, **kwargs) + except utils.CreateServerError: + return self.failure( + request, + reason=_( + 'Unable to initialize a SAML server object, the private key ' + 'is maybe invalid or unreadable, please check its access ' + 'rights and content.' + ), + ) + + def failure(self, request, reason='', status_codes=()): + '''show error message to user after a login failure''' + login = self.profile + idp = utils.get_idp(login and login.remoteProviderId) + if not idp and login: + self.log.warning('entity id %r is unknown', login.remoteProviderId) + return HttpResponseBadRequest('entity id %r is unknown' % login.remoteProviderId) + error_url = utils.get_setting(idp, 'ERROR_URL') + error_redirect_after_timeout = utils.get_setting(idp, 'ERROR_REDIRECT_AFTER_TIMEOUT') + if error_url: + error_url = resolve_url(error_url) + next_url = error_url or self.get_next_url(default=resolve_url(settings.LOGIN_REDIRECT_URL)) + return self.render( + request, + 'mellon/authentication_failed.html', + { + 'debug': settings.DEBUG, + 'reason': reason, + 'status_codes': status_codes, + 'issuer': login and login.remoteProviderId, + 'next_url': next_url, + 'relaystate': login and login.msgRelayState, + 'error_redirect_after_timeout': error_redirect_after_timeout, + }, + ) + class LoginView(ProfileMixin, LogMixin, View): def dispatch(self, request, *args, **kwargs): @@ -197,33 +236,7 @@ class LoginView(ProfileMixin, LogMixin, View): if 'RelayState' in request.POST and utils.is_nonnull(request.POST['RelayState']): login.msgRelayState = request.POST['RelayState'] return self.sso_success(request, login) - return self.sso_failure(request, reason=idp_message, status_codes=status_codes) - - def sso_failure(self, request, reason='', status_codes=()): - '''show error message to user after a login failure''' - login = self.profile - idp = utils.get_idp(login.remoteProviderId) - if not idp: - self.log.warning('entity id %r is unknown', login.remoteProviderId) - return HttpResponseBadRequest('entity id %r is unknown' % login.remoteProviderId) - error_url = utils.get_setting(idp, 'ERROR_URL') - error_redirect_after_timeout = utils.get_setting(idp, 'ERROR_REDIRECT_AFTER_TIMEOUT') - if error_url: - error_url = resolve_url(error_url) - next_url = error_url or self.get_next_url(default=resolve_url(settings.LOGIN_REDIRECT_URL)) - return self.render( - request, - 'mellon/authentication_failed.html', - { - 'debug': settings.DEBUG, - 'reason': reason, - 'status_codes': status_codes, - 'issuer': login.remoteProviderId, - 'next_url': next_url, - 'relaystate': login.msgRelayState, - 'error_redirect_after_timeout': error_redirect_after_timeout, - }, - ) + return self.failure(request, reason=idp_message, status_codes=status_codes) def get_attribute_value(self, attribute, attribute_value): # check attribute_value contains only text @@ -340,7 +353,7 @@ class LoginView(ProfileMixin, LogMixin, View): Use a cookie to prevent looping forever. """ if RETRY_LOGIN_COOKIE in self.request.COOKIES: - response = self.sso_failure( + response = self.failure( self.request, reason=_('There were too many redirections with the identity provider.') ) response.delete_cookie(RETRY_LOGIN_COOKIE) @@ -392,7 +405,7 @@ class LoginView(ProfileMixin, LogMixin, View): ) except RequestException as e: self.log.warning('unable to reach %r: %s', login.msgUrl, e) - return self.sso_failure( + return self.failure( request, reason=_('IdP is temporarily down, please try again ' 'later.'), status_codes=status_codes, @@ -403,7 +416,7 @@ class LoginView(ProfileMixin, LogMixin, View): result.status_code, result.content, ) - return self.sso_failure(request, reason=idp_message, status_codes=status_codes) + return self.failure(request, reason=idp_message, status_codes=status_codes) self.log.info('Got SAML Artifact Response', extra={'saml_response': result.content}) result.encoding = utils.get_xml_encoding(result.content) @@ -447,7 +460,7 @@ class LoginView(ProfileMixin, LogMixin, View): return HttpResponseBadRequest('error processing the authentication response: %r' % e) else: return self.sso_success(request, login) - return self.sso_failure(request, reason=idp_message, status_codes=status_codes) + return self.failure(request, reason=idp_message, status_codes=status_codes) def request_discovery_service(self, request, is_passive=False): return_url = request.build_absolute_uri() diff --git a/tests/test_views.py b/tests/test_views.py index 3783ce4..d2b13dc 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -294,3 +294,19 @@ def test_invalid_msg_on_artifact_resolve(private_settings, client, caplog, artif with HTTMock(html_response): client.get('/login/?SAMLart=%s' % artifact) assert 'ArtifactResolveResponse is malformed' in caplog.text + + +def test_private_key_unreadable(private_settings, app, tmpdir): + private_settings.MELLON_IDENTITY_PROVIDERS = [ + { + 'METADATA': open('tests/metadata.xml').read(), + } + ] + # set an unreadable private key + private_key = tmpdir / 'private.key' + with private_key.open(mode='w') as fd: + fd.write('1') + private_key.chmod(0o000) + private_settings.MELLON_PRIVATE_KEY = str(private_key) + response = app.get('/login/?next=%2Fwhatever') + assert 'Unable to initialize a SAML server object' in response