1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-08-03 00:00:10 +00:00

add file access log web api (#6648)

This commit is contained in:
lian 2024-08-30 07:01:52 +08:00 committed by GitHub
parent fa6427f9dc
commit 1d785cf824
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 193 additions and 217 deletions

View File

@ -0,0 +1,108 @@
import logging
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework.authentication import SessionAuthentication
from seaserv import seafile_api
from seahub.api2.utils import api_error
from seahub.api2.throttling import UserRateThrottle
from seahub.api2.authentication import TokenAuthentication
from seahub.views import check_folder_permission
from seahub.avatar.templatetags.avatar_tags import api_avatar_url
from seahub.base.templatetags.seahub_tags import email2nickname, \
email2contact_email
from seahub.utils.timeutils import datetime_to_isoformat_timestr
from seahub.utils import is_org_context, is_pro_version, \
FILE_AUDIT_ENABLED, get_file_audit_events_by_path, \
generate_file_audit_event_type
logger = logging.getLogger(__name__)
class FileAccessLogView(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
throttle_classes = (UserRateThrottle,)
def get(self, request, repo_id):
""" Get file access log.
"""
if not is_pro_version() or not FILE_AUDIT_ENABLED:
error_msg = 'feature is not enabled.'
return api_error(501, error_msg)
# argument check
path = request.GET.get('path', '')
if not path:
error_msg = 'path invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
# resource check
repo = seafile_api.get_repo(repo_id)
if not repo:
error_msg = 'Library %s not found.' % repo_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
if not seafile_api.get_file_id_by_path(repo_id, path):
error_msg = 'File %s not found.' % path
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# permission check
if not check_folder_permission(request, repo_id, '/'):
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
# get access log
try:
avatar_size = int(request.GET.get('avatar_size', 32))
except ValueError:
avatar_size = 32
try:
current_page = int(request.GET.get('page', '1'))
per_page = int(request.GET.get('per_page', '25'))
except ValueError:
current_page = 1
per_page = 25
offset = per_page * (current_page - 1)
org_id = 0
if is_org_context(request):
org_id = request.user.org.org_id
events = get_file_audit_events_by_path(None, org_id, repo_id, path,
offset, per_page)
result = []
for event in events:
info = {}
username = event.user
url, is_default, date_uploaded = api_avatar_url(username,
avatar_size)
info['avatar_url'] = url
info['email'] = username
info['name'] = email2nickname(username)
info['contact_email'] = email2contact_email(username)
info['ip'] = event.ip
info['time'] = datetime_to_isoformat_timestr(event.timestamp)
# type could be: 'web', 'share-link', 'API', 'download-sync',
# 'upload-sync', 'seadrive-download'
event_type, device = generate_file_audit_event_type(event)
info['etype'] = event_type
info['device'] = device
result.append(info)
return Response({'data': result})

View File

@ -1,86 +0,0 @@
{% extends 'base_wide_page.html' %}
{% load seahub_tags avatar_tags i18n %}
{% block sub_title %}{% trans "Access Log" %} - {% endblock %}
{% block wide_page_content %}
<h2 class="file-access-hd"><span class="op-target">{{ filename }}</span> {% trans "Access Log" %}</h2>
<div class="file-audit-list-topbar">
<p class="path">
{% trans 'Current Path:' %}
{% for name, link in zipped %}
{% if not forloop.last %}
<a href="{% url 'lib_view' repo.id repo.name link|strip_slash %}">{{ name }}</a> /
{% else %}
<a href="{% url 'view_lib_file' repo.id path %}" target="_blank" >{{ name }}</a>
{% endif %}
{% endfor %}
</p>
</div>
{% if events %}
<table class='file-audit-list'>
<tr>
<th width="25%" class="user">{% trans "User" %}</th>
<th width="20%">{% trans "Type" %}</th>
<th width="30%">{% trans "IP" %} / {% trans "Device Name" %}</th>
<th width="25%">{% trans "Date" %}</th>
</tr>
{% for e in events %}
<tr>
<td class="user">
{% avatar e.user 16 %}
{% if e.user %}
<a href="{% url 'user_profile' e.user %}">{{ e.user|email2nickname }}</a>
{% else %}
<span>{% trans "Anonymous User" %}</span>
{% endif %}
</td>
<td><span>{{ e.event_type }}</span></td>
<td>
{% if e.show_device %}
{{ e.ip }} / {{ e.show_device }}
{% else %}
{{ e.ip }}
{% endif %}
</td>
<td>{{ e.time|date:"Y-m-d G:i:s" }}</td>
</tr>
{% endfor %}
</table>
<div id="paginator">
{% if current_page != 1 %}
<a href="?page={{ prev_page }}&per_page={{ per_page }}{{ extra_href }}">{% trans "Previous" %}</a>
{% endif %}
{% if page_next %}
<a href="?page={{ next_page }}&per_page={{ per_page }}{{ extra_href }}">{% trans "Next" %}</a>
{% endif %}
{% if current_page != 1 or page_next %}
|
{% endif %}
<span>{% trans "Per page: " %}</span>
{% if per_page == 25 %}
<span> 25 </span>
{% else %}
<a href="?per_page=25{{ extra_href }}" class="per-page">25</a>
{% endif %}
{% if per_page == 50 %}
<span> 50 </span>
{% else %}
<a href="?per_page=50{{ extra_href }}" class="per-page">50</a>
{% endif %}
{% if per_page == 100 %}
<span> 100 </span>
{% else %}
<a href="?per_page=100{{ extra_href }}" class="per-page">100</a>
{% endif %}
</div>
{% else %}
<div class="empty-tips">
<h2 class="alc">{% trans "This file has (apparently) not been accessed yet" %}</h2>
</div>
{% endif %}
{% endblock %}

View File

@ -15,7 +15,7 @@ from seahub.views.sso_to_thirdpart import sso_to_thirdpart
from seahub.views.file import view_history_file, view_trash_file,\
view_snapshot_file, view_shared_file, view_file_via_shared_dir,\
text_diff, view_raw_file, download_file, view_lib_file, \
file_access, view_lib_file_via_smart_link, view_media_file_via_share_link, \
view_lib_file_via_smart_link, view_media_file_via_share_link, \
view_media_file_via_public_wiki, view_sdoc_revision
from seahub.views.repo import repo_history_view, repo_snapshot, view_shared_dir, \
view_shared_upload_link, view_lib_as_wiki
@ -62,6 +62,7 @@ from seahub.api2.endpoints.repos_batch import ReposBatchView, \
ReposBatchDeleteItemView
from seahub.api2.endpoints.repos import RepoView, ReposView, RepoShareInfoView
from seahub.api2.endpoints.file import FileView
from seahub.api2.endpoints.file_access_log import FileAccessLogView
from seahub.api2.endpoints.file_history import FileHistoryView, NewFileHistoryView
from seahub.api2.endpoints.dir import DirView, DirDetailView
from seahub.api2.endpoints.file_tag import FileTagView
@ -240,7 +241,6 @@ urlpatterns = [
re_path(r'^repo/sdoc_revision/(?P<repo_id>[-0-9a-f]{36})/$', sdoc_revision, name='sdoc_revision'),
re_path(r'^repo/sdoc_revisions/(?P<repo_id>[-0-9a-f]{36})/$', sdoc_revisions, name='sdoc_revisions'),
re_path(r'^repo/sdoc_export_to_docx/(?P<repo_id>[-0-9a-f]{36})/$', sdoc_to_docx, name='sdoc_export_to_docx'),
re_path(r'^repo/file-access/(?P<repo_id>[-0-9a-f]{36})/$', file_access, name='file_access'),
re_path(r'^repo/text_diff/(?P<repo_id>[-0-9a-f]{36})/$', text_diff, name='text_diff'),
re_path(r'^repo/history/(?P<repo_id>[-0-9a-f]{36})/$', repo_history, name='repo_history'),
re_path(r'^repo/history/view/(?P<repo_id>[-0-9a-f]{36})/$', repo_history_view, name='repo_history_view'),
@ -429,6 +429,7 @@ urlpatterns = [
re_path(r'^api/v2.1/repos/$', ReposView.as_view(), name='api-v2.1-repos-view'),
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/$', RepoView.as_view(), name='api-v2.1-repo-view'),
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/file/$', FileView.as_view(), name='api-v2.1-file-view'),
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/file/access-log/$', FileAccessLogView.as_view(), name='api-v2.1-file-access-log-view'),
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/file/history/$', FileHistoryView.as_view(), name='api-v2.1-file-history-view'),
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/file/new_history/$', NewFileHistoryView.as_view(), name='api-v2.1-new-file-history-view'),
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/dir/$', DirView.as_view(), name='api-v2.1-dir-view'),

View File

@ -688,7 +688,7 @@ if EVENTS_CONFIG_FILE:
events = seafevents_api.get_file_audit_events_by_path(session,
email, org_id, repo_id, file_path, start, limit)
return events if events else None
return events if events else []
def get_file_audit_events(email, org_id, repo_id, start, limit):
"""Return file audit events list. (If no file audit, return 'None')

View File

@ -53,7 +53,6 @@ from seahub.utils import render_error, is_org_context, \
get_file_type_and_ext, gen_file_get_url, gen_file_share_link, \
render_permission_error, is_pro_version, is_textual_file, \
EMPTY_SHA1, HtmlDiff, gen_inner_file_get_url, \
get_file_audit_events_by_path, \
generate_file_audit_event_type, FILE_AUDIT_ENABLED, \
get_conf_text_ext, HAS_OFFICE_CONVERTER, PREVIEW_FILEEXT, \
normalize_file_path, get_service_url, OFFICE_PREVIEW_MAX_SIZE, \
@ -1932,84 +1931,6 @@ def office_convert_get_page(request, repo_id, commit_id, path, filename):
resp['Content-Type'] = content_type
return resp
@login_required
def file_access(request, repo_id):
"""List file access log.
"""
if not is_pro_version() or not FILE_AUDIT_ENABLED:
raise Http404
referer = request.headers.get('referer', None)
next_page = settings.SITE_ROOT if referer is None else referer
repo = get_repo(repo_id)
if not repo:
messages.error(request, _("Library does not exist"))
return HttpResponseRedirect(next_page)
path = request.GET.get('p', None)
if not path:
messages.error(request, _("Argument missing"))
return HttpResponseRedirect(next_page)
if not seafile_api.get_file_id_by_path(repo_id, path):
messages.error(request, _("File does not exist"))
return HttpResponseRedirect(next_page)
# perm check
if check_folder_permission(request, repo_id, path) != 'rw':
messages.error(request, _("Permission denied"))
return HttpResponseRedirect(next_page)
# Make sure page request is an int. If not, deliver first page.
try:
current_page = int(request.GET.get('page', '1'))
per_page = int(request.GET.get('per_page', '100'))
except ValueError:
current_page = 1
per_page = 100
start = per_page * (current_page - 1)
limit = per_page + 1
if is_org_context(request):
org_id = request.user.org.org_id
events = get_file_audit_events_by_path(None, org_id, repo_id, path, start, limit)
else:
events = get_file_audit_events_by_path(None, 0, repo_id, path, start, limit)
events = events if events else []
if len(events) == per_page + 1:
page_next = True
else:
page_next = False
events = events[:per_page]
for ev in events:
ev.repo = get_repo(ev.repo_id)
ev.filename = os.path.basename(ev.file_path)
ev.time = utc_to_local(ev.timestamp)
ev.event_type, ev.show_device = generate_file_audit_event_type(ev)
filename = os.path.basename(path)
zipped = gen_path_link(path, repo.name)
extra_href = "&p=%s" % quote(path)
return render(request, 'file_access.html', {
'repo': repo,
'path': path,
'filename': filename,
'zipped': zipped,
'events': events,
'extra_href': extra_href,
'current_page': current_page,
'prev_page': current_page-1,
'next_page': current_page+1,
'per_page': per_page,
'page_next': page_next,
})
def view_media_file_via_share_link(request):
image_path = request.GET.get('path', '')

View File

@ -6,7 +6,9 @@ from seahub.test_utils import BaseTestCase
import datetime
class FileTest(BaseTestCase):
def setUp(self):
self.login_as(self.user)
self.video = self.create_file(repo_id=self.repo.id,
@ -103,6 +105,7 @@ class FileTest(BaseTestCase):
class FileAccessLogTest(BaseTestCase):
def setUp(self):
self.login_as(self.user)
self.file_path = self.file
@ -111,6 +114,15 @@ class FileAccessLogTest(BaseTestCase):
def tearDown(self):
self.remove_repo()
def get_type(self, etype):
return {
'file-download-web': 'web',
'file-download-share-link': 'share-link',
'file-download-api': 'API',
'repo-download-sync': 'download-sync',
'repo-upload-sync': 'upload-sync',
}[etype]
def generate_file_audit_event_type(self, e):
return {
'file-download-web': ('web', ''),
@ -120,21 +132,24 @@ class FileAccessLogTest(BaseTestCase):
'repo-upload-sync': ('upload-sync', e.device),
}[e.etype]
@patch('seahub.views.file.is_pro_version')
@patch('seahub.api2.endpoints.file_access_log.is_pro_version')
def test_can_not_render_if_not_pro(self, mock_is_pro_version):
mock_is_pro_version.return_value = False
url = reverse('file_access', args=[self.repo_id]) + '?p=' + self.file_path
url = reverse('api-v2.1-file-access-log-view',
args=[self.repo_id]) + '?path=' + self.file_path
resp = self.client.get(url)
self.assertEqual(404, resp.status_code)
self.assertEqual(501, resp.status_code)
@patch('seahub.views.file.generate_file_audit_event_type')
@patch('seahub.views.file.get_file_audit_events_by_path')
@patch('seahub.views.file.is_pro_version')
@patch('seahub.views.file.FILE_AUDIT_ENABLED')
def test_can_show_web_type(self, mock_file_audit_enabled, mock_is_pro_version,
mock_get_file_audit_events_by_path, mock_generate_file_audit_event_type):
@patch('seahub.api2.endpoints.file_access_log.generate_file_audit_event_type')
@patch('seahub.api2.endpoints.file_access_log.get_file_audit_events_by_path')
@patch('seahub.api2.endpoints.file_access_log.is_pro_version')
@patch('seahub.api2.endpoints.file_access_log.FILE_AUDIT_ENABLED')
def test_can_show_web_type(self,
mock_file_audit_enabled,
mock_is_pro_version,
mock_get_file_audit_events_by_path,
mock_generate_file_audit_event_type):
etype = 'file-download-web'
event = Event(self.user.email, self.repo_id, self.file_path, etype)
@ -144,18 +159,22 @@ class FileAccessLogTest(BaseTestCase):
mock_get_file_audit_events_by_path.return_value = [event]
mock_generate_file_audit_event_type.side_effect = self.generate_file_audit_event_type
url = reverse('file_access', args=[self.repo_id]) + '?p=' + self.file_path
url = reverse('api-v2.1-file-access-log-view',
args=[self.repo_id]) + '?path=' + self.file_path
resp = self.client.get(url)
self.assertEqual(200, resp.status_code)
self.assertTemplateUsed(resp, 'file_access.html')
self.assertContains(resp, 'web')
for access_log in resp.data.get('data'):
self.assertEqual(self.get_type(etype), access_log['etype'])
@patch('seahub.views.file.generate_file_audit_event_type')
@patch('seahub.views.file.get_file_audit_events_by_path')
@patch('seahub.views.file.is_pro_version')
@patch('seahub.views.file.FILE_AUDIT_ENABLED')
def test_can_show_share_link_type(self, mock_file_audit_enabled, mock_is_pro_version,
mock_get_file_audit_events_by_path, mock_generate_file_audit_event_type):
@patch('seahub.api2.endpoints.file_access_log.generate_file_audit_event_type')
@patch('seahub.api2.endpoints.file_access_log.get_file_audit_events_by_path')
@patch('seahub.api2.endpoints.file_access_log.is_pro_version')
@patch('seahub.api2.endpoints.file_access_log.FILE_AUDIT_ENABLED')
def test_can_show_share_link_type(self,
mock_file_audit_enabled,
mock_is_pro_version,
mock_get_file_audit_events_by_path,
mock_generate_file_audit_event_type):
etype = 'file-download-share-link'
event = Event(self.user.email, self.repo_id, self.file_path, etype)
@ -165,18 +184,22 @@ class FileAccessLogTest(BaseTestCase):
mock_get_file_audit_events_by_path.return_value = [event]
mock_generate_file_audit_event_type.side_effect = self.generate_file_audit_event_type
url = reverse('file_access', args=[self.repo_id]) + '?p=' + self.file_path
url = reverse('api-v2.1-file-access-log-view',
args=[self.repo_id]) + '?path=' + self.file_path
resp = self.client.get(url)
self.assertEqual(200, resp.status_code)
self.assertTemplateUsed(resp, 'file_access.html')
self.assertContains(resp, 'share-link')
for access_log in resp.data.get('data'):
self.assertEqual(self.get_type(etype), access_log['etype'])
@patch('seahub.views.file.generate_file_audit_event_type')
@patch('seahub.views.file.get_file_audit_events_by_path')
@patch('seahub.views.file.is_pro_version')
@patch('seahub.views.file.FILE_AUDIT_ENABLED')
def test_can_show_api_type(self, mock_file_audit_enabled, mock_is_pro_version,
mock_get_file_audit_events_by_path, mock_generate_file_audit_event_type):
@patch('seahub.api2.endpoints.file_access_log.generate_file_audit_event_type')
@patch('seahub.api2.endpoints.file_access_log.get_file_audit_events_by_path')
@patch('seahub.api2.endpoints.file_access_log.is_pro_version')
@patch('seahub.api2.endpoints.file_access_log.FILE_AUDIT_ENABLED')
def test_can_show_api_type(self,
mock_file_audit_enabled,
mock_is_pro_version,
mock_get_file_audit_events_by_path,
mock_generate_file_audit_event_type):
etype = 'file-download-api'
event = Event(self.user.email, self.repo_id, self.file_path, etype)
@ -186,18 +209,22 @@ class FileAccessLogTest(BaseTestCase):
mock_get_file_audit_events_by_path.return_value = [event]
mock_generate_file_audit_event_type.side_effect = self.generate_file_audit_event_type
url = reverse('file_access', args=[self.repo_id]) + '?p=' + self.file_path
url = reverse('api-v2.1-file-access-log-view',
args=[self.repo_id]) + '?path=' + self.file_path
resp = self.client.get(url)
self.assertEqual(200, resp.status_code)
self.assertTemplateUsed(resp, 'file_access.html')
self.assertContains(resp, 'API')
for access_log in resp.data.get('data'):
self.assertEqual(self.get_type(etype), access_log['etype'])
@patch('seahub.views.file.generate_file_audit_event_type')
@patch('seahub.views.file.get_file_audit_events_by_path')
@patch('seahub.views.file.is_pro_version')
@patch('seahub.views.file.FILE_AUDIT_ENABLED')
def test_can_show_download_sync_type(self, mock_file_audit_enabled, mock_is_pro_version,
mock_get_file_audit_events_by_path, mock_generate_file_audit_event_type):
@patch('seahub.api2.endpoints.file_access_log.generate_file_audit_event_type')
@patch('seahub.api2.endpoints.file_access_log.get_file_audit_events_by_path')
@patch('seahub.api2.endpoints.file_access_log.is_pro_version')
@patch('seahub.api2.endpoints.file_access_log.FILE_AUDIT_ENABLED')
def test_can_show_download_sync_type(self,
mock_file_audit_enabled,
mock_is_pro_version,
mock_get_file_audit_events_by_path,
mock_generate_file_audit_event_type):
etype = 'repo-download-sync'
event = Event(self.user.email, self.repo_id, self.file_path, etype)
@ -207,18 +234,22 @@ class FileAccessLogTest(BaseTestCase):
mock_get_file_audit_events_by_path.return_value = [event]
mock_generate_file_audit_event_type.side_effect = self.generate_file_audit_event_type
url = reverse('file_access', args=[self.repo_id]) + '?p=' + self.file_path
url = reverse('api-v2.1-file-access-log-view',
args=[self.repo_id]) + '?path=' + self.file_path
resp = self.client.get(url)
self.assertEqual(200, resp.status_code)
self.assertTemplateUsed(resp, 'file_access.html')
self.assertContains(resp, 'download-sync')
for access_log in resp.data.get('data'):
self.assertEqual(self.get_type(etype), access_log['etype'])
@patch('seahub.views.file.generate_file_audit_event_type')
@patch('seahub.views.file.get_file_audit_events_by_path')
@patch('seahub.views.file.is_pro_version')
@patch('seahub.views.file.FILE_AUDIT_ENABLED')
def test_can_show_upload_sync_type(self, mock_file_audit_enabled, mock_is_pro_version,
mock_get_file_audit_events_by_path, mock_generate_file_audit_event_type):
@patch('seahub.api2.endpoints.file_access_log.generate_file_audit_event_type')
@patch('seahub.api2.endpoints.file_access_log.get_file_audit_events_by_path')
@patch('seahub.api2.endpoints.file_access_log.is_pro_version')
@patch('seahub.api2.endpoints.file_access_log.FILE_AUDIT_ENABLED')
def test_can_show_upload_sync_type(self,
mock_file_audit_enabled,
mock_is_pro_version,
mock_get_file_audit_events_by_path,
mock_generate_file_audit_event_type):
etype = 'repo-upload-sync'
event = Event(self.user.email, self.repo_id, self.file_path, etype)
@ -228,11 +259,12 @@ class FileAccessLogTest(BaseTestCase):
mock_get_file_audit_events_by_path.return_value = [event]
mock_generate_file_audit_event_type.side_effect = self.generate_file_audit_event_type
url = reverse('file_access', args=[self.repo_id]) + '?p=' + self.file_path
url = reverse('api-v2.1-file-access-log-view',
args=[self.repo_id]) + '?path=' + self.file_path
resp = self.client.get(url)
self.assertEqual(200, resp.status_code)
self.assertTemplateUsed(resp, 'file_access.html')
self.assertContains(resp, 'upload-sync')
for access_log in resp.data.get('data'):
self.assertEqual(self.get_type(etype), access_log['etype'])
class Event(object):