1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-08-08 02:23:44 +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 OrgLogsFileAudit from './org-logs-file-audit';
import OrgLogsFileUpdate from './org-logs-file-update'; import OrgLogsFileUpdate from './org-logs-file-update';
import OrgLogsPermAudit from './org-logs-perm-audit'; import OrgLogsPermAudit from './org-logs-perm-audit';
import OrgLogsFileTransfer from './org-logs-file-transfer';
import OrgSAMLConfig from './org-saml-config'; import OrgSAMLConfig from './org-saml-config';
import OrgSubscription from './org-subscription'; import OrgSubscription from './org-subscription';
@ -126,6 +127,7 @@ class Org extends React.Component {
<OrgLogsFileAudit path='/' /> <OrgLogsFileAudit path='/' />
<OrgLogsFileUpdate path='file-update' /> <OrgLogsFileUpdate path='file-update' />
<OrgLogsPermAudit path='perm-audit' /> <OrgLogsPermAudit path='perm-audit' />
<OrgLogsFileTransfer path='repo-transfer' />
</OrgLogs> </OrgLogs>
{enableMultiADFS && {enableMultiADFS &&
<OrgSAMLConfig path={siteRoot + 'org/samlconfig/'}/> <OrgSAMLConfig path={siteRoot + 'org/samlconfig/'}/>

View File

@ -215,7 +215,7 @@ class RepoItem extends React.Component {
onTransferRepo = (email, reshare) => { onTransferRepo = (email, reshare) => {
let repo = this.props.repo; let repo = this.props.repo;
orgAdminAPI.orgAdminTransferOrgRepo(orgID, repo.repoID, email, reshare).then(res => { 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); this.props.transferRepoItem(repo.repoID, owner_name, owner_email);
let msg = gettext('Successfully transferred the library.'); let msg = gettext('Successfully transferred the library.');
toaster.success(msg); 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; const { isExportExcelDialogOpen, logType } = this.state;
return ( return (
<Fragment> <Fragment>
{this.props.currentTab === 'repo-transfer' ?
<MainPanelTopbar />
:
<MainPanelTopbar> <MainPanelTopbar>
<Button className="btn btn-secondary operation-item" onClick={this.toggleExportExcelDialog}>{gettext('Export Excel')}</Button> <Button className="btn btn-secondary operation-item" onClick={this.toggleExportExcelDialog}>{gettext('Export Excel')}</Button>
</MainPanelTopbar> </MainPanelTopbar>
}
<div className="main-panel-center flex-row"> <div className="main-panel-center flex-row">
<div className="cur-view-container h-100"> <div className="cur-view-container h-100">
<div className="cur-view-path org-user-nav"> <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')} to={siteRoot + 'org/logadmin/perm-audit/'} title={gettext('Permission')}>{gettext('Permission')}
</Link> </Link>
</li> </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> </ul>
</div> </div>
<div className="h-100 o-auto"> <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 FileAccessLogs from './logs-page/file-access-logs';
import FileUpdateLogs from './logs-page/file-update-logs'; import FileUpdateLogs from './logs-page/file-update-logs';
import SharePermissionLogs from './logs-page/share-permission-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 WebSettings from './web-settings/web-settings';
import Notifications from './notifications/notifications'; import Notifications from './notifications/notifications';
@ -251,6 +252,7 @@ class SysAdmin extends React.Component {
<InstitutionAdmins path={siteRoot + 'sys/institutions/:institutionID/admins'} {...commonProps} /> <InstitutionAdmins path={siteRoot + 'sys/institutions/:institutionID/admins'} {...commonProps} />
<LoginLogs path={siteRoot + 'sys/logs/login'} {...commonProps} /> <LoginLogs path={siteRoot + 'sys/logs/login'} {...commonProps} />
<FileAccessLogs path={siteRoot + 'sys/logs/file-access'} {...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} /> <FileUpdateLogs path={siteRoot + 'sys/logs/file-update'} {...commonProps} />
<SharePermissionLogs path={siteRoot + 'sys/logs/share-permission'} {...commonProps} /> <SharePermissionLogs path={siteRoot + 'sys/logs/share-permission'} {...commonProps} />
<AdminOperationLogs path={siteRoot + 'sys/admin-logs/operation'} {...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: 'fileAccessLogs', urlPart: 'logs/file-access', text: gettext('File Access') },
{ name: 'fileUpdateLogs', urlPart: 'logs/file-update', text: gettext('File Update') }, { name: 'fileUpdateLogs', urlPart: 'logs/file-update', text: gettext('File Update') },
{ name: 'sharePermissionLogs', urlPart: 'logs/share-permission', text: gettext('Permission') }, { 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', () => { describe('getFileExtension', () => {
it('should return the file extension with dot', () => { it('should return the file extension with dot', () => {

View File

@ -481,6 +481,15 @@ class OrgAdminAPI {
} }
// org admin logs // 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) { orgAdminListFileAudit(email, repoID, page) {
let url = this.server + '/api/v2.1/org/admin/logs/file-access/?page=' + page; let url = this.server + '/api/v2.1/org/admin/logs/file-access/?page=' + page;
if (email) { if (email) {

View File

@ -2169,6 +2169,15 @@ class SeafileAPI {
return this._sendPostRequest(url, formData); 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(); 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.api2.endpoints.group_owned_libraries import get_group_id_by_repo_owner
from seahub.constants import PERMISSION_READ_WRITE from seahub.constants import PERMISSION_READ_WRITE
from seahub.base.models import RepoTransfer
try: try:
from seahub.settings import MULTI_TENANCY from seahub.settings import MULTI_TENANCY
@ -423,6 +424,11 @@ class AdminLibrary(APIView):
# transfer repo # transfer repo
try: try:
transfer_repo(repo_id, new_owner, is_share) 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: except Exception as e:
logger.error(e) logger.error(e)
error_msg = 'Internal Server Error' 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 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.timeutils import datetime_to_isoformat_timestr, utc_datetime_to_isoformat_timestr
from seahub.utils.repo import is_valid_repo_id_format from seahub.utils.repo import is_valid_repo_id_format
from seahub.base.models import RepoTransfer
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -420,3 +421,146 @@ class AdminLogsSharePermissionLogs(APIView):
} }
return Response(resp) 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, \ from seahub.settings import ENABLE_STORAGE_CLASSES, STORAGE_CLASS_MAPPING_POLICY, \
ENCRYPTED_LIBRARY_VERSION, ENCRYPTED_LIBRARY_PWD_HASH_ALGO, \ ENCRYPTED_LIBRARY_VERSION, ENCRYPTED_LIBRARY_PWD_HASH_ALGO, \
ENCRYPTED_LIBRARY_PWD_HASH_PARAMS ENCRYPTED_LIBRARY_PWD_HASH_PARAMS
from seahub.base.models import RepoTransfer
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -1497,6 +1498,12 @@ class GroupOwnedLibraryTransferView(APIView):
# transfer repo # transfer repo
try: try:
transfer_repo(repo_id, new_owner, is_share, org_id) 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: except Exception as e:
logger.error(e) logger.error(e)
error_msg = 'Internal Server Error' 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 seaserv import seafile_api
from seahub.utils.repo import parse_repo_perm from seahub.utils.repo import parse_repo_perm
from seahub.views.file import send_file_access_msg from seahub.views.file import send_file_access_msg
from seahub.utils import normalize_file_path
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -146,6 +147,7 @@ class InternalCheckFileOperationAccess(APIView):
return api_error(status.HTTP_403_FORBIDDEN, error_msg) return api_error(status.HTTP_403_FORBIDDEN, error_msg)
file_path = request.data.get('path', '/') file_path = request.data.get('path', '/')
file_path = normalize_file_path(file_path)
repo = seafile_api.get_repo(repo_id) repo = seafile_api.get_repo(repo_id)
if not repo: if not repo:
return api_error(status.HTTP_404_NOT_FOUND, 'Library %s not found.' % repo_id) 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, \ from seahub.avatar.templatetags.group_avatar_tags import api_grp_avatar_url, \
grp_avatar grp_avatar
from seahub.base.accounts import User 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.models import ExtraSharePermission, ExtraGroupsSharePermission
from seahub.share.utils import is_repo_admin, check_group_share_in_permission, normalize_custom_permission_name 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, \ from seahub.base.templatetags.seahub_tags import email2nickname, \
@ -1871,6 +1871,12 @@ class RepoOwner(APIView):
# transfer repo # transfer repo
try: try:
transfer_repo(repo_id, new_owner, is_share, org_id) 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: except SearpcError as e:
logger.error(e) logger.error(e)
error_msg = 'Internal Server Error' error_msg = 'Internal Server Error'

View File

@ -451,3 +451,15 @@ class ClientSSOToken(models.Model):
if not self.token: if not self.token:
self.token = self.gen_token() self.token = self.gen_token()
return super(ClientSSOToken, self).save(*args, **kwargs) 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 get_user_contact_email_dict, get_repo_dict, get_group_dict
from seahub.base.templatetags.seahub_tags import email2nickname, email2contact_email 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.utils.timeutils import timestamp_to_isoformat_timestr, datetime_to_isoformat_timestr
from seahub.organizations.api.permissions import IsOrgAdmin from seahub.organizations.api.permissions import IsOrgAdmin
@ -263,3 +264,140 @@ class OrgAdminLogsPermAudit(APIView):
'page': page, 'page': page,
'page_next': page_next, '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.authentication import TokenAuthentication
from seahub.api2.utils import api_error from seahub.api2.utils import api_error
from seahub.base.templatetags.seahub_tags import email2nickname, email2contact_email 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.group.utils import group_id_to_name
from seahub.utils.timeutils import timestamp_to_isoformat_timestr from seahub.utils.timeutils import timestamp_to_isoformat_timestr
from seahub.utils import is_valid_email, transfer_repo from seahub.utils import is_valid_email, transfer_repo
@ -166,6 +167,11 @@ class OrgAdminRepo(APIView):
# transfer repo # transfer repo
try: try:
transfer_repo(repo_id, new_owner, is_share, org_id) 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: except Exception as e:
logger.error(e) logger.error(e)
error_msg = 'Internal Server Error' error_msg = 'Internal Server Error'
@ -182,7 +188,13 @@ class OrgAdminRepo(APIView):
break break
repo_info = {} repo_info = {}
repo_info['owner_email'] = new_owner repo_info['owner_email'] = new_owner
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['owner_name'] = email2nickname(new_owner)
repo_info['encrypted'] = repo.encrypted repo_info['encrypted'] = repo.encrypted
repo_info['repo_id'] = repo.repo_id repo_info['repo_id'] = repo.repo_id

View File

@ -20,7 +20,7 @@ from .api.admin.trash_libraries import OrgAdminTrashLibraries, OrgAdminTrashLibr
from .api.admin.info import OrgAdminInfo from .api.admin.info import OrgAdminInfo
from .api.admin.links import OrgAdminLinks, OrgAdminLink from .api.admin.links import OrgAdminLinks, OrgAdminLink
from .api.admin.web_settings import OrgAdminWebSettings 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.user_repos import OrgAdminUserRepos, OrgAdminUserBesharedRepos
from .api.admin.devices import OrgAdminDevices, OrgAdminDevicesErrors 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-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/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-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/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('<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'), 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/', react_fake_view, name='org_log_file_audit'),
path('logadmin/file-update/', react_fake_view, name='org_log_file_update'), 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/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('info/', react_fake_view, name='org_info'),
path('settings/', react_fake_view, name='org_settings'), 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.notifications import AdminNotificationsView
from seahub.api2.endpoints.admin.sys_notifications import AdminSysNotificationsView, AdminSysNotificationView from seahub.api2.endpoints.admin.sys_notifications import AdminSysNotificationsView, AdminSysNotificationView
from seahub.api2.endpoints.admin.logs import AdminLogsLoginLogs, AdminLogsFileAccessLogs, AdminLogsFileUpdateLogs, \ 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.terms_and_conditions import AdminTermsAndConditions, AdminTermAndCondition
from seahub.api2.endpoints.admin.work_weixin import AdminWorkWeixinDepartments, \ from seahub.api2.endpoints.admin.work_weixin import AdminWorkWeixinDepartments, \
AdminWorkWeixinDepartmentMembers, AdminWorkWeixinUsersBatch, AdminWorkWeixinDepartmentsImport 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-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/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/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 ## admin::admin logs
re_path(r'^api/v2.1/admin/admin-logs/$', AdminOperationLogs.as_view(), name='api-v2.1-admin-admin-operation-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-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/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/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/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/admin-logs/login/', sysadmin_react_fake_view, name="sys_admin_logs_login"),
path('sys/organizations/', sysadmin_react_fake_view, name="sys_organizations"), 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`), UNIQUE KEY `publish_url` (`publish_url`),
KEY `ix_wiki2_publish_repo_id` (`repo_id`) KEY `ix_wiki2_publish_repo_id` (`repo_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; ) 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;