diff --git a/seahub/admin_log/__init__.py b/seahub/admin_log/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/seahub/admin_log/admin.py b/seahub/admin_log/admin.py new file mode 100644 index 0000000000..8c38f3f3da --- /dev/null +++ b/seahub/admin_log/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/seahub/admin_log/migrations/0001_initial.py b/seahub/admin_log/migrations/0001_initial.py new file mode 100644 index 0000000000..60f27e16a4 --- /dev/null +++ b/seahub/admin_log/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import datetime + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='AdminLog', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('email', models.EmailField(max_length=254, db_index=True)), + ('operation', models.CharField(max_length=255, db_index=True)), + ('detail', models.TextField()), + ('datetime', models.DateTimeField(default=datetime.datetime.now)), + ], + options={ + 'ordering': ['-datetime'], + }, + ), + ] diff --git a/seahub/admin_log/migrations/__init__.py b/seahub/admin_log/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/seahub/admin_log/models.py b/seahub/admin_log/models.py new file mode 100644 index 0000000000..1ccf4d4906 --- /dev/null +++ b/seahub/admin_log/models.py @@ -0,0 +1,74 @@ +# Copyright (c) 2012-2017 Seafile Ltd. +import json +import datetime + +from django.db import models +from django.db.models import Q +from django.dispatch import receiver + +from seahub.admin_log.signals import admin_operation + +ADMIN_LOG_OPERATION_TYPE = ('repo_transfer', 'repo_delete', + 'group_create', 'group_transfer', 'group_delete', + 'user_add', 'user_delete') + +## operation: detail + +# 'repo_transfer': {'id': repo_id, 'name': repo_name, 'from': from_user, 'to': to_user} +# 'repo_delete': {'id': repo_id, 'name': repo_name, 'owner': repo_owner} + +# 'group_create': {'id': group_id, 'name': group_name, 'owner': group_owner} +# 'group_transfer': {'id': group_id, 'name': group_name, 'from': from_user, 'to': to_user} +# 'group_delete': {'id': group_id, 'name': group_name, 'owner': group_owner} + +# 'user_add': {'email': new_user} +# 'user_delete': {'email': deleted_user} + + +class AdminLogManager(models.Manager): + + def add_admin_log(self, email, operation, detail): + + model= super(AdminLogManager, self).create( + email=email, operation=operation, detail=detail) + + model.save() + + return model + + def get_admin_logs(self, email=None, operation=None): + + logs = super(AdminLogManager, self).all() + + if email and operation: + filtered_logs = logs.filter(Q(email=email) & Q(operation = operation)) + elif email: + filtered_logs = logs.filter(email=email) + elif operation: + filtered_logs = logs.filter(operation=operation) + else: + filtered_logs = logs + + return filtered_logs + +class AdminLog(models.Model): + email = models.EmailField(db_index=True) + operation = models.CharField(max_length=255, db_index=True) + detail = models.CharField(max_length=255) + datetime = models.DateTimeField(default=datetime.datetime.now) + objects = AdminLogManager() + + class Meta: + ordering = ["-datetime"] + + +###### signal handlers +@receiver(admin_operation) +def admin_operation_cb(sender, **kwargs): + admin_name = kwargs['admin_name'] + operation = kwargs['operation'] + detail = kwargs['detail'] + + detail_json = json.dumps(detail) + AdminLog.objects.add_admin_log(admin_name, + operation, detail_json) diff --git a/seahub/admin_log/signals.py b/seahub/admin_log/signals.py new file mode 100644 index 0000000000..2751f0249a --- /dev/null +++ b/seahub/admin_log/signals.py @@ -0,0 +1,4 @@ +# Copyright (c) 2012-2017 Seafile Ltd. +import django.dispatch + +admin_operation = django.dispatch.Signal(providing_args=["admin_name", "operation", "detail"]) diff --git a/seahub/admin_log/views.py b/seahub/admin_log/views.py new file mode 100644 index 0000000000..91ea44a218 --- /dev/null +++ b/seahub/admin_log/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/seahub/api2/endpoints/admin/groups.py b/seahub/api2/endpoints/admin/groups.py index 2e7a2c66f7..f5c2afafd9 100644 --- a/seahub/api2/endpoints/admin/groups.py +++ b/seahub/api2/endpoints/admin/groups.py @@ -16,7 +16,7 @@ from seahub.utils import is_valid_username from seahub.utils.timeutils import timestamp_to_isoformat_timestr from seahub.group.utils import is_group_member, is_group_admin, \ validate_group_name, check_group_name_conflict - +from seahub.admin_log.signals import admin_operation from seahub.api2.utils import api_error from seahub.api2.throttling import UserRateThrottle from seahub.api2.authentication import TokenAuthentication @@ -129,15 +129,25 @@ class AdminGroups(APIView): return api_error(status.HTTP_404_NOT_FOUND, error_msg) username = request.user.username + new_owner = group_owner or username # create group. try: - group_id = ccnet_api.create_group(group_name, group_owner or username) + group_id = ccnet_api.create_group(group_name, new_owner) except SearpcError as e: logger.error(e) error_msg = 'Internal Server Error' return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + # send admin operation log signal + admin_op_detail = { + "id": group_id, + "name": group_name, + "owner": new_owner, + } + admin_operation.send(sender=None, admin_name=username, + operation='group_create', detail=admin_op_detail) + # get info of new group group_info = get_group_info(group_id) @@ -198,16 +208,32 @@ class AdminGroup(APIView): error_msg = 'Internal Server Error' return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) - group_info = get_group_info(group_id) + # send admin operation log signal + admin_op_detail = { + "id": group_id, + "name": group.group_name, + "from": old_owner, + "to": new_owner, + } + admin_operation.send(sender=None, admin_name=request.user.username, + operation='group_transfer', detail=admin_op_detail) + group_info = get_group_info(group_id) return Response(group_info) def delete(self, request, group_id): """ Dismiss a specific group """ + group_id = int(group_id) + group = ccnet_api.get_group(group_id) + if not group: + return Response({'success': True}) + + group_owner = group.creator_name + group_name = group.group_name + try: - group_id = int(group_id) ccnet_api.remove_group(group_id) seafile_api.remove_group_repos(group_id) except Exception as e: @@ -215,4 +241,13 @@ class AdminGroup(APIView): error_msg = 'Internal Server Error' return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + # send admin operation log signal + admin_op_detail = { + "id": group_id, + "name": group_name, + "owner": group_owner, + } + admin_operation.send(sender=None, admin_name=request.user.username, + operation='group_delete', detail=admin_op_detail) + return Response({'success': True}) diff --git a/seahub/api2/endpoints/admin/libraries.py b/seahub/api2/endpoints/admin/libraries.py index 031d7849f2..1258c6b182 100644 --- a/seahub/api2/endpoints/admin/libraries.py +++ b/seahub/api2/endpoints/admin/libraries.py @@ -17,6 +17,7 @@ from seahub.api2.utils import api_error from seahub.base.accounts import User from seahub.signals import repo_deleted from seahub.views import get_system_default_repo_id +from seahub.admin_log.signals import admin_operation try: from seahub.settings import MULTI_TENANCY @@ -150,24 +151,33 @@ class AdminLibrary(APIView): return api_error(status.HTTP_404_NOT_FOUND, 'Library %s not found.' % repo_id) + repo_name = repo.name repo_owner = seafile_api.get_repo_owner(repo_id) if not repo_owner: repo_owner = seafile_api.get_org_repo_owner(repo_id) - related_usernames = seaserv.get_related_users_by_repo(repo_id) try: seafile_api.remove_repo(repo_id) - repo_deleted.send(sender=None, - org_id=-1, - usernames=related_usernames, - repo_owner=repo_owner, - repo_id=repo_id, - repo_name=repo.name) + related_usernames = seaserv.get_related_users_by_repo(repo_id) + + # send signal for seafevents + repo_deleted.send(sender=None, org_id=-1, usernames=related_usernames, + repo_owner=repo_owner, repo_id=repo_id, repo_name=repo.name) + except Exception as e: logger.error(e) error_msg = 'Internal Server Error' return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + # send admin operation log signal + admin_op_detail = { + "id": repo_id, + "name": repo_name, + "owner": repo_owner, + } + admin_operation.send(sender=None, admin_name=request.user.username, + operation='repo_delete', detail=admin_op_detail) + return Response({'success': True}) def put(self, request, repo_id, format=None): @@ -257,6 +267,16 @@ class AdminLibrary(APIView): break + # send admin operation log signal + admin_op_detail = { + "id": repo_id, + "name": repo.name, + "from": repo_owner, + "to": new_owner, + } + admin_operation.send(sender=None, admin_name=request.user.username, + operation='repo_transfer', detail=admin_op_detail) + repo = seafile_api.get_repo(repo_id) repo_info = get_repo_info(repo) diff --git a/seahub/api2/endpoints/admin/logs.py b/seahub/api2/endpoints/admin/logs.py new file mode 100644 index 0000000000..54d4671771 --- /dev/null +++ b/seahub/api2/endpoints/admin/logs.py @@ -0,0 +1,89 @@ +import json +import logging + +from rest_framework.authentication import SessionAuthentication +from rest_framework.permissions import IsAdminUser +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework import status +from django.core.urlresolvers import reverse + +from seahub.utils.timeutils import datetime_to_isoformat_timestr +from seahub.admin_log.models import AdminLog, ADMIN_LOG_OPERATION_TYPE + +from seahub.api2.permissions import IsProVersion +from seahub.api2.utils import api_error +from seahub.api2.throttling import UserRateThrottle +from seahub.api2.authentication import TokenAuthentication +from seahub.api2.endpoints.admin.utils import generate_links_header_for_paginator + +logger = logging.getLogger(__name__) + +def get_log_info(log_obj): + isoformat_timestr = datetime_to_isoformat_timestr(log_obj.datetime) + log_info = { + "email": log_obj.email, + "operation": log_obj.operation, + "detail": json.loads(log_obj.detail), + "datetime": isoformat_timestr, + } + + return log_info + + +class AdminLogs(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + throttle_classes = (UserRateThrottle,) + permission_classes = (IsAdminUser, IsProVersion) + + def get(self, request): + """ List all logs + + Permission checking: + 1. Admin user; + """ + + email = request.GET.get('email', '') + operation = request.GET.get('operation', '') + if operation: + if operation not in ADMIN_LOG_OPERATION_TYPE: + error_msg = 'operation invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + try: + page = int(request.GET.get('page', '1')) + per_page = int(request.GET.get('per_page', '100')) + except ValueError: + page = 1 + per_page = 100 + + if page <= 0: + error_msg = 'page invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + if per_page <= 0: + error_msg = 'per_page invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # generate data result + data = [] + offset = per_page * (page -1) + total_count = AdminLog.objects.get_admin_logs(email=email, operation=operation).count() + admin_logs = AdminLog.objects.get_admin_logs(email=email, operation=operation)[offset:offset+per_page] + + for log in admin_logs: + log_info = get_log_info(log) + data.append(log_info) + + result = {'data': data, 'total_count': total_count} + resp = Response(result) + + ## generate `Links` header for paginator + options_dict = {'email': email, 'operation': operation} + base_url = reverse('api-v2.1-admin-admin-logs') + links_header = generate_links_header_for_paginator(base_url, + page, per_page, total_count, options_dict) + resp['Links'] = links_header + + return resp diff --git a/seahub/api2/endpoints/admin/utils.py b/seahub/api2/endpoints/admin/utils.py index a6c0f21b38..7fad56356e 100644 --- a/seahub/api2/endpoints/admin/utils.py +++ b/seahub/api2/endpoints/admin/utils.py @@ -2,6 +2,7 @@ import re import datetime import time +import urllib from seahub.utils import get_log_events_by_time @@ -27,3 +28,50 @@ def get_log_events_by_type_and_time(log_type, start, end): events = get_log_events_by_time(log_type, start_timestamp, end_timestamp) events = events if events else [] return events + +def is_first_page(page): + return True if page == 1 else False + +def is_last_page(page, per_page, total_count): + if page * per_page >= total_count: + return True + else: + return False + +def generate_links_header_for_paginator(base_url, page, per_page, total_count, option_dict={}): + + if type(option_dict) is not dict: + return '' + + query_dict = {'page': 1, 'per_page': per_page} + query_dict.update(option_dict) + + # generate first page url + first_page_url = base_url + '?' + urllib.urlencode(query_dict) + + # generate last page url + last_page_query_dict = {'page': (total_count / per_page) + 1} + query_dict.update(last_page_query_dict) + last_page_url = base_url + '?' + urllib.urlencode(query_dict) + + # generate next page url + next_page_query_dict = {'page': page + 1} + query_dict.update(next_page_query_dict) + next_page_url = base_url + '?' + urllib.urlencode(query_dict) + + # generate prev page url + prev_page_query_dict = {'page': page - 1} + query_dict.update(prev_page_query_dict) + prev_page_url = base_url + '?' + urllib.urlencode(query_dict) + + # generate `Links` header + links_header = '' + if is_first_page(page): + links_header = '<%s>; rel="next", <%s>; rel="last"' % (next_page_url, last_page_url) + elif is_last_page(page, per_page, total_count): + links_header = '<%s>; rel="first", <%s>; rel="prev"' % (first_page_url, prev_page_url) + else: + links_header = '<%s>; rel="next", <%s>; rel="last", <%s>; rel="first", <%s>; rel="prev"' % \ + (next_page_url, last_page_url, first_page_url, prev_page_url) + + return links_header diff --git a/seahub/api2/permissions.py b/seahub/api2/permissions.py index e307ed0d4b..a3643918e3 100644 --- a/seahub/api2/permissions.py +++ b/seahub/api2/permissions.py @@ -9,6 +9,8 @@ from django.conf import settings from seaserv import check_permission, is_repo_owner, ccnet_api +from seahub.utils import is_pro_version + SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] class IsRepoWritable(BasePermission): @@ -76,3 +78,11 @@ class CanGenerateUploadLink(BasePermission): """ def has_permission(self, request, *args, **kwargs): return request.user.permissions.can_generate_upload_link() + +class IsProVersion(BasePermission): + """ + Check whether Seafile is pro version + """ + + def has_permission(self, request, *args, **kwargs): + return is_pro_version() diff --git a/seahub/settings.py b/seahub/settings.py index 0b40098109..71edd5bc84 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -214,6 +214,7 @@ INSTALLED_APPS = ( 'seahub.help', 'seahub.thumbnail', 'seahub.password_session', + 'seahub.admin_log', ) # Enabled or disable constance(web settings). diff --git a/seahub/templates/js/sysadmin-templates.html b/seahub/templates/js/sysadmin-templates.html index 7570013fd6..d727dd2581 100644 --- a/seahub/templates/js/sysadmin-templates.html +++ b/seahub/templates/js/sysadmin-templates.html @@ -67,6 +67,10 @@ {% trans "Terms and Conditions" %} {% endif %} +