1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-07-31 22:57:47 +00:00

Update transfer repo add audit log (#7480)

* add admin log

* update

* update

* update

* add org transfer log

* optimize

* Update file-transfer-log.js

* add db_index

* add-operator

* update

* file --> repo

* Update mysql.sql

* update

* update

---------

Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com>
Co-authored-by: r350178982 <32759763+r350178982@users.noreply.github.com>
This commit is contained in:
awu0403 2025-02-21 18:12:25 +08:00 committed by GitHub
parent 83d615d3ed
commit d91cdad79e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 825 additions and 11 deletions

View File

@ -0,0 +1,29 @@
import { lang } from '../utils/constants';
import dayjs from 'dayjs';
dayjs.locale(lang);
class OrgLogsFileTransferEvent {
constructor(object) {
this.from_group_id = object.from_group_id;
this.from_group_name = object.from_group_name;
this.from_type = object.from_type;
this.from_user_contact_email = object.from_user_contact_email;
this.from_user_email = object.from_user_email;
this.from_user_name = object.from_user_name;
this.repo_id = object.repo_id;
this.repo_name = object.repo_name;
this.to_group_id = object.to_group_id;
this.to_group_name = object.to_group_name;
this.to_type = object.to_type;
this.to_user_contact_email = object.to_user_contact_email;
this.to_user_email = object.to_user_email;
this.to_user_name = object.to_user_name;
this.operator_email = object.operator_email;
this.operator_name = object.operator_name;
this.operator_contact_email = object.operator_contact_email;
this.time = dayjs(object.date).format('YYYY-MM-DD HH:mm:ss');
}
}
export default OrgLogsFileTransferEvent;

View File

@ -37,6 +37,7 @@ import OrgLogs from './org-logs';
import OrgLogsFileAudit from './org-logs-file-audit';
import OrgLogsFileUpdate from './org-logs-file-update';
import OrgLogsPermAudit from './org-logs-perm-audit';
import OrgLogsFileTransfer from './org-logs-file-transfer';
import OrgSAMLConfig from './org-saml-config';
import OrgSubscription from './org-subscription';
@ -126,6 +127,7 @@ class Org extends React.Component {
<OrgLogsFileAudit path='/' />
<OrgLogsFileUpdate path='file-update' />
<OrgLogsPermAudit path='perm-audit' />
<OrgLogsFileTransfer path='repo-transfer' />
</OrgLogs>
{enableMultiADFS &&
<OrgSAMLConfig path={siteRoot + 'org/samlconfig/'}/>

View File

@ -215,7 +215,7 @@ class RepoItem extends React.Component {
onTransferRepo = (email, reshare) => {
let repo = this.props.repo;
orgAdminAPI.orgAdminTransferOrgRepo(orgID, repo.repoID, email, reshare).then(res => {
const { owner_name, owner_email } = res;
const { owner_name, owner_email } = res.data;
this.props.transferRepoItem(repo.repoID, owner_name, owner_email);
let msg = gettext('Successfully transferred the library.');
toaster.success(msg);

View File

@ -0,0 +1,178 @@
import React from 'react';
import PropTypes from 'prop-types';
import dayjs from 'dayjs';
import { orgAdminAPI } from '../../utils/org-admin-api';
import { siteRoot, gettext, lang } from '../../utils/constants';
import { Utils } from '../../utils/utils';
import toaster from '../../components/toast';
import OrgLogsFileTransferEvent from '../../models/org-logs-file-transfer';
import '../../css/org-logs.css';
import UserLink from './user-link';
import { Link } from '@gatsbyjs/reach-router';
dayjs.locale(lang);
class OrgLogsFileTransfer extends React.Component {
constructor(props) {
super(props);
this.state = {
page: 1,
perPage: 25,
pageNext: false,
eventList: [],
isItemFreezed: false
};
}
componentDidMount() {
let page = this.state.page;
let perPage = this.state.perPage;
this.initData(page, perPage);
}
initData = (page, perPage) => {
orgAdminAPI.orgAdminListFileTransfer(page, perPage).then(res => {
let eventList = res.data.log_list.map(item => {
return new OrgLogsFileTransferEvent(item);
});
this.setState({
eventList: eventList,
pageNext: res.data.page_next,
page: res.data.page,
});
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
onChangePageNum = (e, num) => {
e.preventDefault();
let page = this.state.page;
let perPage = this.state.perPage;
if (num == 1) {
page = page + 1;
} else {
page = page - 1;
}
this.initData(page, perPage);
};
render() {
let eventList = this.state.eventList;
return (
<div className="cur-view-content">
<table>
<thead>
<tr>
<th width="20%">{gettext('Transfer From')}</th>
<th width="20%">{gettext('Transfer To')}</th>
<th width="20%">{gettext('Operator')}</th>
<th width="25%">{gettext('Library')}</th>
<th width="15%">{gettext('Date')}</th>
</tr>
</thead>
<tbody>
{eventList.map((item, index) => {
return (
<FileTransferItem
key={index}
fileEvent={item}
isItemFreezed={this.state.isItemFreezed}
/>
);
})}
</tbody>
</table>
<div className="paginator">
{this.state.page != 1 && <a href="#" onClick={(e) => this.onChangePageNum(e, -1)}>{gettext('Previous')}</a>}
{(this.state.page != 1 && this.state.pageNext) && <span> | </span>}
{this.state.pageNext && <a href="#" onClick={(e) => this.onChangePageNum(e, 1)}>{gettext('Next')}</a>}
</div>
</div>
);
}
}
const propTypes = {
isItemFreezed: PropTypes.bool.isRequired,
fileEvent: PropTypes.object.isRequired,
};
class FileTransferItem extends React.Component {
constructor(props) {
super(props);
this.state = {
highlight: false,
showMenu: false,
isItemMenuShow: false,
};
}
onMouseEnter = () => {
if (!this.props.isItemFreezed) {
this.setState({
showMenu: true,
highlight: true,
});
}
};
onMouseLeave = () => {
if (!this.props.isItemFreezed) {
this.setState({
showMenu: false,
highlight: false
});
}
};
getTransferTo = (item) => {
switch (item.to_type) {
case 'user':
return <UserLink email={item.to_user_email} name={item.to_user_name} />;
case 'group':
return <Link to={`${siteRoot}org/groupadmin/${item.to_group_id}/`}>{item.to_group_name}</Link>;
default:
return gettext('Deleted');
}
};
getTransferFrom = (item) => {
switch (item.from_type) {
case 'user':
return <UserLink email={item.from_user_email} name={item.from_user_name} />;
case 'group':
return <Link to={`${siteRoot}org/groupadmin/${item.from_group_id}/`}>{item.from_group_name}</Link>;
default:
return gettext('Deleted');
}
};
getOperator = (item) => {
return <UserLink email={item.operator_email} name={item.operator_name} />;
};
render() {
let { fileEvent } = this.props;
return (
<tr className={this.state.highlight ? 'tr-highlight' : ''}
onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<td>{this.getTransferFrom(fileEvent)}</td>
<td>{this.getTransferTo(fileEvent)}</td>
<td>{this.getOperator(fileEvent)}</td>
<td>{fileEvent.repo_name ? fileEvent.repo_name : gettext('Deleted')}</td>
<td>{dayjs(fileEvent.time).fromNow()}</td>
</tr>
);
}
}
FileTransferItem.propTypes = propTypes;
export default OrgLogsFileTransfer;

View File

@ -40,9 +40,13 @@ class OrgLogs extends Component {
const { isExportExcelDialogOpen, logType } = this.state;
return (
<Fragment>
<MainPanelTopbar>
<Button className="btn btn-secondary operation-item" onClick={this.toggleExportExcelDialog}>{gettext('Export Excel')}</Button>
</MainPanelTopbar>
{this.props.currentTab === 'repo-transfer' ?
<MainPanelTopbar />
:
<MainPanelTopbar>
<Button className="btn btn-secondary operation-item" onClick={this.toggleExportExcelDialog}>{gettext('Export Excel')}</Button>
</MainPanelTopbar>
}
<div className="main-panel-center flex-row">
<div className="cur-view-container h-100">
<div className="cur-view-path org-user-nav">
@ -65,6 +69,12 @@ class OrgLogs extends Component {
to={siteRoot + 'org/logadmin/perm-audit/'} title={gettext('Permission')}>{gettext('Permission')}
</Link>
</li>
<li className="nav-item" onClick={() => this.tabItemClick('repo-transfer')}>
<Link
className={`nav-link ${this.props.currentTab === 'repo-transfer' ? 'active' : ''}`}
to={siteRoot + 'org/logadmin/repo-transfer/'} title={gettext('Repo Transfer')}>{gettext('Repo Transfer')}
</Link>
</li>
</ul>
</div>
<div className="h-100 o-auto">

View File

@ -67,6 +67,7 @@ import LoginLogs from './logs-page/login-logs';
import FileAccessLogs from './logs-page/file-access-logs';
import FileUpdateLogs from './logs-page/file-update-logs';
import SharePermissionLogs from './logs-page/share-permission-logs';
import FIleTransferLogs from './logs-page/file-transfer-log';
import WebSettings from './web-settings/web-settings';
import Notifications from './notifications/notifications';
@ -251,6 +252,7 @@ class SysAdmin extends React.Component {
<InstitutionAdmins path={siteRoot + 'sys/institutions/:institutionID/admins'} {...commonProps} />
<LoginLogs path={siteRoot + 'sys/logs/login'} {...commonProps} />
<FileAccessLogs path={siteRoot + 'sys/logs/file-access'} {...commonProps} />
<FIleTransferLogs path={siteRoot + 'sys/logs/repo-transfer'} {...commonProps} />
<FileUpdateLogs path={siteRoot + 'sys/logs/file-update'} {...commonProps} />
<SharePermissionLogs path={siteRoot + 'sys/logs/share-permission'} {...commonProps} />
<AdminOperationLogs path={siteRoot + 'sys/admin-logs/operation'} {...commonProps} />

View File

@ -0,0 +1,230 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { Link } from '@gatsbyjs/reach-router';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { seafileAPI } from '../../../utils/seafile-api';
import { gettext, siteRoot } from '../../../utils/constants';
import { Utils } from '../../../utils/utils';
import EmptyTip from '../../../components/empty-tip';
import Loading from '../../../components/loading';
import Paginator from '../../../components/paginator';
import MainPanelTopbar from '../main-panel-topbar';
import UserLink from '../user-link';
import LogsNav from './logs-nav';
dayjs.extend(relativeTime);
class Content extends Component {
getPreviousPage = () => {
this.props.getLogsByPage(this.props.currentPage - 1);
};
getNextPage = () => {
this.props.getLogsByPage(this.props.currentPage + 1);
};
render() {
const { loading, errorMsg, items, perPage, currentPage, hasNextPage } = this.props;
if (loading) {
return <Loading />;
} else if (errorMsg) {
return <p className="error text-center">{errorMsg}</p>;
} else {
const emptyTip = (
<EmptyTip text={gettext('No transfer logs')}>
</EmptyTip>
);
const table = (
<Fragment>
<table className="table-hover">
<thead>
<tr>
<th width="20%">{gettext('Transfer From')}</th>
<th width="20%">{gettext('Transfer To')}</th>
<th width="20%">{gettext('Operator')}</th>
<th width="25%">{gettext('Library')}</th>
<th width="15%">{gettext('Date')}</th>
</tr>
</thead>
{items &&
<tbody>
{items.map((item, index) => {
return (<Item
key={index}
item={item}
/>);
})}
</tbody>
}
</table>
<Paginator
gotoPreviousPage={this.getPreviousPage}
gotoNextPage={this.getNextPage}
currentPage={currentPage}
hasNextPage={hasNextPage}
curPerPage={perPage}
resetPerPage={this.props.resetPerPage}
/>
</Fragment>
);
return items.length ? table : emptyTip;
}
}
}
Content.propTypes = {
loading: PropTypes.bool.isRequired,
errorMsg: PropTypes.string.isRequired,
items: PropTypes.array.isRequired,
getLogsByPage: PropTypes.func,
resetPerPage: PropTypes.func,
currentPage: PropTypes.number,
perPage: PropTypes.number,
pageInfo: PropTypes.object,
hasNextPage: PropTypes.bool,
};
class Item extends Component {
constructor(props) {
super(props);
this.state = {
isOpIconShown: false,
};
}
handleMouseOver = () => {
this.setState({
isOpIconShown: true
});
};
handleMouseOut = () => {
this.setState({
isOpIconShown: false
});
};
getTransferTo = (item) => {
switch (item.to_type) {
case 'user':
return <UserLink email={item.to_user_email} name={item.to_user_name} />;
case 'group':
return <Link to={`${siteRoot}sys/groups/${item.to_group_id}/libraries/`}>{item.to_group_name}</Link>;
default:
return gettext('Deleted');
}
};
getTransferFrom = (item) => {
switch (item.from_type) {
case 'user':
return <UserLink email={item.from_user_email} name={item.from_user_name} />;
case 'group':
return <Link to={`${siteRoot}sys/groups/${item.from_group_id}/libraries/`}>{item.from_group_name}</Link>;
default:
return gettext('Deleted');
}
};
getOperator = (item) => {
return <UserLink email={item.operator_email} name={item.operator_name} />;
};
render() {
let { item } = this.props;
return (
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
<td>{this.getTransferFrom(item)}</td>
<td>{this.getTransferTo(item)}</td>
<td>{this.getOperator(item)}</td>
<td>{item.repo_name ? item.repo_name : gettext('Deleted')}</td>
<td>{dayjs(item.date).fromNow()}</td>
</tr>
);
}
}
Item.propTypes = {
item: PropTypes.object.isRequired,
};
class FIleTransferLogs extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
errorMsg: '',
logList: [],
perPage: 100,
currentPage: 1,
hasNextPage: false,
};
this.initPage = 1;
}
componentDidMount() {
let urlParams = (new URL(window.location)).searchParams;
const { currentPage, perPage } = this.state;
this.setState({
perPage: parseInt(urlParams.get('per_page') || perPage),
currentPage: parseInt(urlParams.get('page') || currentPage)
}, () => {
this.getLogsByPage(this.state.currentPage);
});
}
getLogsByPage = (page) => {
let { perPage } = this.state;
seafileAPI.sysAdminListFileTransferLogs(page, perPage).then((res) => {
this.setState({
logList: res.data.repo_transfer_log_list,
loading: false,
currentPage: page,
hasNextPage: res.data.has_next_page,
});
}).catch((error) => {
this.setState({
loading: false,
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
});
});
};
resetPerPage = (newPerPage) => {
this.setState({
perPage: newPerPage,
}, () => this.getLogsByPage(this.initPage));
};
render() {
let { logList, currentPage, perPage, hasNextPage } = this.state;
return (
<Fragment>
<MainPanelTopbar {...this.props} />
<div className="main-panel-center flex-row">
<div className="cur-view-container">
<LogsNav currentItem="fileTransfer" />
<div className="cur-view-content">
<Content
loading={this.state.loading}
errorMsg={this.state.errorMsg}
items={logList}
currentPage={currentPage}
perPage={perPage}
hasNextPage={hasNextPage}
getLogsByPage={this.getLogsByPage}
resetPerPage={this.resetPerPage}
/>
</div>
</div>
</div>
</Fragment>
);
}
}
export default FIleTransferLogs;

View File

@ -16,6 +16,7 @@ class Nav extends React.Component {
{ name: 'fileAccessLogs', urlPart: 'logs/file-access', text: gettext('File Access') },
{ name: 'fileUpdateLogs', urlPart: 'logs/file-update', text: gettext('File Update') },
{ name: 'sharePermissionLogs', urlPart: 'logs/share-permission', text: gettext('Permission') },
{ name: 'fileTransfer', urlPart: 'logs/repo-transfer', text: gettext('Repo Transfer') },
];
}

View File

@ -1,4 +1,4 @@
import { Utils } from "../../utils/utils";
import { Utils } from '../../utils/utils';
describe('getFileExtension', () => {
it('should return the file extension with dot', () => {

View File

@ -481,6 +481,15 @@ class OrgAdminAPI {
}
// org admin logs
orgAdminListFileTransfer(page, perPage) {
let url = this.server + '/api/v2.1/org/admin/logs/repo-transfer/';
let params = {
page: page,
per_page: perPage
};
return this.req.get(url, { params: params });
}
orgAdminListFileAudit(email, repoID, page) {
let url = this.server + '/api/v2.1/org/admin/logs/file-access/?page=' + page;
if (email) {

View File

@ -2169,6 +2169,15 @@ class SeafileAPI {
return this._sendPostRequest(url, formData);
}
sysAdminListFileTransferLogs(page, perPage) {
const url = this.server + '/api/v2.1/admin/logs/repo-transfer-logs/';
let params = {
page: page,
per_page: perPage
};
return this.req.get(url, { params: params });
}
}
let seafileAPI = new SeafileAPI();

View File

@ -28,6 +28,7 @@ from seahub.utils.timeutils import timestamp_to_isoformat_timestr
from seahub.api2.endpoints.group_owned_libraries import get_group_id_by_repo_owner
from seahub.constants import PERMISSION_READ_WRITE
from seahub.base.models import RepoTransfer
try:
from seahub.settings import MULTI_TENANCY
@ -423,6 +424,11 @@ class AdminLibrary(APIView):
# transfer repo
try:
transfer_repo(repo_id, new_owner, is_share)
RepoTransfer.objects.create(from_user=repo_owner,
to=new_owner,
repo_id=repo_id,
org_id=-1,
operator=request.user.username)
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'

View File

@ -20,6 +20,7 @@ from seahub.utils import get_file_audit_events, generate_file_audit_event_type,
get_file_update_events, get_perm_audit_events, is_valid_email
from seahub.utils.timeutils import datetime_to_isoformat_timestr, utc_datetime_to_isoformat_timestr
from seahub.utils.repo import is_valid_repo_id_format
from seahub.base.models import RepoTransfer
logger = logging.getLogger(__name__)
@ -420,3 +421,146 @@ class AdminLogsSharePermissionLogs(APIView):
}
return Response(resp)
class AdminLogsFileTransferLogs(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAdminUser, IsProVersion)
throttle_classes = (UserRateThrottle,)
def get(self, request):
""" Get all transfer repo logs.
Permission checking:
1. only admin can perform this action.
"""
if not request.user.admin_permissions.can_view_user_log():
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
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
if start < 0:
error_msg = 'start invalid'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
if limit < 0:
error_msg = 'limit invalid'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
events = RepoTransfer.objects.all().order_by('-timestamp')[start:start+limit+1]
if len(events) > limit:
has_next_page = True
events = events[:limit]
else:
has_next_page = False
# Use dict to reduce memcache fetch cost in large for-loop.
nickname_dict = {}
contact_email_dict = {}
repo_dict = {}
group_name_dict = {}
user_email_set = set()
repo_id_set = set()
group_id_set = set()
for event in events:
repo_id_set.add(event.repo_id)
if is_valid_email(event.from_user):
user_email_set.add(event.from_user)
if is_valid_email(event.to):
user_email_set.add(event.to)
if is_valid_email(event.operator):
user_email_set.add(event.operator)
if '@seafile_group' in event.to:
group_id = int(event.to.split('@')[0])
group_id_set.add(group_id)
if '@seafile_group' in event.from_user:
group_id = int(event.from_user.split('@')[0])
group_id_set.add(group_id)
for e in user_email_set:
if e not in nickname_dict:
nickname_dict[e] = email2nickname(e)
if e not in contact_email_dict:
contact_email_dict[e] = email2contact_email(e)
for e in repo_id_set:
if e not in repo_dict:
repo_dict[e] = seafile_api.get_repo(e)
for group_id in group_id_set:
if group_id not in group_name_dict:
group = ccnet_api.get_group(int(group_id))
if group:
group_name_dict[group_id] = group.group_name
events_info = []
for ev in events:
data = {
'from_user_email': '',
'from_user_name': '',
'from_user_contact_email': '',
'from_group_id': '',
'from_group_name': '',
'to_user_email': '',
'to_user_name': '',
'to_user_contact_email': '',
'to_group_id': '',
'to_group_name': '',
'operator_email': '',
'operator_name': '',
'operator_contact_email': '',
}
from_user_email = ev.from_user
data['from_user_email'] = from_user_email
data['from_user_name'] = nickname_dict.get(from_user_email, '')
data['from_user_contact_email'] = contact_email_dict.get(from_user_email, '')
operator_email = ev.operator
data['operator_email'] = operator_email
data['operator_name'] = nickname_dict.get(operator_email, '')
data['operator_contact_email'] = contact_email_dict.get(operator_email, '')
if is_valid_email(from_user_email):
data['from_type'] = 'user'
if '@seafile_group' in from_user_email:
from_group_id = int(from_user_email.split('@')[0])
data['from_group_id'] = from_group_id
data['from_group_name'] = group_name_dict.get(from_group_id, '')
data['from_type'] = 'group'
repo_id = ev.repo_id
data['repo_id'] = repo_id
repo = repo_dict.get(repo_id, None)
data['repo_name'] = repo.name if repo else ''
data['date'] = datetime_to_isoformat_timestr(ev.timestamp)
if is_valid_email(ev.to):
to_user_email = ev.to
data['to_user_email'] = to_user_email
data['to_user_name'] = nickname_dict.get(to_user_email, '')
data['to_user_contact_email'] = contact_email_dict.get(to_user_email, '')
data['to_type'] = 'user'
if '@seafile_group' in ev.to:
to_group_id = int(ev.to.split('@')[0])
data['to_group_id'] = to_group_id
data['to_group_name'] = group_name_dict.get(to_group_id, '')
data['to_type'] = 'group'
events_info.append(data)
resp = {
'repo_transfer_log_list': events_info,
'has_next_page': has_next_page,
}
return Response(resp)

View File

@ -50,6 +50,7 @@ from seahub.views import check_folder_permission
from seahub.settings import ENABLE_STORAGE_CLASSES, STORAGE_CLASS_MAPPING_POLICY, \
ENCRYPTED_LIBRARY_VERSION, ENCRYPTED_LIBRARY_PWD_HASH_ALGO, \
ENCRYPTED_LIBRARY_PWD_HASH_PARAMS
from seahub.base.models import RepoTransfer
logger = logging.getLogger(__name__)
@ -1497,6 +1498,12 @@ class GroupOwnedLibraryTransferView(APIView):
# transfer repo
try:
transfer_repo(repo_id, new_owner, is_share, org_id)
org_id = seafile_api.get_org_id_by_repo_id(repo_id)
RepoTransfer.objects.create(from_user=repo_owner,
to=new_owner,
repo_id=repo_id,
org_id=org_id,
operator=request.user.username)
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'

View File

@ -14,6 +14,7 @@ from seahub.share.models import UploadLinkShare, FileShare, check_share_link_acc
from seaserv import seafile_api
from seahub.utils.repo import parse_repo_perm
from seahub.views.file import send_file_access_msg
from seahub.utils import normalize_file_path
logger = logging.getLogger(__name__)
@ -146,6 +147,7 @@ class InternalCheckFileOperationAccess(APIView):
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
file_path = request.data.get('path', '/')
file_path = normalize_file_path(file_path)
repo = seafile_api.get_repo(repo_id)
if not repo:
return api_error(status.HTTP_404_NOT_FOUND, 'Library %s not found.' % repo_id)

View File

@ -46,7 +46,7 @@ from seahub.avatar.templatetags.avatar_tags import api_avatar_url, avatar
from seahub.avatar.templatetags.group_avatar_tags import api_grp_avatar_url, \
grp_avatar
from seahub.base.accounts import User
from seahub.base.models import UserStarredFiles, DeviceToken, RepoSecretKey, FileComment
from seahub.base.models import UserStarredFiles, DeviceToken, RepoSecretKey, FileComment, RepoTransfer
from seahub.share.models import ExtraSharePermission, ExtraGroupsSharePermission
from seahub.share.utils import is_repo_admin, check_group_share_in_permission, normalize_custom_permission_name
from seahub.base.templatetags.seahub_tags import email2nickname, \
@ -1871,6 +1871,12 @@ class RepoOwner(APIView):
# transfer repo
try:
transfer_repo(repo_id, new_owner, is_share, org_id)
org_id = seafile_api.get_org_id_by_repo_id(repo_id)
RepoTransfer.objects.create(from_user=repo_owner,
to=new_owner,
repo_id=repo_id,
operator=username,
org_id=org_id)
except SearpcError as e:
logger.error(e)
error_msg = 'Internal Server Error'

View File

@ -451,3 +451,15 @@ class ClientSSOToken(models.Model):
if not self.token:
self.token = self.gen_token()
return super(ClientSSOToken, self).save(*args, **kwargs)
class RepoTransfer(models.Model):
repo_id = models.CharField(max_length=36)
org_id = models.IntegerField(db_index=True)
from_user = models.CharField(max_length=255)
to = models.CharField(max_length=255)
operator = models.CharField(max_length=255)
timestamp = models.DateTimeField(default=timezone.now, db_index=True)
class Meta:
db_table = 'RepoTransfer'

View File

@ -17,7 +17,8 @@ from seahub.api2.endpoints.utils import get_user_name_dict, \
get_user_contact_email_dict, get_repo_dict, get_group_dict
from seahub.base.templatetags.seahub_tags import email2nickname, email2contact_email
from seahub.utils import EVENTS_ENABLED, get_file_audit_events, get_file_update_events, get_perm_audit_events
from seahub.base.models import RepoTransfer
from seahub.utils import EVENTS_ENABLED, get_file_audit_events, get_file_update_events, get_perm_audit_events, is_valid_email
from seahub.utils.timeutils import timestamp_to_isoformat_timestr, datetime_to_isoformat_timestr
from seahub.organizations.api.permissions import IsOrgAdmin
@ -263,3 +264,140 @@ class OrgAdminLogsPermAudit(APIView):
'page': page,
'page_next': page_next,
})
class OrgAdminLogsFileTransfer(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
throttle_classes = (UserRateThrottle,)
permission_classes = (IsProVersion, IsOrgAdmin)
def get(self, request):
"""List organization file transfer in logs
"""
try:
page = int(request.GET.get('page', '1'))
per_page = int(request.GET.get('per_page', '25'))
except ValueError:
page = 1
per_page = 25
start = per_page * (page - 1)
limit = per_page
org_id = request.user.org.org_id
events = RepoTransfer.objects.filter(org_id=org_id).all().order_by('-timestamp')[start:start+limit+1]
if len(events) > limit:
page_next = True
events = events[:limit]
else:
page_next = False
event_list = []
if not events:
return Response({
'log_list': event_list,
'page': page,
'page_next': False
})
# Use dict to reduce memcache fetch cost in large for-loop.
nickname_dict = {}
contact_email_dict = {}
repo_dict = {}
group_name_dict = {}
user_email_set = set()
repo_id_set = set()
group_id_set = set()
for event in events:
repo_id_set.add(event.repo_id)
if is_valid_email(event.from_user):
user_email_set.add(event.from_user)
if is_valid_email(event.to):
user_email_set.add(event.to)
if is_valid_email(event.operator):
user_email_set.add(event.operator)
if '@seafile_group' in event.to:
group_id = int(event.to.split('@')[0])
group_id_set.add(group_id)
if '@seafile_group' in event.from_user:
group_id = int(event.from_user.split('@')[0])
group_id_set.add(group_id)
for e in user_email_set:
if e not in nickname_dict:
nickname_dict[e] = email2nickname(e)
if e not in contact_email_dict:
contact_email_dict[e] = email2contact_email(e)
for e in repo_id_set:
if e not in repo_dict:
repo_dict[e] = seafile_api.get_repo(e)
for group_id in group_id_set:
if group_id not in group_name_dict:
group = ccnet_api.get_group(int(group_id))
if group:
group_name_dict[group_id] = group.group_name
event_list = []
for ev in events:
data = {
'from_user_email': '',
'from_user_name': '',
'from_user_contact_email': '',
'from_group_id': '',
'from_group_name': '',
'to_user_email': '',
'to_user_name': '',
'to_user_contact_email': '',
'to_group_id': '',
'to_group_name': '',
'operator_email': '',
'operator_name': '',
'operator_contact_email': '',
}
from_user_email = ev.from_user
data['from_user_email'] = from_user_email
data['from_user_name'] = nickname_dict.get(from_user_email, '')
data['from_user_contact_email'] = contact_email_dict.get(from_user_email, '')
operator_email = ev.operator
data['operator_email'] = operator_email
data['operator_name'] = nickname_dict.get(operator_email, '')
data['operator_contact_email'] = contact_email_dict.get(operator_email, '')
if is_valid_email(from_user_email):
data['from_type'] = 'user'
if '@seafile_group' in from_user_email:
from_group_id = int(from_user_email.split('@')[0])
data['from_group_id'] = from_group_id
data['from_group_name'] = group_name_dict.get(from_group_id, '')
data['from_type'] = 'group'
repo_id = ev.repo_id
data['repo_id'] = repo_id
repo = repo_dict.get(repo_id, None)
data['repo_name'] = repo.name if repo else ''
data['date'] = datetime_to_isoformat_timestr(ev.timestamp)
if is_valid_email(ev.to):
to_user_email = ev.to
data['to_user_email'] = to_user_email
data['to_user_name'] = nickname_dict.get(to_user_email, '')
data['to_user_contact_email'] = contact_email_dict.get(to_user_email, '')
data['to_type'] = 'user'
if '@seafile_group' in ev.to:
to_group_id = int(ev.to.split('@')[0])
data['to_group_id'] = to_group_id
data['to_group_name'] = group_name_dict.get(to_group_id, '')
data['to_type'] = 'group'
event_list.append(data)
return Response({
'log_list': event_list,
'page': page,
'page_next': page_next,
})

View File

@ -15,6 +15,7 @@ from seahub.api2.throttling import UserRateThrottle
from seahub.api2.authentication import TokenAuthentication
from seahub.api2.utils import api_error
from seahub.base.templatetags.seahub_tags import email2nickname, email2contact_email
from seahub.base.models import RepoTransfer
from seahub.group.utils import group_id_to_name
from seahub.utils.timeutils import timestamp_to_isoformat_timestr
from seahub.utils import is_valid_email, transfer_repo
@ -166,6 +167,11 @@ class OrgAdminRepo(APIView):
# transfer repo
try:
transfer_repo(repo_id, new_owner, is_share, org_id)
RepoTransfer.objects.create(from_user=repo_owner,
to=new_owner,
repo_id=repo_id,
org_id=org_id,
operator=request.user.username)
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
@ -182,9 +188,15 @@ class OrgAdminRepo(APIView):
break
repo_info = {}
repo_info['owner_email'] = new_owner
repo_info['owner_name'] = email2nickname(new_owner)
repo_info['encrypted'] = repo.encrypted
if '@seafile_group' in new_owner:
group_id = get_group_id_by_repo_owner(new_owner)
repo_info['group_name'] = group_id_to_name(group_id)
repo_info['owner_name'] = group_id_to_name(group_id)
else:
repo_info['owner_name'] = email2nickname(new_owner)
repo_info['encrypted'] = repo.encrypted
repo_info['repo_id'] = repo.repo_id
repo_info['repo_name'] = repo.name
repo_info['is_department_repo'] = False

View File

@ -20,7 +20,7 @@ from .api.admin.trash_libraries import OrgAdminTrashLibraries, OrgAdminTrashLibr
from .api.admin.info import OrgAdminInfo
from .api.admin.links import OrgAdminLinks, OrgAdminLink
from .api.admin.web_settings import OrgAdminWebSettings
from .api.admin.logs import OrgAdminLogsFileAccess, OrgAdminLogsFileUpdate, OrgAdminLogsPermAudit
from .api.admin.logs import OrgAdminLogsFileAccess, OrgAdminLogsFileUpdate, OrgAdminLogsPermAudit, OrgAdminLogsFileTransfer
from .api.admin.user_repos import OrgAdminUserRepos, OrgAdminUserBesharedRepos
from .api.admin.devices import OrgAdminDevices, OrgAdminDevicesErrors
@ -102,6 +102,7 @@ urlpatterns = [
path('admin/logs/file-access/', OrgAdminLogsFileAccess.as_view(), name='api-v2.1-org-admin-logs-file-access'),
path('admin/logs/file-update/', OrgAdminLogsFileUpdate.as_view(), name='api-v2.1-org-admin-logs-file-update'),
path('admin/logs/repo-permission/', OrgAdminLogsPermAudit.as_view(), name='api-v2.1-org-admin-logs-repo-permission'),
path('admin/logs/repo-transfer/', OrgAdminLogsFileTransfer.as_view(), name='api-v2.1-org-admin-logs-repo-transfer'),
path('<int:org_id>/admin/departments/', OrgAdminDepartments.as_view(), name='api-v2.1-org-admin-departments'),
path('<int:org_id>/admin/logs/export-excel/', OrgLogsExport.as_view(), name='api-v2.1-org-logs-export-excel'),
path('admin/log/export-excel/', org_log_export_excel, name='org_log_export_excel'),

View File

@ -35,6 +35,7 @@ urlpatterns = [
path('logadmin/', react_fake_view, name='org_log_file_audit'),
path('logadmin/file-update/', react_fake_view, name='org_log_file_update'),
path('logadmin/perm-audit/', react_fake_view, name='org_log_perm_audit'),
path('logadmin/repo-transfer/', react_fake_view, name='org_log_file_transfer'),
path('info/', react_fake_view, name='org_info'),
path('settings/', react_fake_view, name='org_settings'),

View File

@ -193,7 +193,7 @@ from seahub.api2.endpoints.admin.file_scan_records import AdminFileScanRecords
from seahub.api2.endpoints.admin.notifications import AdminNotificationsView
from seahub.api2.endpoints.admin.sys_notifications import AdminSysNotificationsView, AdminSysNotificationView
from seahub.api2.endpoints.admin.logs import AdminLogsLoginLogs, AdminLogsFileAccessLogs, AdminLogsFileUpdateLogs, \
AdminLogsSharePermissionLogs
AdminLogsSharePermissionLogs, AdminLogsFileTransferLogs
from seahub.api2.endpoints.admin.terms_and_conditions import AdminTermsAndConditions, AdminTermAndCondition
from seahub.api2.endpoints.admin.work_weixin import AdminWorkWeixinDepartments, \
AdminWorkWeixinDepartmentMembers, AdminWorkWeixinUsersBatch, AdminWorkWeixinDepartmentsImport
@ -691,6 +691,7 @@ urlpatterns = [
re_path(r'^api/v2.1/admin/logs/file-access-logs/$', AdminLogsFileAccessLogs.as_view(), name='api-v2.1-admin-logs-file-access-logs'),
re_path(r'^api/v2.1/admin/logs/file-update-logs/$', AdminLogsFileUpdateLogs.as_view(), name='api-v2.1-admin-logs-file-update-logs'),
re_path(r'^api/v2.1/admin/logs/share-permission-logs/$', AdminLogsSharePermissionLogs.as_view(), name='api-v2.1-admin-logs-share-permission-logs'),
re_path(r'^api/v2.1/admin/logs/repo-transfer-logs/$', AdminLogsFileTransferLogs.as_view(), name='api-v2.1-admin-logs-repo-transfer-logs'),
## admin::admin logs
re_path(r'^api/v2.1/admin/admin-logs/$', AdminOperationLogs.as_view(), name='api-v2.1-admin-admin-operation-logs'),
@ -865,6 +866,7 @@ urlpatterns = [
path('sys/logs/file-access/', sysadmin_react_fake_view, name="sys_logs_file_access"),
path('sys/logs/file-update/', sysadmin_react_fake_view, name="sys_logs_file_update"),
path('sys/logs/share-permission/', sysadmin_react_fake_view, name="sys_logs_share_permission"),
path('sys/logs/repo-transfer/', sysadmin_react_fake_view, name="sys_logs_file_transfer"),
path('sys/admin-logs/operation/', sysadmin_react_fake_view, name="sys_admin_logs_operation"),
path('sys/admin-logs/login/', sysadmin_react_fake_view, name="sys_admin_logs_login"),
path('sys/organizations/', sysadmin_react_fake_view, name="sys_organizations"),

View File

@ -1575,3 +1575,16 @@ CREATE TABLE `wiki_wiki2_publish` (
UNIQUE KEY `publish_url` (`publish_url`),
KEY `ix_wiki2_publish_repo_id` (`repo_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `RepoTransfer` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`repo_id` varchar(36) NOT NULL,
`org_id` int(11) NOT NULL,
`from_user` varchar(255) NOT NULL,
`to` varchar(255) NOT NULL,
`timestamp` datetime NOT NULL,
`operator` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_file_transfer_org_id` (`org_id`),
KEY `idx_file_transfer_timestamp` (`timestamp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;