Compare commits

...

2 Commits

Author SHA1 Message Date
Benjamin Dauvergne 84f36b6702 translation update (#56354)
gitea/django-mellon/pipeline/head Build started... Details
2022-01-12 23:24:18 +01:00
Benjamin Dauvergne 72f303d146 views: use a view to show all error messages (#56354) 2022-01-12 23:24:04 +01:00
6 changed files with 294 additions and 167 deletions

View File

@ -5,8 +5,8 @@ msgid ""
msgstr ""
"Project-Id-Version: django-mellon\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-05 22:09+0200\n"
"PO-Revision-Date: 2021-10-05 22:10+0200\n"
"POT-Creation-Date: 2022-01-12 16:23-0600\n"
"PO-Revision-Date: 2022-01-12 23:23+0100\n"
"Last-Translator: Benjamin Dauvergne <bdauvergne@entrouvert.com>\n"
"Language-Team: None\n"
"Language: fr\n"
@ -15,7 +15,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n>1;\n"
#: adapters.py:320
#: mellon/adapters.py
msgid ""
"A transient NameID was received but TRANSIENT_FEDERATION_ATTRIBUTE is not "
"set."
@ -23,99 +23,103 @@ msgstr ""
"Un NameID éphémère (transient) a été reçu mais "
"TRANSIENT_FEDERATION_ATTRIBUTE nest pas renseigné."
#: models.py:26
#: mellon/models.py
msgid "user"
msgstr "utilisateur"
#: models.py:31 models.py:45
#: mellon/models.py
msgid "SAML identifier"
msgstr "Identifiant SAML"
#: models.py:32
#: mellon/models.py
msgid "created"
msgstr "création"
#: models.py:33
#: mellon/models.py
msgid "Issuer"
msgstr "Émetteur"
#: models.py:36
#: mellon/models.py
msgid "user SAML identifier"
msgstr "Identifiant SAML dun usager"
#: models.py:37
#: mellon/models.py
msgid "users SAML identifiers"
msgstr "Identifiants SAML des usagers"
#: models.py:42 models.py:60
#: mellon/models.py
msgid "SAML SessionIndex"
msgstr "Index de session SAML"
#: models.py:43
#: mellon/models.py
msgid "Django session key"
msgstr "Clé de session Django"
#: models.py:61
#: mellon/models.py
msgid "SAML SessionIndexes"
msgstr "Index des sessions SAML"
#: models.py:66
#: mellon/models.py
msgid "IdP Entity ID"
msgstr "Entity ID de lIdP"
#: models.py:67
#: mellon/models.py
msgid "IdP slug"
msgstr "slug de lIdP"
#: models.py:70
#: mellon/models.py
msgid "SAML IdP"
msgstr "IdP SAML"
#: models.py:71
#: mellon/models.py
msgid "SAML IdPs"
msgstr "IdPs SAML"
#: templates/mellon/authentication_failed.html:12
#: mellon/templates/mellon/authentication_failed.html
msgid "Authentication failed"
msgstr "Échec de lauthentification"
#: templates/mellon/authentication_failed.html:14
#: mellon/templates/mellon/authentication_failed.html
msgid "The authentication has failed."
msgstr "Lauthentification a échoué."
#: templates/mellon/authentication_failed.html:15
#: mellon/templates/mellon/authentication_failed.html
msgid "Reason"
msgstr "Raison"
#: templates/mellon/authentication_failed.html:18
#: mellon/templates/mellon/authentication_failed.html
msgid "Continue"
msgstr "Continuer"
#: templates/mellon/debug_login.html:4
#: mellon/templates/mellon/authentication_failed.html
msgid "Information shown because DEBUG is True"
msgstr "Informations affichées car DEBUG est activé."
#: mellon/templates/mellon/debug_login.html
msgid "Try again"
msgstr "Essayer à nouveau"
#: templates/mellon/debug_login.html:5
#: mellon/templates/mellon/debug_login.html
msgid "Attributes:"
msgstr "Attributs :"
#: templates/mellon/debug_login.html:6
#: mellon/templates/mellon/debug_login.html
msgid "SAML assertion:"
msgstr "Assertion SAML :"
#: templates/mellon/debug_login.html:7
#: mellon/templates/mellon/debug_login.html
msgid "SAML response:"
msgstr "Réponse SAML :"
#: templates/mellon/debug_login.html:8
#: mellon/templates/mellon/debug_login.html
msgid "SAML artifact:"
msgstr "Artéfact SAML :"
#: templates/mellon/debug_login.html:9
#: mellon/templates/mellon/debug_login.html
msgid "Logs:"
msgstr "Journaux :"
#: templates/mellon/inactive_user.html:6
#: mellon/templates/mellon/inactive_user.html
#, python-format
msgid ""
"Your user, %(user)s, \n"
@ -125,12 +129,12 @@ msgstr ""
"Lutilisateur « %(user)s est inactif, veuillez contacter votre "
"administrateur."
#: templates/mellon/user_not_found.html:6
#: mellon/templates/mellon/user_not_found.html
#, python-format
msgid "No user found for NameID %(name_id)s"
msgstr "Aucun utilisateur trouvé pour lidentifiant SAML %(name_id)s"
#: views.py:142
#: mellon/views.py
msgid ""
"Unable to initialize a SAML server object, the private key is maybe invalid "
"or unreadable, please check its access rights and content."
@ -138,21 +142,102 @@ msgstr ""
"Impossible dinitialiser lobjet serveur SAML, la clé privée est peut-être "
"invalide ou inaccessible, il faut en vérifier les permissions et le contenu."
#: views.py:357
#: mellon/views.py
#, python-format
msgid "%s refused by identity provider"
msgstr "%s refusé par le fournisseur d'identité"
#: mellon/views.py
#, python-format
msgid "Status codes: %s"
msgstr "Codes de statut: %s"
#: mellon/views.py
#, python-format
msgid "Other error: %s"
msgstr "Autre erreur: %s"
#: mellon/views.py
#, python-format
msgid "the received message EntityID (%r) is unknown."
msgstr "L'identifiant d'entité SAML est inconnu (%r)."
#: mellon/views.py
msgid ""
"There was a problem with a signature. It usually means that signature key "
"has changed or is unknown. The metadata of the identity provider may be out "
"of date."
msgstr ""
"Une signature n'a pas pu être vérifié. Cela provient généralement du fait "
"que la clé de signature n'a pu être trouvé, ou bien que ce n'est pas la "
"bonne. Il se peut que les métadonnées du fournisseur d'identité soient "
"périmées."
#: mellon/views.py
msgid ""
"Possible solution: you currently do not use METADATA_URL to reference the "
"metadata, use it to always keep the metadata up to date."
msgstr ""
"Une solution possible: vous n'utilisez pas actuellement l'option "
"METADATA_URL , l'utiliser permettra de conserver les métadonnées à jour."
#: mellon/views.py
msgid "Artifact binding is not supported"
msgstr "Le binding « Artifact » n'est pas géré."
#: mellon/views.py
msgid "Post binding is not supported"
msgstr "Le binding « POST » n'est pas géré."
#: mellon/views.py
msgid "SAMLResponse contains a null character"
msgstr "SAMLResponse contienn un caractère nulle."
#: mellon/views.py
msgid "Login"
msgstr "Connection"
#: mellon/views.py
msgid "Technical error"
msgstr "Erreur technique"
#: mellon/views.py
msgid "There were too many redirections with the identity provider."
msgstr "Il y a eu trop de redirections avec le fournisseur didentité."
#: views.py:410
#: mellon/views.py
msgid "Artifact is invalid."
msgstr "« L'artifact » est invalide."
#: mellon/views.py
msgid "IdP is temporarily down, please try again later."
msgstr ""
"Le fournisseur didentités est temporairement inaccessible, veuillez "
"réessayer plus tard."
#~ msgid "Message"
#~ msgstr "Message"
#: mellon/views.py
msgid "Could not process the ArtifactResolveResponse"
msgstr "Impossible de traiter la réponse ArtifactResolveResponse"
#~ msgid "Codes"
#~ msgstr "Codes"
#: mellon/views.py
#, python-format
msgid "No idp found for \"%s\""
msgstr ""
"Pas de fournisseur d'identité trouvé dans la configuration pour "
"l'identifiant « %s »."
#~ msgid "Relaystate"
#~ msgstr "Information détat (relaystate)"
#: mellon/views.py
msgid "Could not initialize the authentication request"
msgstr "Impossible d'initialiser la requête d'authentification."
#: mellon/views.py
msgid "Could not process the logout request"
msgstr "Impossible de traiter la requête de déconnexion."
#: mellon/views.py
msgid "Could not process the logout response"
msgstr "Impossible de traiter la réponse de déconnexion."
#: mellon/views.py
msgid "Logout"
msgstr "Déconnexion"

View File

@ -12,18 +12,24 @@
<h2 class="mellon-message-header">{% trans "Authentication failed" %}</h2>
<p class="mellon-message-body">
{% blocktrans %}The authentication has failed.{% endblocktrans %}
{% if reason %}<p class="mellon-reason">{% trans "Reason" %}&nbsp;: {{ reason }}</p>{% endif %}
{% for reason in reasons %}
<p class="mellon-reason">{% if loop.first %}{% trans "Reason" %}&nbsp;: {% endif %}{{ reason }}</p>
{% endfor %}
</p>
<p class="mellon-message-continue">
<a class="mellon-link" href="{{ next_url }}">{% trans "Continue" %}</a>
</p>
{% if debug %}
<!-- DEBUG INFO:
Issuer: {{ issuer }}
Message: {{ status_message }}
Status codes: {{ status_codes|join:", " }}
Relaystate: {{ relaystate }}
-->
<p>{% trans "Information shown because DEBUG is True" %}</p>
<pre>
Issuer: {{ issuer }}
Message: {{ status_message }}
Status codes: {{ status_codes|join:", " }}
Relaystate: {{ relaystate }}
{% for key, value in debug_details.items %}
{{key}}: {{value|pprint}}
{% endfor %}
</pre>
{% endif %}
</div>
{% endblock %}

View File

@ -22,7 +22,6 @@ from importlib import import_module
from io import StringIO
from xml.sax.saxutils import escape
import django.http
import lasso
import requests
from django.conf import settings
@ -57,13 +56,6 @@ LOGIN_HINT = '{%s}login-hint' % EO_NS
User = get_user_model()
class HttpResponseBadRequest(django.http.HttpResponseBadRequest):
def __init__(self, *args, **kwargs):
kwargs['content_type'] = kwargs.get('content_type', 'text/plain')
super().__init__(*args, **kwargs)
self['X-Content-Type-Options'] = 'nosniff'
class LogMixin:
"""Initialize a module logger in new objects"""
@ -93,6 +85,20 @@ def check_next_url(request, next_url):
class ProfileMixin:
profile = None
def dispatch(self, request, *args, **kwargs):
try:
return super().dispatch(request, *args, **kwargs)
# handle common errors
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 set_next_url(self, next_url):
if not check_next_url(self.request, next_url):
return
@ -124,34 +130,69 @@ class ProfileMixin:
def get_next_url(self, default=None):
return self.get_state('next_url', default=default)
def show_message_status_is_not_success(self, profile, prefix):
@property
def template_base(self):
return self.kwargs.get('template_base', 'base.html')
def render(self, request, template_names, context, status=None):
context['template_base'] = self.template_base
if 'context_hook' in self.kwargs:
self.kwargs['context_hook'](context)
return render(request, template_names, context, status=status)
def failure_status_is_not_success(self, request, profile, prefix):
status_codes, idp_message = utils.get_status_codes_and_message(profile)
args = ['%s: status is not success codes: %r', prefix, status_codes]
if idp_message:
args[0] += ' message: %s'
args.append(idp_message)
self.log.warning(*args)
return self.failure(
request,
reasons=[
_('%s refused by identity provider') % prefix,
idp_message,
_('Status codes: %s') % ', '.join([str(code) for code in status_codes]),
],
status_codes=status_codes,
)
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=()):
def failure(self, request, reason='', reasons=None, status_codes=(), exception=None, debug_details=None):
'''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)
reasons = reasons or []
reasons.append(reason)
profile = self.profile
idp = utils.get_idp(profile.remoteProviderId) if profile else {}
if not idp and profile and not isinstance(exception, lasso.ServerProviderNotFoundError):
self.log.warning('entity id %r is unknown', profile.remoteProviderId)
reasons.insert(
0,
_('Other error: %s')
% (_('the received message EntityID (%r) is unknown.') % profile.remoteProviderId),
)
if exception and isinstance(
exception,
(
lasso.DsError,
lasso.ProfileCannotVerifySignatureError,
lasso.LoginInvalidSignatureError,
lasso.LoginInvalidAssertionSignatureError,
),
):
reasons.append(
_(
'There was a problem with a signature. It usually means '
'that signature key has changed or is unknown. '
'The metadata of the identity provider may be out of date.'
)
)
if 'METADATA_URL' not in idp:
reasons.append(
_(
'Possible solution: you currently do not use METADATA_URL '
'to reference the metadata, use it to always keep the metadata up to date.'
)
)
error_url = utils.get_setting(idp, 'ERROR_URL')
error_redirect_after_timeout = utils.get_setting(idp, 'ERROR_REDIRECT_AFTER_TIMEOUT')
if error_url:
@ -162,13 +203,15 @@ class ProfileMixin:
'mellon/authentication_failed.html',
{
'debug': settings.DEBUG,
'reason': reason,
'reasons': reasons,
'status_codes': status_codes,
'issuer': login and login.remoteProviderId,
'issuer': profile.remoteProviderId if profile else None,
'next_url': next_url,
'relaystate': login and login.msgRelayState,
'relaystate': profile.msgRelayState if profile else None,
'error_redirect_after_timeout': error_redirect_after_timeout,
'debug_details': debug_details or {},
},
status=400,
)
@ -178,18 +221,7 @@ class LoginView(ProfileMixin, LogMixin, View):
with self.capture_logs() if self.debug_login else nullcontext():
return super().dispatch(request, *args, **kwargs)
@property
def template_base(self):
return self.kwargs.get('template_base', 'base.html')
def render(self, request, template_names, context):
context['template_base'] = self.template_base
if 'context_hook' in self.kwargs:
self.kwargs['context_hook'](context)
return render(request, template_names, context)
def get_idp(self, request):
entity_id = request.POST.get('entityID') or request.GET.get('entityID')
def get_idp(self, entity_id):
if not entity_id:
for idp in utils.get_idps():
return idp
@ -202,41 +234,31 @@ class LoginView(ProfileMixin, LogMixin, View):
'''Assertion consumer'''
if 'SAMLart' in request.POST:
if 'artifact' not in app_settings.ASSERTION_CONSUMER_BINDINGS:
raise Http404('artifact binding is not supported')
raise self.failure(request, _('Artifact binding is not supported'))
return self.continue_sso_artifact(request, lasso.HTTP_METHOD_ARTIFACT_POST)
if 'SAMLResponse' not in request.POST:
if 'post' not in app_settings.ASSERTION_CONSUMER_BINDINGS:
raise Http404('post binding is not supported')
raise self.failure(request, _('Post binding is not supported'))
return self.get(request, *args, **kwargs)
# prevent null characters in SAMLResponse
if not utils.is_nonnull(request.POST['SAMLResponse']):
return HttpResponseBadRequest('SAMLResponse contains a null character')
return self.failure(_('SAMLResponse contains a null character'))
self.log.info('Got SAML Response', extra={'saml_response': request.POST['SAMLResponse']})
self.profile = login = utils.create_login(request)
idp_message = None
status_codes = []
# prevent null characters in SAMLResponse
try:
login.processAuthnResponseMsg(request.POST['SAMLResponse'])
login.acceptSso()
except lasso.ProfileCannotVerifySignatureError:
self.log.warning(
'SAML authentication failed: signature validation failed for %r', login.remoteProviderId
)
except lasso.ParamError:
self.log.exception('lasso param error')
except (
lasso.LoginStatusNotSuccessError,
lasso.ProfileStatusNotSuccessError,
lasso.ProfileRequestDeniedError,
):
self.show_message_status_is_not_success(login, 'SAML authentication failed')
except lasso.Error as e:
return HttpResponseBadRequest('error processing the authentication response: %r' % e)
else:
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.failure(request, reason=idp_message, status_codes=status_codes)
return self.failure_status_is_not_success(request, login, _('Login'))
except Exception as e:
return self.failure(request, _('Technical error'), exception=e)
if 'RelayState' in request.POST and utils.is_nonnull(request.POST['RelayState']):
login.msgRelayState = request.POST['RelayState']
return self.sso_success(request, login)
def get_attribute_value(self, attribute, attribute_value):
# check attribute_value contains only text
@ -369,9 +391,6 @@ class LoginView(ProfileMixin, LogMixin, View):
return response
def continue_sso_artifact(self, request, method):
idp_message = None
status_codes = []
if method == lasso.HTTP_METHOD_ARTIFACT_GET:
message = request.META['QUERY_STRING']
artifact = request.GET['SAMLart']
@ -386,15 +405,22 @@ class LoginView(ProfileMixin, LogMixin, View):
login.msgRelayState = relay_state
try:
login.initRequest(message, method)
except lasso.ProfileInvalidArtifactError:
except lasso.ProfileInvalidArtifactError as e:
self.log.warning('artifact is malformed %r', artifact)
return HttpResponseBadRequest('artifact is malformed %r' % artifact)
except lasso.ServerProviderNotFoundError:
return self.failure(request, _('Artifact is invalid.'), exception=e)
except lasso.ServerProviderNotFoundError as e:
self.log.warning('no entity id found for artifact %s', artifact)
return HttpResponseBadRequest('no entity id found for this artifact %r' % artifact)
return self.failure(
request,
_('the received message EntityID (%r) is unknown.') % login.remoteProviderId,
exception=e,
)
idp = utils.get_idp(login.remoteProviderId)
if not idp:
return HttpResponseBadRequest('entity id %r is unknown' % login.remoteProviderId)
return self.failure(
request,
_('the received message EntityID (%r) is unknown.') % login.remoteProviderId,
)
verify_ssl_certificate = utils.get_setting(idp, 'VERIFY_SSL_CERTIFICATE')
login.buildRequestMsg()
try:
@ -407,18 +433,14 @@ class LoginView(ProfileMixin, LogMixin, View):
)
except RequestException as e:
self.log.warning('unable to reach %r: %s', login.msgUrl, e)
return self.failure(
request,
reason=_('IdP is temporarily down, please try again ' 'later.'),
status_codes=status_codes,
)
return self.failure(request, reason=_('IdP is temporarily down, please try again later.'))
if result.status_code != 200:
self.log.warning(
'SAML authentication failed: IdP returned %s when given artifact: %r',
result.status_code,
result.content,
)
return self.failure(request, reason=idp_message, status_codes=status_codes)
return self.failure(request, reason=_('IdP is temporarily down, please try again later.'))
self.log.info('Got SAML Artifact Response', extra={'saml_response': result.content})
result.encoding = utils.get_xml_encoding(result.content)
@ -429,40 +451,28 @@ class LoginView(ProfileMixin, LogMixin, View):
# artifact is invalid, idp returned no response
self.log.warning('ArtifactResolveResponse is empty: dead artifact %r', artifact)
return self.retry_login()
except lasso.ProfileInvalidMsgError:
except lasso.ProfileInvalidMsgError as e:
self.log.warning('ArtifactResolveResponse is malformed %r', result.content[:200])
if settings.DEBUG:
return HttpResponseBadRequest('ArtififactResolveResponse is malformed\n%r' % result.content)
else:
return HttpResponseBadRequest('ArtififactResolveResponse is malformed')
except lasso.ProfileCannotVerifySignatureError:
self.log.warning(
'SAML authentication failed: signature validation failed for %r', login.remoteProviderId
return self.failure(
request,
_('Could not process the ArtifactResolveResponse'),
exception=e,
)
except lasso.ParamError:
self.log.exception('lasso param error')
except (
lasso.LoginStatusNotSuccessError,
lasso.ProfileStatusNotSuccessError,
lasso.ProfileRequestDeniedError,
):
status = login.response.status
a = status
while a.statusCode:
status_codes.append(a.statusCode.value)
a = a.statusCode
args = ['SAML authentication failed: status is not success codes: %r', status_codes]
if status.statusMessage:
idp_message = lasso_decode(status.statusMessage)
args[0] += ' message: %r'
args.append(status.statusMessage)
self.log.warning(*args)
except lasso.Error as e:
self.log.exception('unexpected lasso error')
return HttpResponseBadRequest('error processing the authentication response: %r' % e)
return self.failure_status_is_not_success(request, login, _('Login'))
except Exception as e:
return self.failure(
request,
_('Technical error'),
exception=e,
debug_details={'ArtifactResolveResponse_content': result.content},
)
else:
return self.sso_success(request, login)
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()
@ -495,9 +505,10 @@ class LoginView(ProfileMixin, LogMixin, View):
return self.request_discovery_service(request, is_passive=request.GET.get('passive') == '1')
next_url = check_next_url(self.request, request.GET.get(REDIRECT_FIELD_NAME))
idp = self.get_idp(request)
entity_id = request.POST.get('entityID') or request.GET.get('entityID')
idp = self.get_idp(entity_id)
if not idp:
return HttpResponseBadRequest('no idp found')
return self.failure(request, _('No idp found for "%s"') % (entity_id or ''))
self.profile = login = utils.create_login(request)
self.log.debug('authenticating to %r', idp['ENTITY_ID'])
try:
@ -550,7 +561,7 @@ class LoginView(ProfileMixin, LogMixin, View):
self.add_login_hints(idp, authn_request, request=request, next_url=next_url or '/')
login.buildAuthnRequestMsg()
except lasso.Error as e:
return HttpResponseBadRequest('error initializing the authentication request: %r' % e)
return self.failure(request, _('Could not initialize the authentication request'), exception=e)
self.log.debug('sending authn request %r', authn_request.dump())
self.log.debug('to url %r', login.msgUrl)
return HttpResponseRedirect(login.msgUrl)
@ -661,7 +672,7 @@ class LogoutView(ProfileMixin, LogMixin, View):
try:
logout.processRequestMsg(msg)
except lasso.Error as e:
return HttpResponseBadRequest('error processing logout request: %r' % e)
return self.failure(request, _('Could not process the logout request'), exception=e)
entity_id = force_text(logout.remoteProviderId)
session_indexes = {force_text(sessionIndex) for sessionIndex in logout.request.sessionIndexes}
@ -706,7 +717,7 @@ class LogoutView(ProfileMixin, LogMixin, View):
try:
logout.buildResponseMsg()
except lasso.Error as e:
return HttpResponseBadRequest('error processing logout request: %r' % e)
return self.failure(request, _('Could not process the logout response'), exception=e)
if logout.msgBody:
return HttpResponse(force_text(logout.msgBody), content_type='text/xml')
else:
@ -759,7 +770,7 @@ class LogoutView(ProfileMixin, LogMixin, View):
try:
logout.processResponseMsg(request.META['QUERY_STRING'])
except lasso.ProfileStatusNotSuccessError:
self.show_message_status_is_not_success(logout, 'SAML logout failed')
return self.failure_status_is_not_success(request, logout, _('Logout'))
except lasso.LogoutPartialLogoutError:
self.log.warning('partial logout')
except lasso.Error as e:

View File

@ -429,12 +429,16 @@ def test_sso_request_denied(db, app, idp, caplog, sp_settings):
)
assert not relay_state
assert url.endswith(reverse('mellon_login'))
response = app.post(reverse('mellon_login'), params={'SAMLResponse': body, 'RelayState': relay_state})
response = app.post(
reverse('mellon_login'), params={'SAMLResponse': body, 'RelayState': relay_state}, status=400
)
assert 'User is not allowed to login' in response
assert (
"status is not success codes: ['urn:oasis:names:tc:SAML:2.0:status:Responder',\
'urn:oasis:names:tc:SAML:2.0:status:RequestDenied']"
in caplog.text
)
assert 'User is not allowed to login' in caplog.text
@pytest.mark.urls('urls_tests_template_base')
@ -444,7 +448,9 @@ def test_template_base(db, app, idp, caplog, sp_settings):
url, body, relay_state = idp.process_authn_request_redirect(
response['Location'], auth_result=False, msg='User is not allowed to login'
)
response = app.post(reverse('mellon_login'), params={'SAMLResponse': body, 'RelayState': relay_state})
response = app.post(
reverse('mellon_login'), params={'SAMLResponse': body, 'RelayState': relay_state}, status=400
)
assert 'Theme is ok' in response.text
response = app.get(reverse('mellon_login'))
@ -461,7 +467,9 @@ def test_template_hook(db, app, idp, caplog, sp_settings):
url, body, relay_state = idp.process_authn_request_redirect(
response['Location'], auth_result=False, msg='User is not allowed to login'
)
response = app.post(reverse('mellon_login'), params={'SAMLResponse': body, 'RelayState': relay_state})
response = app.post(
reverse('mellon_login'), params={'SAMLResponse': body, 'RelayState': relay_state}, status=400
)
assert 'Theme is ok' in response.text
assert 'HOOK' in response.text
@ -477,7 +485,9 @@ def test_no_template_base(db, app, idp, caplog, sp_settings):
url, body, relay_state = idp.process_authn_request_redirect(
response['Location'], auth_result=False, msg='User is not allowed to login'
)
response = app.post(reverse('mellon_login'), params={'SAMLResponse': body, 'RelayState': relay_state})
response = app.post(
reverse('mellon_login'), params={'SAMLResponse': body, 'RelayState': relay_state}, status=400
)
assert 'Theme is ok' not in response.text
@ -496,7 +506,7 @@ def test_sso_request_denied_artifact(db, app, caplog, sp_settings, idp_metadata,
assert 'SAMLart' in url
acs_artifact_url = url.split('testserver', 1)[1]
with HTTMock(idp.mock_artifact_resolver()):
response = app.get(acs_artifact_url, params={'RelayState': relay_state})
response = app.get(acs_artifact_url, params={'RelayState': relay_state}, status=400)
assert (
"status is not success codes: ['urn:oasis:names:tc:SAML:2.0:status:Responder',\
'urn:oasis:names:tc:SAML:2.0:status:RequestDenied']"
@ -601,7 +611,7 @@ def test_sso_artifact_no_loop(db, app, caplog, sp_settings, idp_metadata, idp_pr
# forget the artifact
idp.artifact = ''
with HTTMock(idp.mock_artifact_resolver()):
response = app.get(acs_artifact_url)
response = app.get(acs_artifact_url, status=400)
# check cookie is deleted after failed retry
# Py3-Dj111 variation

View File

@ -158,7 +158,7 @@ def test_sp_initiated_login_improperly_configured2(private_settings, client):
private_settings.MELLON_IDENTITY_PROVIDERS = []
response = client.get('/login/')
assert response.status_code == 400
assert b'no idp found' in response.content
assert 'No idp found for' in response.content.decode()
def test_sp_initiated_login_discovery_service(private_settings, client):
@ -190,8 +190,7 @@ def test_sp_initiated_login_discovery_service_nodisco(private_settings, client):
private_settings.MELLON_IDENTITY_PROVIDERS = []
private_settings.MELLON_DISCOVERY_SERVICE_URL = 'https://disco'
response = client.get('/login/?nodisco=1')
assert response.status_code == 400
assert b'no idp found' in response.content
assert 'No idp found for' in response.content.decode()
def test_sp_initiated_login(private_settings, client):
@ -254,16 +253,15 @@ def test_sp_initiated_login_requested_authn_context(private_settings, client):
)
def test_malfortmed_artifact(private_settings, client, caplog):
def test_malfortmed_artifact(private_settings, app, caplog):
private_settings.MELLON_IDENTITY_PROVIDERS = [
{
'METADATA': open('tests/metadata.xml').read(),
}
]
response = client.get('/login/?SAMLart=xxx', status=400)
assert response['Content-Type'] == 'text/plain'
assert response['X-Content-Type-Options'] == 'nosniff'
assert b'artifact is malformed' in response.content
response = app.get('/login/?SAMLart=xxx', status=400)
assert response['Content-Type'].split(';')[0] == 'text/html'
assert 'Artifact is invalid' in response
assert 'artifact is malformed' in caplog.text
@ -308,5 +306,5 @@ def test_private_key_unreadable(private_settings, app, tmpdir):
fd.write('1')
private_key.chmod(0o000)
private_settings.MELLON_PRIVATE_KEY = str(private_key)
response = app.get('/login/?next=%2Fwhatever')
response = app.get('/login/?next=%2Fwhatever', status=400)
assert 'Unable to initialize a SAML server object' in response

17
tox.ini
View File

@ -51,11 +51,28 @@ setenv =
DJANGO_SETTINGS_MODULE=testsettings
PYTHONPATH=.:tests
deps =
django<2.3
psycopg2-binary
commands =
./getlasso3.sh
django-admin {posargs:--help}
[testenv:update-locales]
whitelist_externals = django-admin
usedevelop = True
setenv =
DJANGO_SETTINGS_MODULE=testsettings
PYTHONPATH=.
deps =
django<2.3
psycopg2-binary
commands =
./getlasso3.sh
django-admin makemessages --add-location=file -l fr
django-admin compilemessages -l fr
[testenv:code-style]
skip_install = true
deps =