show an error page when create_server fails (#57176)

This commit is contained in:
Benjamin Dauvergne 2021-09-23 10:35:19 +02:00
parent 3f32879520
commit 4941fd7281
3 changed files with 66 additions and 31 deletions

View File

@ -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):

View File

@ -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()

View File

@ -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