Skip to content

Commit 4e87dfc

Browse files
stsewdericholscher
andauthored
Organizations: show audit logs (#8588)
Similar to the user security logs, but from all the organization and according to their plan. We also now respect the filters when downloading the data. Co-authored-by: Eric Holscher <[email protected]>
1 parent 6162eb7 commit 4e87dfc

File tree

12 files changed

+582
-68
lines changed

12 files changed

+582
-68
lines changed

docs/index.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ and some of the core features of Read the Docs.
8484
:doc:`/analytics` |
8585
:doc:`/pull-requests` |
8686
:doc:`/build-notifications` |
87-
:doc:`/user-defined-redirects`
87+
:doc:`/user-defined-redirects` |
88+
:doc:`/security-log`
8889

8990
* **Connecting with GitHub, BitBucket, or GitLab**:
9091
:doc:`Connecting your VCS account </connected-accounts>`
@@ -114,6 +115,7 @@ and some of the core features of Read the Docs.
114115
/analytics
115116
/pull-requests
116117
/build-notifications
118+
/security-log
117119

118120
/connected-accounts
119121

docs/security-log.rst

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
Security Log
2+
============
3+
4+
Security logs allow you to see what has happened recently in your organization or account.
5+
We store the IP address and the browser's User-Agent_ on each event,
6+
so that you can confirm this access was from the intended person.
7+
8+
.. _User-Agent: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent
9+
10+
User security log
11+
-----------------
12+
13+
We store user security logs from the last 90 days, and track the following events:
14+
15+
- Authentication on the dashboard
16+
- Authentication on documentation pages (:doc:`/commercial/index` only)
17+
18+
Authentication failures and successes are both tracked.
19+
20+
To access your logs:
21+
22+
- Click on :guilabel:`Username` dropdown
23+
- Click on :guilabel:`Settings`
24+
- Click on :guilabel:`Security Log`
25+
26+
Organization security log
27+
-------------------------
28+
29+
.. note::
30+
31+
This feature exists only on :doc:`/commercial/index`.
32+
33+
We store logs according to your plan,
34+
check our `pricing page <https://readthedocs.com/pricing/>`__ for more details.
35+
We track the following events:
36+
37+
- Authentication on documentation pages from your organization
38+
- User access to every documentation page from your organization (**Enterprise plans only**)
39+
40+
Authentication failures and successes are both tracked.
41+
42+
To access your organization logs:
43+
44+
- Click on :guilabel:`Organizations` from your user dropdown
45+
- Click on your organization
46+
- Click on :guilabel:`Settings`
47+
- Click on :guilabel:`Security Log`

readthedocs/audit/filters.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
"""Filters used in our views."""
22

33
from django.utils.translation import ugettext_lazy as _
4-
from django_filters import CharFilter, ChoiceFilter, FilterSet
4+
from django_filters import (
5+
CharFilter,
6+
ChoiceFilter,
7+
DateFromToRangeFilter,
8+
FilterSet,
9+
)
510

611
from readthedocs.audit.models import AuditLog
712

@@ -18,7 +23,23 @@ class UserSecurityLogFilter(FilterSet):
1823
(AuditLog.AUTHN_FAILURE, _('Authentication failure')),
1924
],
2025
)
26+
date = DateFromToRangeFilter(field_name='created')
2127

2228
class Meta:
2329
model = AuditLog
24-
fields = ['ip', 'project', 'action']
30+
fields = []
31+
32+
33+
class OrganizationSecurityLogFilter(UserSecurityLogFilter):
34+
35+
action = ChoiceFilter(
36+
field_name='action',
37+
lookup_expr='exact',
38+
choices=[
39+
(AuditLog.AUTHN, _('Authentication success')),
40+
(AuditLog.AUTHN_FAILURE, _('Authentication failure')),
41+
(AuditLog.PAGEVIEW, _('Page view')),
42+
(AuditLog.DOWNLOAD, _('Download')),
43+
],
44+
)
45+
user = CharFilter(field_name='log_user_username', lookup_expr='exact')

readthedocs/audit/models.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,5 +197,23 @@ def save(self, **kwargs):
197197
self.log_organization_slug = organization.slug
198198
super().save(**kwargs)
199199

200+
def auth_backend_display(self):
201+
"""
202+
Get a string representation for backends that aren't part of the normal login.
203+
204+
.. note::
205+
206+
The backends listed here are implemented on .com only.
207+
"""
208+
backend = self.auth_backend or ''
209+
backend_displays = {
210+
'TemporaryAccessTokenBackend': _('shared link'),
211+
'TemporaryAccessPasswordBackend': _('shared password'),
212+
}
213+
for name, display in backend_displays.items():
214+
if name in backend:
215+
return display
216+
return ''
217+
200218
def __str__(self):
201219
return self.action
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
{% load i18n %}
2+
3+
{% comment %}
4+
If `omit_user` is given, the username attached to the log isn't shown.
5+
This is useful when listing logs for the same user.
6+
{% endcomment %}
7+
8+
<div class="module-list">
9+
<div class="module-list-wrapper">
10+
<ul>
11+
{% for log in object_list %}
12+
<li class="module-item">
13+
{% if log.action == AuditLog.AUTHN %}
14+
{% if omit_user %}
15+
{% blocktrans trimmed with action=log.action method=log.auth_backend_display %}
16+
<a href="?action={{ action }}" title="{{ method }}">Authenticated</a>
17+
{% endblocktrans %}
18+
{% elif log.log_user_id %}
19+
{% blocktrans trimmed with action=log.action user=log.log_user_username method=log.auth_backend_display %}
20+
<a href="?user={{ user }}">
21+
<code>{{ user }}</code>
22+
</a>
23+
<a href="?action={{ action }}" title="{{ method }}">authenticated</a>
24+
{% endblocktrans %}
25+
{% else %}
26+
{% blocktrans trimmed with action=log.action method=log.auth_backend_display %}
27+
User <a href="?action={{ action }}" title="{{ method }}">authenticated</a>
28+
{% endblocktrans %}
29+
{% endif %}
30+
{% elif log.action == AuditLog.AUTHN_FAILURE %}
31+
{% blocktrans trimmed with action=log.action method=log.auth_backend_display %}
32+
<a href="?action={{ action }}" title="{{ method }}">Authentication failed</a>
33+
{% endblocktrans %}
34+
{% elif log.action == AuditLog.PAGEVIEW %}
35+
{% if log.log_user_id %}
36+
{% blocktrans trimmed with action=log.action user=log.log_user_username path=log.resource %}
37+
<a href="?user={{ user }}">
38+
<code>{{ user }}</code>
39+
</a>
40+
<a href="?action={{ action }}" title="{{ path }}">visited</a> a page
41+
{% endblocktrans %}
42+
{% else %}
43+
{% blocktrans trimmed with action=log.action user=log.log_user_username path=log.resource %}
44+
A user <a href="?action={{ action }}" title="{{ path }}">visited</a> a page
45+
{% endblocktrans %}
46+
{% endif %}
47+
{% elif log.action == AuditLog.DOWNLOAD %}
48+
{% if log.log_user_id %}
49+
{% blocktrans trimmed with action=log.action user=log.log_user_username path=log.resource %}
50+
<a href="?user={{ user }}">
51+
<code>{{ user }}</code>
52+
</a>
53+
<a href="?action={{ action }}" title="{{ path }}">downloaded</a> a document
54+
{% endblocktrans %}
55+
{% else %}
56+
{% blocktrans trimmed with action=log.action user=log.log_user_username path=log.resource %}
57+
A user <a href="?action={{ action }}" title="{{ path }}">downloaded</a> a document
58+
{% endblocktrans %}
59+
{% endif %}
60+
{% endif %}
61+
62+
{% trans "from" %}
63+
64+
<a href="?ip={{ log.ip }}" title="{{ log.browser }}">
65+
<code>{{ log.ip }}</code>
66+
</a>
67+
68+
{# If the authentication was on a doc domain. #}
69+
{% if log.log_project_id %}
70+
{% trans "on" %}
71+
{% if log.project %}
72+
<a href="{% url 'projects_detail' log.log_project_slug %}">
73+
<code>{{ log.log_project_slug }}</code>
74+
</a>
75+
{% else %}
76+
{# The original project has been deleted, don't link to it. #}
77+
<code title="{{ log.resource }}">{{ log.log_project_slug }}</code>
78+
{% endif %}
79+
{% endif %}
80+
81+
<span class="quiet right" title="{{ log.created|date:"DATETIME_FORMAT" }}">
82+
{% blocktrans trimmed with log.created|timesince as date %}
83+
{{ date }} ago
84+
{% endblocktrans %}
85+
</span>
86+
</li>
87+
{% empty %}
88+
<li class="module-item">
89+
<p class="quiet">
90+
{% trans 'No activity logged yet' %}
91+
</p>
92+
</li>
93+
{% endfor %}
94+
</ul>
95+
</div>
96+
</div>

readthedocs/organizations/templates/organizations/admin/base.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<ul>
1212
<li class="{% block organization-admin-details %}{% endblock %}"><a href="{% url 'organization_edit' organization.slug %}">{% trans "Details" %}</a></li>
1313
<li class="{% block organization-admin-owners %}{% endblock %}"><a href="{% url 'organization_owners' organization.slug %}">{% trans "Owners" %}</a></li>
14+
<li class="{% block organization-admin-security-log %}{% endblock %}"><a href="{% url 'organization_security_log' organization.slug %}">{% trans "Security Log" %}</a></li>
1415
</ul>
1516
<div>
1617
<h2>{% block edit_content_header %}{% endblock %}</h2>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{% extends "organizations/admin/base.html" %}
2+
3+
{% load i18n %}
4+
{% load pagination_tags %}
5+
6+
{% block title %}{% trans "Organization Security Log" %}{% endblock %}
7+
8+
{% block organization-admin-security-log %}active{% endblock %}
9+
10+
{% block edit_content_header %} {% trans "Organization Security Log" %} {% endblock %}
11+
12+
{% block edit_content %}
13+
14+
{% if not enabled %}
15+
{% include 'projects/includes/feature_disabled.html' with organization=organization %}
16+
{% endif %}
17+
18+
<p class="help_text">
19+
{% blocktrans trimmed with docs_url='https://docs.readthedocs.io/page/security-log.html#organization-security-log' %}
20+
The <a href="{{ docs_url }}">organization security log</a> allows you to see what has happened recently in your organization.
21+
{% endblocktrans %}
22+
23+
{% if enabled %}
24+
{% if days_limit and days_limit > 0 %}
25+
{% blocktrans trimmed with days_limit=days_limit %}
26+
Showing logs from the last {{ days_limit }} days.
27+
You can upgrade your plan to increase the time period that is stored.
28+
{% endblocktrans %}
29+
{% else %}
30+
{% trans "Showing logs from all time." %}
31+
{% endif %}
32+
{% endif %}
33+
</p>
34+
35+
{% autopaginate object_list 15 %}
36+
37+
<div class="module">
38+
<div class="button-bar">
39+
<ul>
40+
<li>
41+
<a class="button"
42+
href="?download=true&{{ request.GET.urlencode }}">
43+
{% trans "Download" %}
44+
</a>
45+
</li>
46+
</ul>
47+
</div>
48+
49+
{% include "audit/list_logs.html" with omit_user=False %}
50+
</div>
51+
{% paginate %}
52+
{% endblock %}

0 commit comments

Comments
 (0)