authenticators: add view for login failure events (#76781) #79

Merged
vdeniaud merged 2 commits from wip/76781-manager-verifier-que-les-eveneme into main 2023-07-18 11:05:43 +02:00
6 changed files with 69 additions and 13 deletions

View File

@ -50,6 +50,11 @@ urlpatterns = [
views.journal,
name='a2-manager-authenticator-journal',
),
path(
'authenticators/<int:pk>/login-journal/',
views.login_journal,
name='a2-manager-authenticator-login-journal',
),
path('authenticators/<int:pk>/export/', views.export_json, name='a2-manager-authenticator-export'),
path('authenticators/import/', views.import_json, name='a2-manager-authenticator-import'),
path(

View File

@ -12,7 +12,8 @@
<a href="{% url 'a2-manager-authenticator-edit' pk=object.pk %}">{% trans "Edit" %}</a>
<ul class="extra-actions-menu">
<li><a href="{% url 'a2-manager-authenticator-export' pk=object.pk %}">{% trans "Export" %}</a></li>
<li><a href="{% url 'a2-manager-authenticator-journal' pk=object.pk %}">{% trans "Journal" %}</a></li>
<li><a href="{% url 'a2-manager-authenticator-journal' pk=object.pk %}">{% trans "Journal of edits" %}</a></li>
<li><a href="{% url 'a2-manager-authenticator-login-journal' pk=object.pk %}">{% trans "Journal of logins" %}</a></li>
{% if not object.protected %}
<li><a rel="popup" href="{% url 'a2-manager-authenticator-delete' pk=object.pk %}">{% trans "Delete" %}</a></li>
{% endif %}

View File

@ -179,22 +179,44 @@ toggle = AuthenticatorToggleView.as_view()
class AuthenticatorJournal(JournalViewWithContext, BaseJournalView):
template_name = 'authentic2/authenticators/authenticator_journal.html'
title = _('Journal')
title = _('Journal of edits')
@cached_property
def context(self):
return get_object_or_404(BaseAuthenticator.authenticators.all(), pk=self.kwargs['pk'])
def get_events(self):
return super().get_events().filter(type__name__startswith='authenticator')
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['object'] = self.context
ctx['object_name'] = str(self.context)
return ctx
journal = AuthenticatorJournal.as_view()
class AuthenticatorLoginJournal(JournalViewWithContext, BaseJournalView):
template_name = 'authentic2/authenticators/authenticator_journal.html'
title = _('Journal of logins')
@cached_property
def context(self):
return get_object_or_404(BaseAuthenticator.authenticators.all(), pk=self.kwargs['pk'])
def get_events(self):
return super().get_events().filter(type__name__startswith='user')
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['object'] = self.context
return ctx
login_journal = AuthenticatorLoginJournal.as_view()
class AuthenticatorExportView(AuthenticatorsMixin, DetailView):
def get(self, request, *args, **kwargs):
authenticator = self.get_object()

View File

@ -169,19 +169,23 @@ class UserLoginFailure(EventTypeWithService):
'username': username,
'reason': reason,
},
references=[authenticator.baseauthenticator_ptr],
references=[authenticator],
)
@classmethod
def get_message(cls, event, context):
username = event.get_data('username')
reason = event.get_data('reason')
(authenticator,) = event.get_typed_references(BaseAuthenticator)
(service, authenticator) = event.get_typed_references(Service, BaseAuthenticator)
if service is None:
(authenticator,) = event.get_typed_references(BaseAuthenticator)
if username:
msg = _('login failure with username "{username}"').format(username=username)
else:
msg = _('unknown failed login attempt')
if authenticator:
if authenticator and context != authenticator:
msg += _(' on authenticator {authenticator}').format(authenticator=authenticator)
if reason:
msg.append(_(' (reason: {reason})').format(reason=reason))

View File

@ -126,7 +126,7 @@ def test_authenticators_password(app, superuser_or_admin, settings):
assert 'Disable' not in resp.text
app.get('/manage/authenticators/%s/toggle/' % authenticator.pk, status=403)
resp = resp.click('Journal')
resp = resp.click('Journal of edits')
assert resp.text.count('edit (show_condition)') == 2
# cannot add another password authenticator
@ -272,7 +272,7 @@ def test_authenticators_oidc(app, superuser, ou1, ou2):
assert 'Authenticator has been enabled.' in resp.text
assert_event('authenticator.enable', user=superuser, session=app.session)
resp = resp.click('Journal')
resp = resp.click('Journal of edits')
assert 'enable' in resp.text
assert (
'edit (ou, issuer, scopes, strategy, client_id, button_label, idtoken_algo, '
@ -600,7 +600,7 @@ def test_authenticators_saml(app, superuser, ou1, ou2):
assert resp.pyquery('button#tab-advanced').attr('class') == 'pk-tabs--button-marker'
resp = app.get(authenticator.get_absolute_url())
resp = resp.click('Journal')
resp = resp.click('Journal of edits')
assert 'edit (metadata_url)' in resp.text
@ -881,3 +881,27 @@ def test_authenticators_configuration_info(app, superuser, ou1, ou2):
'Redirect URI after logout (post_logout_redirect_uri):<br><a href="https://testserver/logout/" '
'rel="nofollow">https://testserver/logout/</a>'
) in resp.text
def test_authenticators_journal_pages(app, superuser):
authenticator = LoginPasswordAuthenticator.objects.get()
# generate login failure event
login(app, 'noone', password='wrong', fail=True)
login(app, superuser)
resp = app.get('/manage/authenticators/%s/edit/' % authenticator.pk)
# generate edit event
resp.form['button_description'] = 'abc'
resp = resp.form.submit().follow()
resp = app.get('/manage/authenticators/%s/detail/' % authenticator.pk)
resp = resp.click('Journal of edits')
assert resp.pyquery('td.journal-list--message-column').text() == 'edit (button_description)'
assert 'noone' not in resp.text
resp = app.get('/manage/authenticators/%s/detail/' % authenticator.pk)
resp = resp.click('Journal of logins')
assert resp.pyquery('td.journal-list--message-column').text() == 'login failure with username "noone"'
assert 'edit (button_description)' not in resp.text

View File

@ -84,7 +84,7 @@ def events(db, superuser, freezer):
)
make("user.logout", user=user, session=session1)
make("user.login.failure", authenticator=saml_authenticator, username="user")
make("user.login.failure", service=service, authenticator=saml_authenticator, username="user")
make("user.login.failure", authenticator=saml_authenticator, username="agent")
make("user.login", user=user, session=session1, how="password")
make("user.password.change", user=user, session=session1)
@ -397,13 +397,13 @@ def test_global_journal(app, superuser, events):
'user': 'Johnny doe',
},
{
'message': 'login failure with username "user" on authenticator base authenticator - saml',
'message': 'login failure with username "user" on authenticator SAML - saml',
'timestamp': 'Jan. 1, 2020, 3 a.m.',
'type': 'user.login.failure',
'user': '-',
},
{
'message': 'login failure with username "agent" on authenticator base authenticator - saml',
'message': 'login failure with username "agent" on authenticator SAML - saml',
'timestamp': 'Jan. 1, 2020, 4 a.m.',
'type': 'user.login.failure',
'user': '-',
@ -1230,7 +1230,7 @@ def test_search(app, superuser, events):
for p in zip(pq('tbody td.journal-list--user-column'), pq('tbody td.journal-list--message-column'))
] == [
['agent', 'login using SAML'],
['-', 'login failure with username "agent" on authenticator base authenticator - saml'],
['-', 'login failure with username "agent" on authenticator SAML - saml'],
]
response.form.set('search', 'uuid:%s event:reset' % events['user'].uuid)