diff --git a/frontend/config/webpack.config.dev.js b/frontend/config/webpack.config.dev.js index 9f110abce8..30cadb6e63 100644 --- a/frontend/config/webpack.config.dev.js +++ b/frontend/config/webpack.config.dev.js @@ -143,7 +143,12 @@ module.exports = { require.resolve('./polyfills'), require.resolve('react-dev-utils/webpackHotDevClient'), paths.appSrc + "/view-file-umind.js", - ] + ], + sysAdmin: [ + require.resolve('./polyfills'), + require.resolve('react-dev-utils/webpackHotDevClient'), + paths.appSrc + "/pages/sys-admin", + ], }, output: { diff --git a/frontend/config/webpack.config.prod.js b/frontend/config/webpack.config.prod.js index 20348e98c7..ddbb9d506f 100644 --- a/frontend/config/webpack.config.prod.js +++ b/frontend/config/webpack.config.prod.js @@ -77,6 +77,7 @@ module.exports = { viewFilePDF: [require.resolve('./polyfills'), paths.appSrc + "/view-file-pdf.js"], orgAdmin: [require.resolve('./polyfills'), paths.appSrc + "/pages/org-admin"], viewFileUMind: [require.resolve('./polyfills'), paths.appSrc + "/view-file-umind.js"], + sysAdmin: [require.resolve('./polyfills'), paths.appSrc + "/pages/sys-admin"], }, output: { diff --git a/frontend/src/components/common/account.js b/frontend/src/components/common/account.js index e05c5ee7e2..3ec9f1d667 100644 --- a/frontend/src/components/common/account.js +++ b/frontend/src/components/common/account.js @@ -1,9 +1,14 @@ import React, { Component } from 'react'; +import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; import { Utils } from '../../utils/utils'; import editorUtilities from '../../utils/editor-utilties'; import { siteRoot, gettext } from '../../utils/constants'; +const propTypes = { + isAdminPanel: PropTypes.bool, +}; + class Account extends Component { constructor(props) { super(props); @@ -87,11 +92,16 @@ class Account extends Component { } renderMenu = () => { - if(this.state.isStaff){ + if (this.state.isStaff && !this.props.isAdminPanel) { return ( {gettext('System Admin')} ); } + if (this.props.isAdminPanel) { + return ( + {gettext('Exit Admin Panel')} + ); + } if (this.state.isOrgStaff) { return ( {gettext('Organization Admin')} @@ -143,4 +153,6 @@ class Account extends Component { } } +Account.propTypes = propTypes; + export default Account; diff --git a/frontend/src/pages/sys-admin/file-scan-records.js b/frontend/src/pages/sys-admin/file-scan-records.js new file mode 100644 index 0000000000..b113ec0dad --- /dev/null +++ b/frontend/src/pages/sys-admin/file-scan-records.js @@ -0,0 +1,137 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { seafileAPI } from '../../utils/seafile-api'; +import { gettext, loginUrl } from '../../utils/constants'; + + +const tablePropTypes = { + loading: PropTypes.bool.isRequired, + errorMsg: PropTypes.string.isRequired, + records: PropTypes.array.isRequired, +}; + +class Table extends Component { + + render() { + let { loading, errorMsg, records } = this.props; + + if (loading) { + return ; + } else if (errorMsg) { + return

{errorMsg}

; + } else { + return ( + + + + + + + + + + + + {records.map((record, index) => { + return ( + + ); + })} + +
{gettext('Library')}{gettext('ID')}{gettext('Path')}{gettext('Label')}{gettext('Suggestion')}
+ ); + } + } +} + +Table.propTypes = tablePropTypes; + + +const itemPropTypes = { + record: PropTypes.object.isRequired, +}; + +class Item extends Component { + constructor(props) { + super(props); + this.state = {}; + } + + render() { + let record = this.props.record; + + return ( + + {record.repo_name} + {record.repo_id} + {record.path} + {record.detail.label} + {record.detail.suggestion} + + ); + } +} + +Item.propTypes = itemPropTypes; + + +class FileScanRecords extends Component { + constructor(props) { + super(props); + this.state = { + loading: true, + errorMsg: '', + records: [], + }; + } + + componentDidMount() { + seafileAPI.listFileScanRecords().then((res) => { + this.setState({ + loading: false, + records: res.data.record_list, + }); + }).catch((error) => { + if (error.response) { + if (error.response.status == 403) { + this.setState({ + loading: false, + errorMsg: gettext('Permission denied') + }); + location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`; + } else { + this.setState({ + loading: false, + errorMsg: gettext('Error') + }); + } + } else { + this.setState({ + loading: false, + errorMsg: gettext('Please check the network.') + }); + } + }); + } + + render() { + return ( +
+
+
+

{gettext('Content Scan Records')}

+
+
+ + + + + ); + } +} + +export default FileScanRecords; diff --git a/frontend/src/pages/sys-admin/index.js b/frontend/src/pages/sys-admin/index.js new file mode 100644 index 0000000000..7207322eb8 --- /dev/null +++ b/frontend/src/pages/sys-admin/index.js @@ -0,0 +1,60 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Router } from '@reach/router'; +import { siteRoot, gettext } from '../../utils/constants'; +import SidePanel from './side-panel'; +import MainPanel from './main-panel'; +import FileScanRecords from './file-scan-records'; + +import '../../assets/css/fa-solid.css'; +import '../../assets/css/fa-regular.css'; +import '../../assets/css/fontawesome.css'; +import '../../css/layout.css'; +import '../../css/toolbar.css'; + +class SysAdmin extends React.Component { + constructor(props) { + super(props); + this.state = { + isSidePanelClosed: false, + currentTab: 'file-scan', + }; + } + + componentDidMount() { + let href = window.location.href.split('/'); + this.setState({currentTab: href[href.length - 2]}); + } + + onCloseSidePanel = () => { + this.setState({isSidePanelClosed: !this.state.isSidePanelClosed}); + } + + tabItemClick = (param) => { + this.setState({currentTab: param}); + } + + render() { + let { currentTab, isSidePanelClosed, } = this.state; + + return ( +
+ + + + + + +
+ ); + } +} + +ReactDOM.render( + , + document.getElementById('wrapper') +); diff --git a/frontend/src/pages/sys-admin/main-panel.js b/frontend/src/pages/sys-admin/main-panel.js new file mode 100644 index 0000000000..ebe86a42ed --- /dev/null +++ b/frontend/src/pages/sys-admin/main-panel.js @@ -0,0 +1,31 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import Account from '../../components/common/account'; + + +const propTypes = { + children: PropTypes.object.isRequired, +}; + +class MainPanel extends Component { + + render() { + return ( +
+
+
+ +
+
+ +
+
+ {this.props.children} +
+ ); + } +} + +MainPanel.propTypes = propTypes; + +export default MainPanel; diff --git a/frontend/src/pages/sys-admin/side-panel.js b/frontend/src/pages/sys-admin/side-panel.js new file mode 100644 index 0000000000..2beec7e00c --- /dev/null +++ b/frontend/src/pages/sys-admin/side-panel.js @@ -0,0 +1,183 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link } from '@reach/router'; +import Logo from '../../components/logo'; +import { gettext, siteRoot, isPro, isDefaultAdmin, canViewSystemInfo, canViewStatistic, + canConfigSystem, canManageLibrary, canManageUser, canManageGroup, canViewUserLog, + canViewAdminLog, constanceEnabled, multiTenancy, multiInstitution, sysadminExtraEnabled, + enableGuestInvitation, enableTermsAndConditions, enableFileScan } from '../../utils/constants'; + +const propTypes = { + isSidePanelClosed: PropTypes.bool.isRequired, + onCloseSidePanel: PropTypes.func.isRequired, +}; + +class SidePanel extends React.Component { + + render() { + return ( +
+
+ +
+
+
+
+

{gettext('System Admin')}

+ +
+
+
+
+ ); + } +} + +SidePanel.propTypes = propTypes; + +export default SidePanel; diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index c799c70327..f1704451fe 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -73,3 +73,21 @@ export const originFileExists = window.draft ? window.draft.config.originFileExi // org admin export const orgID = window.org ? window.org.pageOptions.orgID : ''; export const invitationLink = window.org ? window.org.pageOptions.invitationLink : ''; + +// sys admin +export const constanceEnabled = window.sysadmin ? window.sysadmin.pageOptions.constance_enabled : ''; +export const multiTenancy = window.sysadmin ? window.sysadmin.pageOptions.multi_tenancy : ''; +export const multiInstitution = window.sysadmin ? window.sysadmin.pageOptions.multi_institution : ''; +export const sysadminExtraEnabled = window.sysadmin ? window.sysadmin.pageOptions.sysadmin_extra_enabled : ''; +export const enableGuestInvitation = window.sysadmin ? window.sysadmin.pageOptions.enable_guest_invitation : ''; +export const enableTermsAndConditions = window.sysadmin ? window.sysadmin.pageOptions.enable_terms_and_conditions : ''; +export const isDefaultAdmin = window.sysadmin ? window.sysadmin.pageOptions.is_default_admin : ''; +export const enableFileScan = window.sysadmin ? window.sysadmin.pageOptions.enable_file_scan : ''; +export const canViewSystemInfo = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_view_system_info : ''; +export const canViewStatistic = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_view_statistic : ''; +export const canConfigSystem = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_config_system : ''; +export const canManageLibrary = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_manage_library : ''; +export const canManageUser = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_manage_user : ''; +export const canManageGroup = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_manage_group : ''; +export const canViewUserLog = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_view_user_log : ''; +export const canViewAdminLog = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_view_admin_log : ''; diff --git a/media/css/seahub_react.css b/media/css/seahub_react.css index e004f95096..a2288f4a7d 100644 --- a/media/css/seahub_react.css +++ b/media/css/seahub_react.css @@ -52,6 +52,7 @@ -webkit-font-smoothing: antialiased; } +.sf2-icon-histogram:before { content:"\e000"; } .sf2-icon-wrench:before { content:"\e001"; } .sf2-icon-clock:before { content:"\e002"; } .sf2-icon-bell:before { content:"\e003"; } @@ -68,21 +69,26 @@ .sf2-icon-history:before { content:"\e014"; } .sf2-icon-cog1:before { content:"\e015"; } .sf2-icon-trash:before { content:"\e016"; } +.sf2-icon-security:before { content:"\e017"; } .sf2-icon-tick:before { content:"\e01e"; } .sf2-icon-x2:before { content:"\e01f"; } .sf2-icon-edit:before { content:"\e018"; } .sf2-icon-caret-down:before { content:"\e01a"; } +.sf2-icon-cog2:before { content:"\e01b"; } .sf2-icon-x1:before { content:"\e01d"; } .sf2-icon-minus:before {content:"\e01c"} .sf2-icon-confirm:before {content:"\e01e"} .sf2-icon-cancel:before {content:"\e01f"} .sf2-icon-user2:before { content:"\e020"; } +.sf2-icon-msgs:before { content:"\e021"; } .sf2-icon-grid-view:before { content:"\e025"; } .sf2-icon-list-view:before { content:"\e026"; } .sf2-icon-plus:before { content: "\e027"; } .sf2-icon-copy:before {content:"\e028"} .sf2-icon-move:before {content:"\e029"} .sf2-icon-reply:before { content:"\e02a"; } +.sf2-icon-admin-log:before { content:"\e02e"; } +.sf2-icon-info:before { content:"\e02f"; } .sf2-icon-menu:before { content: "\e031"; } .sf2-icon-more:before { content: "\e032"; } .sf2-icon-x3:before {content:"\e035";} diff --git a/seahub/api2/endpoints/admin/file_scan_records.py b/seahub/api2/endpoints/admin/file_scan_records.py new file mode 100644 index 0000000000..9ee03d3b55 --- /dev/null +++ b/seahub/api2/endpoints/admin/file_scan_records.py @@ -0,0 +1,64 @@ +# Copyright (c) 2012-2016 Seafile Ltd. +import logging +import json + +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 seahub.api2.authentication import TokenAuthentication +from seahub.api2.throttling import UserRateThrottle +from seahub.api2.permissions import IsProVersion +from seahub.api2.utils import api_error +from seahub.utils import get_file_scan_record + +from seaserv import seafile_api + +logger = logging.getLogger(__name__) + + +class AdminFileScanRecords(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAdminUser, IsProVersion) + throttle_classes = (UserRateThrottle,) + + def get(self, request): + """get file content scan records + """ + try: + page = int(request.GET.get('page', '')) + except ValueError: + page = 1 + + try: + per_page = int(request.GET.get('per_page', '')) + except ValueError: + per_page = 25 + + start = (page - 1) * per_page + count = per_page + + try: + record_list = get_file_scan_record(start, count) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + for record in record_list: + repo = seafile_api.get_repo(record["repo_id"]) + if not repo: + record["repo_name"] = "" + else: + record["repo_name"] = repo.name + record_detail = json.loads(record['detail']) + detail_dict = record_detail.values()[0] + detail = dict() + detail["suggestion"] = detail_dict["suggestion"] + detail["label"] = detail_dict["label"] + record["detail"] = detail + + return Response({"record_list": record_list}, status=status.HTTP_200_OK) diff --git a/seahub/base/context_processors.py b/seahub/base/context_processors.py index afc8fa88dd..8197284353 100644 --- a/seahub/base/context_processors.py +++ b/seahub/base/context_processors.py @@ -44,6 +44,11 @@ try: from seahub.settings import MULTI_TENANCY except ImportError: MULTI_TENANCY = False +try: + from seahub.settings import ENABLE_FILE_SCAN +except ImportError: + ENABLE_FILE_SCAN = False + def base(request): """ @@ -120,6 +125,7 @@ def base(request): 'enable_upload_folder': dj_settings.ENABLE_UPLOAD_FOLDER, 'enable_resumable_fileupload': dj_settings.ENABLE_RESUMABLE_FILEUPLOAD, 'service_url': get_service_url().rstrip('/'), + 'enable_file_scan': ENABLE_FILE_SCAN, } if request.user.is_staff: diff --git a/seahub/settings.py b/seahub/settings.py index 238d0cd50c..acec70729f 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -258,6 +258,9 @@ INSTALLED_APPS = ( 'seahub.related_files', ) +# Enable or disable view File Scan +# ENABLE_FILE_SCAN = True + # Enable or disable multiple storage backends. ENABLE_STORAGE_CLASSES = False diff --git a/seahub/templates/sysadmin/base.html b/seahub/templates/sysadmin/base.html index 0679c1aaed..b85162d9d1 100644 --- a/seahub/templates/sysadmin/base.html +++ b/seahub/templates/sysadmin/base.html @@ -94,6 +94,12 @@ {% endif %} + {% if is_pro and is_default_admin and enable_file_scan %} +
  • + {% trans "File Scan" %} +
  • + {% endif %} + {% if is_pro and is_default_admin %}
  • {% trans "Virus Scan" %} diff --git a/seahub/templates/sysadmin/sys_file_scan_records_react.html b/seahub/templates/sysadmin/sys_file_scan_records_react.html new file mode 100644 index 0000000000..ee472d4eea --- /dev/null +++ b/seahub/templates/sysadmin/sys_file_scan_records_react.html @@ -0,0 +1,33 @@ +{% extends "base_for_react.html" %} +{% load seahub_tags i18n %} +{% load render_bundle from webpack_loader %} + +{% block extra_script %} + +{% render_bundle 'sysAdmin' %} +{% endblock %} diff --git a/seahub/urls.py b/seahub/urls.py index ed7036930d..c686b1933b 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -130,6 +130,7 @@ from seahub.api2.endpoints.admin.address_book.groups import AdminAddressBookGrou AdminAddressBookGroup from seahub.api2.endpoints.admin.group_owned_libraries import AdminGroupOwnedLibraries, \ AdminGroupOwnedLibrary +from seahub.api2.endpoints.admin.file_scan_records import AdminFileScanRecords urlpatterns = [ url(r'^accounts/', include('seahub.base.registration_urls')), @@ -501,6 +502,9 @@ urlpatterns = [ url(r'^api/v2.1/admin/address-book/groups/$', AdminAddressBookGroups.as_view(), name='api-v2.1-admin-address-book-groups'), url(r'^api/v2.1/admin/address-book/groups/(?P\d+)/$', AdminAddressBookGroup.as_view(), name='api-v2.1-admin-address-book-group'), + ## admin::file-scan-records + url(r'^api/v2.1/admin/file-scan-records/$', AdminFileScanRecords.as_view(), name='api-v2.1-admin-file-scan-records'), + ### system admin ### url(r'^sysadmin/$', sysadmin, name='sysadmin'), url(r'^sys/settings/$', sys_settings, name='sys_settings'), @@ -572,6 +576,15 @@ urlpatterns = [ url(r'^client-login/$', client_token_login, name='client_token_login'), ] +try: + from seahub.settings import ENABLE_FILE_SCAN +except ImportError: + ENABLE_FILE_SCAN = False +if ENABLE_FILE_SCAN: + urlpatterns += [ + url(r'^sys/file-scan-records/$', sys_file_scan_records, name="sys_file_scan_records"), + ] + from seahub.utils import EVENTS_ENABLED if EVENTS_ENABLED: urlpatterns += [ diff --git a/seahub/utils/__init__.py b/seahub/utils/__init__.py index be950275d2..7cef763780 100644 --- a/seahub/utils/__init__.py +++ b/seahub/utils/__init__.py @@ -829,6 +829,11 @@ if EVENTS_CONFIG_FILE: def get_virus_record_by_id(vid): with _get_seafevents_session() as session: return seafevents.get_virus_record_by_id(session, vid) + + def get_file_scan_record(start=-1, limit=-1): + records = seafevents_api.get_content_scan_results(start, limit) + return records if records else [] + else: EVENTS_ENABLED = False def get_user_events(): @@ -867,6 +872,8 @@ else: pass def get_virus_record_by_id(vid): pass + def get_file_scan_record(): + pass def calc_file_path_hash(path, bits=12): if isinstance(path, unicode): diff --git a/seahub/views/sysadmin.py b/seahub/views/sysadmin.py index eb4afc5936..321bfd8a46 100644 --- a/seahub/views/sysadmin.py +++ b/seahub/views/sysadmin.py @@ -88,8 +88,16 @@ try: from seahub_extra.organizations.models import OrgSettings except ImportError: MULTI_TENANCY = False +try: + from seahub.settings import ENABLE_SYSADMIN_EXTRA +except ImportError: + ENABLE_SYSADMIN_EXTRA = False from seahub.utils.two_factor_auth import has_two_factor_auth from termsandconditions.models import TermsAndConditions +try: + from seahub.settings import ENABLE_FILE_SCAN +except ImportError: + ENABLE_FILE_SCAN = False logger = logging.getLogger(__name__) @@ -124,6 +132,22 @@ def sysadmin(request): 'trash_repos_expire_days': expire_days if expire_days > 0 else 30, }) + +@login_required +@sys_staff_required +def sys_file_scan_records(request): + + return render(request, 'sysadmin/sys_file_scan_records_react.html', { + 'constance_enabled': dj_settings.CONSTANCE_ENABLED, + 'multi_tenancy': MULTI_TENANCY, + 'multi_institution': getattr(dj_settings, 'MULTI_INSTITUTION', False), + 'sysadmin_extra_enabled': ENABLE_SYSADMIN_EXTRA, + 'enable_guest_invitation': ENABLE_GUEST_INVITATION, + 'enable_terms_and_conditions': config.ENABLE_TERMS_AND_CONDITIONS, + 'enable_file_scan': ENABLE_FILE_SCAN, + }) + + @login_required @sys_staff_required def sys_statistic_file(request):