diff --git a/frontend/src/components/user-settings/social-login.js b/frontend/src/components/user-settings/social-login.js index 1c5ce02d01..6aa86a1b9f 100644 --- a/frontend/src/components/user-settings/social-login.js +++ b/frontend/src/components/user-settings/social-login.js @@ -40,13 +40,13 @@ class SocialLogin extends React.Component {

{langCode == 'zh-cn' ? '企业微信': 'WeChat Work'}

{socialConnected ? {gettext('Disconnect')} : - {gettext('Connect')} + {gettext('Connect')} } {this.state.isConfirmDialogOpen && ( diff --git a/frontend/src/pages/sys-admin/index.js b/frontend/src/pages/sys-admin/index.js index 91b645c6f2..f09f842393 100644 --- a/frontend/src/pages/sys-admin/index.js +++ b/frontend/src/pages/sys-admin/index.js @@ -5,6 +5,7 @@ import { siteRoot, gettext } from '../../utils/constants'; import SidePanel from './side-panel'; import MainPanel from './main-panel'; import FileScanRecords from './file-scan-records'; +import WorkWeixinDepartments from './work-weixin-departments' import '../../assets/css/fa-solid.css'; import '../../assets/css/fa-regular.css'; @@ -48,6 +49,13 @@ class SysAdmin extends React.Component { tabItemClick={this.tabItemClick} /> + + + ); diff --git a/frontend/src/pages/sys-admin/main-panel.js b/frontend/src/pages/sys-admin/main-panel.js index ebe86a42ed..a83b54728b 100644 --- a/frontend/src/pages/sys-admin/main-panel.js +++ b/frontend/src/pages/sys-admin/main-panel.js @@ -4,7 +4,7 @@ import Account from '../../components/common/account'; const propTypes = { - children: PropTypes.object.isRequired, + children: PropTypes.array.isRequired, }; class MainPanel extends Component { diff --git a/frontend/src/pages/sys-admin/side-panel.js b/frontend/src/pages/sys-admin/side-panel.js index b4a8579a65..4cc0746c31 100644 --- a/frontend/src/pages/sys-admin/side-panel.js +++ b/frontend/src/pages/sys-admin/side-panel.js @@ -5,7 +5,7 @@ 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'; + enableGuestInvitation, enableTermsAndConditions, enableFileScan, enableWorkWeixinDepartments } from '../../utils/constants'; const propTypes = { isSidePanelClosed: PropTypes.bool.isRequired, @@ -169,6 +169,14 @@ class SidePanel extends React.Component { } + {isDefaultAdmin && enableWorkWeixinDepartments && +
  • + + + {'企业微信集成'} + +
  • + } diff --git a/frontend/src/pages/sys-admin/work-weixin-departments.js b/frontend/src/pages/sys-admin/work-weixin-departments.js new file mode 100644 index 0000000000..db526dddc8 --- /dev/null +++ b/frontend/src/pages/sys-admin/work-weixin-departments.js @@ -0,0 +1,495 @@ +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; +import {seafileAPI} from '../../utils/seafile-api'; +import {gettext, siteRoot} from '../../utils/constants'; +import toaster from '../../components/toast'; +import Loading from '../../components/loading'; +import {Button, Table} from 'reactstrap'; + + +class WorkWeixinDepartmentMembersList extends Component { + constructor(props) { + super(props); + this.state = {}; + } + + render() { + const membersList = this.props.membersList.map((member, index) => { + let avatar = member.avatar; + if (member.avatar.length > 0) { + // get smaller avatar + avatar = member.avatar.substring(0, member.avatar.length - 1) + '100'; + } else { + avatar = siteRoot + 'media/avatars/default.png'; + } + const userCheckBox = member.email ? '' : + this.props.onUserChecked(member)} + >; + return ( + + {userCheckBox} + + {member.name} + {member.mobile} + {member.contact_email} + {member.email ? : ''} + + ); + }); + + const allUsersCheckBox = !this.props.canCheckUserIds.length ? '' : + this.props.onAllUsersChecked()} + >; + + return ( +
    +
    + {this.props.isMembersListLoading && } + + {!this.props.isMembersListLoading && + + + + + + + + + + + + + {membersList} + +
    {allUsersCheckBox}{'名称'}{'手机号'}{'邮箱'}{'已添加'}
    + } + {!this.props.isMembersListLoading && this.props.membersList.length === 0 && +
    +

    {gettext('无成员')}

    +
    + } +
    +
    + ); + } +} + +const WorkWeixinDepartmentMembersListPropTypes = { + isMembersListLoading: PropTypes.bool.isRequired, + membersList: PropTypes.array.isRequired, + newUsersTempObj: PropTypes.object.isRequired, + checkedDepartmentId: PropTypes.number.isRequired, + onUserChecked: PropTypes.func.isRequired, + onAllUsersChecked: PropTypes.func.isRequired, + isCheckedAll: PropTypes.bool.isRequired, + canCheckUserIds: PropTypes.array.isRequired, +}; + +WorkWeixinDepartmentMembersList.propTypes = WorkWeixinDepartmentMembersListPropTypes; + + +class WorkWeixinDepartmentsTreeNode extends Component { + constructor(props) { + super(props); + this.state = { + isChildrenShow: false, + }; + } + + toggleChildren = () => { + this.setState({ + isChildrenShow: !this.state.isChildrenShow, + }); + }; + + renderTreeNodes = (departmentsTree) => { + if (departmentsTree.length > 0) { + return departmentsTree.map((department) => { + return ( + + ); + }); + } + }; + + componentDidMount() { + if (this.props.index === 0) { + this.toggleChildren(); + this.props.onChangeDepartment(this.props.department.id); + } + } + + render() { + let toggleIconClass = ''; + if (this.props.department.children) { + if (this.state.isChildrenShow) { + toggleIconClass = 'folder-toggle-icon fa fa-caret-down'; + } else { + toggleIconClass = 'folder-toggle-icon fa fa-caret-right'; + } + } + let departmentStyle = this.props.checkedDepartmentId === this.props.department.id ? {color: 'blue'} : {}; + + return ( +
    + {this.props.isChildrenShow && +
    + this.toggleChildren()}>{' '} + this.props.onChangeDepartment(this.props.department.id)} + >{this.props.department.name} +
    + } + {this.state.isChildrenShow && +
    + {this.props.department.children && this.renderTreeNodes(this.props.department.children)} +
    + } +
    + ); + } +} + +const WorkWeixinDepartmentsTreeNodePropTypes = { + index: PropTypes.number, + department: PropTypes.object.isRequired, + isChildrenShow: PropTypes.bool.isRequired, + onChangeDepartment: PropTypes.func.isRequired, + checkedDepartmentId: PropTypes.number.isRequired, +}; + +WorkWeixinDepartmentsTreeNode.propTypes = WorkWeixinDepartmentsTreeNodePropTypes; + + +class WorkWeixinDepartmentsTreePanel extends Component { + constructor(props) { + super(props); + this.state = {}; + } + + render() { + const departmentsTree = this.props.departmentsTree.map((department, index) => { + return ( + + ); + }); + + return ( +
    +
    + {this.props.isTreeLoading && } + {!this.props.isTreeLoading && +
    + {this.props.departmentsTree.length > 0 && departmentsTree} +
    + } +
    +
    + ); + } +} + +const WorkWeixinDepartmentsTreePanelPropTypes = { + isTreeLoading: PropTypes.bool.isRequired, + departmentsTree: PropTypes.array.isRequired, + onChangeDepartment: PropTypes.func.isRequired, + checkedDepartmentId: PropTypes.number.isRequired, +}; + +WorkWeixinDepartmentsTreePanel.propTypes = WorkWeixinDepartmentsTreePanelPropTypes; + + +class WorkWeixinDepartments extends Component { + + constructor(props) { + super(props); + this.state = { + isTreeLoading: true, + isMembersListLoading: true, + departmentsTree: [], + checkedDepartmentId: 0, + membersTempObj: {}, + membersList: [], + newUsersTempObj: {}, + isCheckedAll: false, + canCheckUserIds: [], + }; + } + + getDepartmentsTree = (list) => { + let childIds = []; + let parentIds = []; + for (let i = 0; i < list.length; i++) { + if (childIds.indexOf(list[i].id) === -1) { + childIds.push(list[i].id); + } + if (parentIds.indexOf(list[i].parentid) === -1) { + parentIds.push(list[i].parentid); + } + } + let intersection = parentIds.filter((v) => { + return childIds.indexOf(v) !== -1; + }); + let rootIds = parentIds.concat(intersection).filter((v) => { + return parentIds.indexOf(v) === -1 || intersection.indexOf(v) === -1; + }); + + let cloneData = JSON.parse(JSON.stringify(list)); + return cloneData.filter(father => { + let branchArr = cloneData.filter(child => father.id === child.parentid); + branchArr.length > 0 ? father.children = branchArr : ''; + return rootIds.indexOf(father.parentid) !== -1; + }); + }; + + getWorkWeixinDepartmentsList = () => { + seafileAPI.adminListWorkWeixinDepartments().then((res) => { + let departmentsTree = this.getDepartmentsTree(res.data.department); + this.setState({ + isTreeLoading: false, + departmentsTree: departmentsTree, + }); + }).catch((error) => { + this.setState({ + isTreeLoading: false, + isMembersListLoading: false, + }); + if (error.response) { + toaster.danger(error.response.data.error_msg || error.response.data.detail || gettext('Error'), {duration: 3}); + } else { + toaster.danger(gettext('Please check the network.'), {duration: 3}); + } + if (error.response.status === 403) { + window.location = siteRoot + 'sys/useradmin/'; + } + }); + }; + + getWorkWeixinDepartmentMembersList = (department_id) => { + this.setState({ + isMembersListLoading: true, + }); + seafileAPI.adminListWorkWeixinDepartmentMembers(department_id.toString(), {fetch_child: true}).then((res) => { + let membersTempObj = this.state.membersTempObj; + membersTempObj[department_id] = res.data.userlist; + let canCheckUserIds = this.getCanCheckUserIds(res.data.userlist); + this.setState({ + membersTempObj: membersTempObj, + membersList: res.data.userlist, + isMembersListLoading: false, + canCheckUserIds: canCheckUserIds, + }); + }).catch((error) => { + this.setState({isMembersListLoading: false}); + if (error.response) { + toaster.danger(error.response.data.error_msg || error.response.data.detail || gettext('Error'), {duration: 3}); + } else { + toaster.danger(gettext('Please check the network.'), {duration: 3}); + } + }); + }; + + getCanCheckUserIds = (membersList) => { + let canCheckUserIds = []; + for (let i = 0; i < membersList.length; i++) { + let user = membersList[i]; + if (!user.email) { + canCheckUserIds.push(user.userid); + } + } + return canCheckUserIds; + }; + + onChangeDepartment = (department_id) => { + this.setState({ + newUsersTempObj: {}, + isCheckedAll: false, + checkedDepartmentId: department_id, + }); + if (!(department_id in this.state.membersTempObj)) { + this.getWorkWeixinDepartmentMembersList(department_id); + } else { + let canCheckUserIds = this.getCanCheckUserIds(this.state.membersTempObj[department_id]); + this.setState({ + membersList: this.state.membersTempObj[department_id], + canCheckUserIds: canCheckUserIds, + }); + } + }; + + onUserChecked = (user) => { + if (this.state.canCheckUserIds.indexOf(user.userid) !== -1) { + let newUsersTempObj = this.state.newUsersTempObj; + if (user.userid in newUsersTempObj) { + delete newUsersTempObj[user.userid]; + if (this.state.isCheckedAll) { + this.setState({ + isCheckedAll: false, + }); + } + } else { + newUsersTempObj[user.userid] = user; + if (Object.keys(newUsersTempObj).length === this.state.canCheckUserIds.length) { + this.setState({ + isCheckedAll: true, + }); + } + } + this.setState({ + newUsersTempObj: newUsersTempObj, + }); + } + }; + + onAllUsersChecked = () => { + this.setState({ + isCheckedAll: !this.state.isCheckedAll, + }, () => { + if (this.state.isCheckedAll) { + let newUsersTempObj = {}; + let newUsersTempList = this.state.membersList.filter(user => { + return this.state.canCheckUserIds.indexOf(user.userid) !== -1; + }); + + for (let i = 0; i < newUsersTempList.length; i++) { + newUsersTempObj[newUsersTempList[i].userid] = newUsersTempList[i]; + } + this.setState({ + newUsersTempObj: newUsersTempObj, + }); + } else { + this.setState({ + newUsersTempObj: {}, + }); + } + }); + }; + + onSubmit = () => { + let userList = []; + for (let i in this.state.newUsersTempObj) { + userList.push(this.state.newUsersTempObj[i]); + } + if (!userList.length) { + toaster.danger('未选择成员', {duration: 3}); + } else { + seafileAPI.adminAddWorkWeixinUsersBatch(userList).then((res) => { + this.setState({ + newUsersTempObj: {}, + isCheckedAll: false, + }); + if (res.data.success) { + let membersTempObj = this.state.membersTempObj; + let membersList = this.state.membersList; + let canCheckUserIds = this.state.canCheckUserIds; + for (let i = 0; i < res.data.success.length; i++) { + let userid = res.data.success[i].userid; + let name = res.data.success[i].name; + let email = res.data.success[i].email; + toaster.success(name + ' 成功导入', {duration: 1}); + // refresh all temp + if (canCheckUserIds.indexOf(userid) !== -1) { + canCheckUserIds.splice(canCheckUserIds.indexOf(userid), 1); + } + for (let j = 0; j < membersList.length; j++) { + if (membersList[j].userid === userid) { + membersList[j].email = email; + break; + } + } + for (let departmentId in membersTempObj) { + for (let k = 0; k < membersTempObj[departmentId].length; k++) { + if (membersTempObj[departmentId][k].userid === userid) { + membersTempObj[departmentId][k].email = email; + break; + } + } + } + } + this.setState({ + membersTempObj: membersTempObj, + membersList: membersList, + canCheckUserIds: canCheckUserIds, + }); + } + if (res.data.failed) { + for (let i = 0; i < res.data.failed.length; i++) { + let name = res.data.failed[i].name; + let errorMsg = res.data.failed[i].error_msg; + toaster.danger(name + ' ' + errorMsg, {duration: 3}); + } + } + }).catch((error) => { + if (error.response) { + toaster.danger(error.response.data.error_msg || error.response.data.detail || gettext('Error'), {duration: 3}); + } else { + toaster.danger(gettext('Please check the network.'), {duration: 3}); + } + }); + } + + }; + + componentDidMount() { + this.getWorkWeixinDepartmentsList(); + } + + render() { + return ( +
    +
    +
    +

    {'企业微信集成'}

    + {JSON.stringify(this.state.newUsersTempObj) !== '{}' && + + } +
    +
    + +
    + +
    +
    +
    + ); + } +} + +export default WorkWeixinDepartments; + diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index 2b5ac97ad8..5b542eae52 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -112,4 +112,5 @@ export const canManageUser = window.sysadmin ? window.sysadmin.pageOptions.admin 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 : ''; +export const enableWorkWeixinDepartments = window.sysadmin ? window.sysadmin.pageOptions.enable_work_weixin_departments : ''; diff --git a/seahub/api2/endpoints/admin/work_weixin.py b/seahub/api2/endpoints/admin/work_weixin.py new file mode 100644 index 0000000000..b248a97f27 --- /dev/null +++ b/seahub/api2/endpoints/admin/work_weixin.py @@ -0,0 +1,206 @@ +# Copyright (c) 2012-2019 Seafile Ltd. +# encoding: utf-8 + +import logging +import requests +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.utils import api_error +from seahub.work_weixin.utils import handler_work_weixin_api_response, \ + get_work_weixin_access_token, admin_work_weixin_departments_check, \ + update_work_weixin_user_info +from seahub.work_weixin.settings import WORK_WEIXIN_DEPARTMENTS_URL, \ + WORK_WEIXIN_DEPARTMENT_MEMBERS_URL, WORK_WEIXIN_PROVIDER, WORK_WEIXIN_UID_PREFIX +from seahub.base.accounts import User +from seahub.utils.auth import gen_user_virtual_id +from seahub.auth.models import SocialAuthUser + +logger = logging.getLogger(__name__) +WORK_WEIXIN_DEPARTMENT_FIELD = 'department' +WORK_WEIXIN_DEPARTMENT_MEMBERS_FIELD = 'userlist' + +# # uid = corpid + '_' + userid +# from social_django.models import UserSocialAuth +# get departments: https://work.weixin.qq.com/api/doc#90000/90135/90208 +# get members: https://work.weixin.qq.com/api/doc#90000/90135/90200 + + +class AdminWorkWeixinDepartments(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + throttle_classes = (UserRateThrottle,) + permission_classes = (IsAdminUser,) + + def get(self, request): + if not admin_work_weixin_departments_check(): + error_msg = 'Feature is not enabled.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + access_token = get_work_weixin_access_token() + if not access_token: + logger.error('can not get work weixin access_token') + error_msg = '获取企业微信组织架构失败' + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + data = { + 'access_token': access_token, + } + department_id = request.GET.get('department_id', None) + if department_id: + data['id'] = department_id + + api_response = requests.get(WORK_WEIXIN_DEPARTMENTS_URL, params=data) + api_response_dic = handler_work_weixin_api_response(api_response) + if not api_response_dic: + logger.error('can not get work weixin departments response') + error_msg = '获取企业微信组织架构失败' + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + if WORK_WEIXIN_DEPARTMENT_FIELD not in api_response_dic: + logger.error(json.dumps(api_response_dic)) + logger.error('can not get department list in work weixin departments response') + error_msg = '获取企业微信组织架构失败' + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + return Response(api_response_dic) + + +class AdminWorkWeixinDepartmentMembers(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + throttle_classes = (UserRateThrottle,) + permission_classes = (IsAdminUser,) + + def get(self, request, department_id): + if not admin_work_weixin_departments_check(): + error_msg = 'Feature is not enabled.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + access_token = get_work_weixin_access_token() + if not access_token: + logger.error('can not get work weixin access_token') + error_msg = '获取企业微信组织架构成员失败' + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + data = { + 'access_token': access_token, + 'department_id': department_id, + } + fetch_child = request.GET.get('fetch_child', None) + if fetch_child: + if fetch_child not in ('true', 'false'): + error_msg = 'fetch_child invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + data['fetch_child'] = 1 if fetch_child == 'true' else 0 + + api_response = requests.get(WORK_WEIXIN_DEPARTMENT_MEMBERS_URL, params=data) + api_response_dic = handler_work_weixin_api_response(api_response) + if not api_response_dic: + logger.error('can not get work weixin department members response') + error_msg = '获取企业微信组织架构成员失败' + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + if WORK_WEIXIN_DEPARTMENT_MEMBERS_FIELD not in api_response_dic: + logger.error(json.dumps(api_response_dic)) + logger.error('can not get userlist in work weixin department members response') + error_msg = '获取企业微信组织架构成员失败' + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + api_user_list = api_response_dic[WORK_WEIXIN_DEPARTMENT_MEMBERS_FIELD] + # todo filter ccnet User database + social_auth_queryset = SocialAuthUser.objects.filter( + provider=WORK_WEIXIN_PROVIDER, uid__contains=WORK_WEIXIN_UID_PREFIX) + for api_user in api_user_list: + uid = WORK_WEIXIN_UID_PREFIX + api_user.get('userid', '') + api_user['contact_email'] = api_user['email'] + # # determine the user exists + if social_auth_queryset.filter(uid=uid).exists(): + api_user['email'] = social_auth_queryset.get(uid=uid).username + else: + api_user['email'] = '' + + return Response(api_response_dic) + + +def _handler_work_weixin_user_data(api_user, social_auth_queryset): + user_id = api_user.get('userid', '') + uid = WORK_WEIXIN_UID_PREFIX + user_id + name = api_user.get('name', None) + error_data = None + if not uid: + error_data = { + 'userid': None, + 'name': None, + 'error_msg': 'userid invalid.', + } + elif social_auth_queryset.filter(uid=uid).exists(): + error_data = { + 'userid': user_id, + 'name': name, + 'error_msg': '用户已存在', + } + + return error_data + + +def _import_user_from_work_weixin(email, api_user): + + api_user['username'] = email + uid = WORK_WEIXIN_UID_PREFIX + api_user.get('userid') + try: + User.objects.create_user(email) + SocialAuthUser.objects.add(email, WORK_WEIXIN_PROVIDER, uid) + update_work_weixin_user_info(api_user) + except Exception as e: + logger.error(e) + return False + + return True + + +class AdminWorkWeixinUsersBatch(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAdminUser,) + throttle_classes = (UserRateThrottle,) + + def post(self, request): + if not admin_work_weixin_departments_check(): + error_msg = 'Feature is not enabled.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + api_user_list = request.data.get(WORK_WEIXIN_DEPARTMENT_MEMBERS_FIELD, None) + if not api_user_list or not isinstance(api_user_list, list): + error_msg = 'userlist invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + success = [] + failed = [] + social_auth_queryset = SocialAuthUser.objects.filter( + provider=WORK_WEIXIN_PROVIDER, uid__contains=WORK_WEIXIN_UID_PREFIX) + + for api_user in api_user_list: + + error_data = _handler_work_weixin_user_data(api_user, social_auth_queryset) + if not error_data: + email = gen_user_virtual_id() + if _import_user_from_work_weixin(email, api_user): + success.append({ + 'userid': api_user.get('userid'), + 'name': api_user.get('name'), + 'email': email, + }) + else: + failed.append({ + 'userid': api_user.get('userid'), + 'name': api_user.get('name'), + 'error_msg': '导入失败' + }) + else: + failed.append(error_data) + + return Response({'success': success, 'failed': failed}) diff --git a/seahub/auth/models.py b/seahub/auth/models.py index 800cc2b1fb..bf4bdcb714 100644 --- a/seahub/auth/models.py +++ b/seahub/auth/models.py @@ -2,6 +2,7 @@ import datetime import hashlib import urllib +import logging # import auth from django.core.exceptions import ImproperlyConfigured @@ -11,8 +12,9 @@ from django.contrib.contenttypes.models import ContentType from django.utils.encoding import smart_str from django.utils.translation import ugettext_lazy as _ +logger = logging.getLogger(__name__) +UNUSABLE_PASSWORD = '!' # This will never be a valid hash -UNUSABLE_PASSWORD = '!' # This will never be a valid hash def get_hexdigest(algorithm, salt, raw_password): """ @@ -33,6 +35,7 @@ def get_hexdigest(algorithm, salt, raw_password): return hashlib.sha1(salt + raw_password).hexdigest() raise ValueError("Got unknown password algorithm type in password.") + def check_password(raw_password, enc_password): """ Returns a boolean of whether the raw_password was correct. Handles @@ -41,6 +44,7 @@ def check_password(raw_password, enc_password): algo, salt, hsh = enc_password.split('$') return hsh == get_hexdigest(algo, salt, raw_password) + class SiteProfileNotAvailable(Exception): pass @@ -70,7 +74,7 @@ class AnonymousUser(object): return not self.__eq__(other) def __hash__(self): - return 1 # instances always return the same hash value + return 1 # instances always return the same hash value def save(self): raise NotImplementedError @@ -86,10 +90,12 @@ class AnonymousUser(object): def _get_groups(self): return self._groups + groups = property(_get_groups) def _get_user_permissions(self): return self._user_permissions + user_permissions = property(_get_user_permissions) def get_group_permissions(self, obj=None): @@ -118,3 +124,49 @@ class AnonymousUser(object): def is_authenticated(self): return False + + +class SocialAuthUserManager(models.Manager): + def add(self, username, provider, uid, extra_data=''): + try: + social_auth_user = self.model(username=username, provider=provider, uid=uid, extra_data=extra_data) + social_auth_user.save() + return social_auth_user + except Exception as e: + logger.error(e) + return None + + def get_by_provider_and_uid(self, provider, uid): + try: + social_auth_user = self.get(provider=provider, uid=uid) + return social_auth_user + except self.model.DoesNotExist: + return None + + def delete_by_username_and_provider(self, username, provider): + self.filter(username=username, provider=provider).delete() + + +class SocialAuthUser(models.Model): + username = models.CharField(max_length=255, db_index=True) + provider = models.CharField(max_length=32) + uid = models.CharField(max_length=255) + extra_data = models.TextField() + objects = SocialAuthUserManager() + + class Meta: + """Meta data""" + app_label = "seahub.work_weixin" + unique_together = ('provider', 'uid') + db_table = 'social_auth_usersocialauth' + + +# # handle signals +from django.dispatch import receiver +from registration.signals import user_deleted + + +@receiver(user_deleted) +def user_deleted_cb(sender, **kwargs): + username = kwargs['username'] + SocialAuthUser.objects.filter(username=username).delete() diff --git a/seahub/auth/views.py b/seahub/auth/views.py index a56a35e458..efb1cdb0fb 100644 --- a/seahub/auth/views.py +++ b/seahub/auth/views.py @@ -185,7 +185,8 @@ def login(request, template_name='registration/login.html', getattr(settings, 'ENABLE_ADFS_LOGIN', False) or \ getattr(settings, 'ENABLE_OAUTH', False) or \ getattr(settings, 'ENABLE_CAS', False) or \ - getattr(settings, 'ENABLE_REMOTE_USER_AUTHENTICATION', False) + getattr(settings, 'ENABLE_REMOTE_USER_AUTHENTICATION', False) or \ + getattr(settings, 'ENABLE_WORK_WEIXIN_OAUTH', False) login_bg_image_path = get_login_bg_image_path() diff --git a/seahub/base/context_processors.py b/seahub/base/context_processors.py index 3f04a18a80..25e610c24a 100644 --- a/seahub/base/context_processors.py +++ b/seahub/base/context_processors.py @@ -49,6 +49,7 @@ try: from seahub.settings import ENABLE_FILE_SCAN except ImportError: ENABLE_FILE_SCAN = False +from seahub.work_weixin.settings import ENABLE_WORK_WEIXIN_DEPARTMENTS def base(request): @@ -128,6 +129,7 @@ def base(request): 'enable_resumable_fileupload': dj_settings.ENABLE_RESUMABLE_FILEUPLOAD, 'service_url': get_service_url().rstrip('/'), 'enable_file_scan': ENABLE_FILE_SCAN, + 'enable_work_weixin_departments': ENABLE_WORK_WEIXIN_DEPARTMENTS, } if request.user.is_staff: diff --git a/seahub/oauth/backends.py b/seahub/oauth/backends.py index c2cac677ec..6cad124101 100644 --- a/seahub/oauth/backends.py +++ b/seahub/oauth/backends.py @@ -4,7 +4,7 @@ from seahub.auth.backends import RemoteUserBackend from seahub.base.accounts import User from registration.models import (notify_admins_on_activate_request, notify_admins_on_register_complete) - +from seahub.work_weixin.settings import ENABLE_WORK_WEIXIN_OAUTH class OauthRemoteUserBackend(RemoteUserBackend): """ @@ -21,9 +21,11 @@ class OauthRemoteUserBackend(RemoteUserBackend): # Create a User object if not already in the database? create_unknown_user = getattr(settings, 'OAUTH_CREATE_UNKNOWN_USER', True) # Create active user by default. - activate_after_creation = getattr(settings, - 'OAUTH_ACTIVATE_USER_AFTER_CREATION', - True) + activate_after_creation = getattr(settings, 'OAUTH_ACTIVATE_USER_AFTER_CREATION', True) + + if ENABLE_WORK_WEIXIN_OAUTH: + create_unknown_user = getattr(settings, 'WORK_WEIXIN_OAUTH_CREATE_UNKNOWN_USER', True) + activate_after_creation = getattr(settings, 'WORK_WEIXIN_OAUTH_ACTIVATE_USER_AFTER_CREATION', True) def get_user(self, username): try: @@ -51,10 +53,10 @@ class OauthRemoteUserBackend(RemoteUserBackend): if self.create_unknown_user: user = User.objects.create_user( email=username, is_active=self.activate_after_creation) - if user and self.activate_after_creation is False: - notify_admins_on_activate_request(user.email) - if user and settings.NOTIFY_ADMIN_AFTER_REGISTRATION is True: - notify_admins_on_register_complete(user.email) + if not self.activate_after_creation: + notify_admins_on_activate_request(username) + elif settings.NOTIFY_ADMIN_AFTER_REGISTRATION: + notify_admins_on_register_complete(username) else: user = None diff --git a/seahub/profile/views.py b/seahub/profile/views.py index 35e69f47e4..67bb203153 100644 --- a/seahub/profile/views.py +++ b/seahub/profile/views.py @@ -22,7 +22,7 @@ from seahub.options.models import UserOptions, CryptoOptionNotSetError from seahub.utils import is_ldap_user from seahub.utils.two_factor_auth import has_two_factor_auth from seahub.views import get_owned_repo_list - +from seahub.work_weixin.utils import work_weixin_oauth_check from seahub.settings import ENABLE_DELETE_ACCOUNT, ENABLE_UPDATE_USER_INFO @login_required @@ -86,12 +86,14 @@ def edit_profile(request): email_inverval = UserOptions.objects.get_file_updates_email_interval(username) email_inverval = email_inverval if email_inverval is not None else 0 - if settings.SOCIAL_AUTH_WEIXIN_WORK_KEY: + if work_weixin_oauth_check(): enable_wechat_work = True - from social_django.models import UserSocialAuth - social_connected = UserSocialAuth.objects.filter( - username=request.user.username, provider='weixin-work').count() > 0 + # from social_django.models import UserSocialAuth + from seahub.auth.models import SocialAuthUser + from seahub.work_weixin.settings import WORK_WEIXIN_PROVIDER + social_connected = SocialAuthUser.objects.filter( + username=request.user.username, provider=WORK_WEIXIN_PROVIDER).count() > 0 else: enable_wechat_work = False social_connected = False diff --git a/seahub/settings.py b/seahub/settings.py index 0c77dc402a..3713a3433a 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -257,6 +257,7 @@ INSTALLED_APPS = ( 'seahub.repo_tags', 'seahub.file_tags', 'seahub.related_files', + 'seahub.work_weixin', ) # Enable or disable view File Scan @@ -300,6 +301,11 @@ SOCIAL_AUTH_PIPELINE = ( ENABLE_OAUTH = False ENABLE_WATERMARK = False +# allow user scan the work weixin qrcode to login +ENABLE_WORK_WEIXIN_OAUTH = False +# allow seafile admin import user from work weixin +ENABLE_WORK_WEIXIN_DEPARTMENTS = False + # allow user to clean library trash ENABLE_USER_CLEAN_TRASH = True @@ -906,5 +912,5 @@ if ENABLE_REMOTE_USER_AUTHENTICATION: MIDDLEWARE_CLASSES += ('seahub.auth.middleware.SeafileRemoteUserMiddleware',) AUTHENTICATION_BACKENDS += ('seahub.auth.backends.SeafileRemoteUserBackend',) -if ENABLE_OAUTH: +if ENABLE_OAUTH or ENABLE_WORK_WEIXIN_OAUTH: AUTHENTICATION_BACKENDS += ('seahub.oauth.backends.OauthRemoteUserBackend',) diff --git a/seahub/templates/js/sysadmin-templates.html b/seahub/templates/js/sysadmin-templates.html index 8ae1b0b79f..c28ea22c08 100644 --- a/seahub/templates/js/sysadmin-templates.html +++ b/seahub/templates/js/sysadmin-templates.html @@ -83,6 +83,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" %} @@ -107,6 +113,12 @@
  • {% endif %} + {% if is_default_admin and enable_work_weixin_departments %} +
  • + 企业微信集成 +
  • + {% endif %} + <% if (cur_tab == 'libraries') { %> <% if (option == 'all') { %> diff --git a/seahub/templates/sysadmin/base.html b/seahub/templates/sysadmin/base.html index ea2747c106..661a752936 100644 --- a/seahub/templates/sysadmin/base.html +++ b/seahub/templates/sysadmin/base.html @@ -124,6 +124,12 @@ {% endif %} + {% if is_default_admin and enable_work_weixin_departments %} +
  • + 企业微信集成 +
  • + {% endif %} + {% endblock %} diff --git a/seahub/templates/sysadmin/sys_file_scan_records_react.html b/seahub/templates/sysadmin/sys_file_scan_records_react.html deleted file mode 100644 index ee472d4eea..0000000000 --- a/seahub/templates/sysadmin/sys_file_scan_records_react.html +++ /dev/null @@ -1,33 +0,0 @@ -{% 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/templates/sysadmin/sysadmin_backbone.html b/seahub/templates/sysadmin/sysadmin_backbone.html index 598b1aff29..9e4104fe96 100644 --- a/seahub/templates/sysadmin/sysadmin_backbone.html +++ b/seahub/templates/sysadmin/sysadmin_backbone.html @@ -106,6 +106,8 @@ app["pageOptions"] = { file_audit_enabled: {% if file_audit_enabled %} true {% else %} false {% endif %}, cur_note: {% if request.cur_note %} {'id': '{{ request.cur_note.id }}'} {% else %} null {% endif %}, is_default_admin: {% if is_default_admin %} true {% else %} false {% endif %}, + enable_file_scan: {% if enable_file_scan %} true {% else %} false {% endif %}, + enable_work_weixin_departments: {% if enable_work_weixin_departments %} true {% else %} false {% endif %}, admin_permissions: { "can_view_system_info": {% if user.admin_permissions.can_view_system_info %} true {% else %} false {% endif %}, "can_view_statistic": {% if user.admin_permissions.can_view_statistic %} true {% else %} false {% endif %}, diff --git a/seahub/templates/sysadmin/sysadmin_react_app.html b/seahub/templates/sysadmin/sysadmin_react_app.html new file mode 100644 index 0000000000..7b7ae200ca --- /dev/null +++ b/seahub/templates/sysadmin/sysadmin_react_app.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 2179dd4b49..e9fd491d5a 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -133,7 +133,9 @@ from seahub.api2.endpoints.admin.group_owned_libraries import AdminGroupOwnedLib AdminGroupOwnedLibrary from seahub.api2.endpoints.admin.user_activities import UserActivitiesView 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.work_weixin import AdminWorkWeixinDepartments, \ + AdminWorkWeixinDepartmentMembers, AdminWorkWeixinUsersBatch urlpatterns = [ url(r'^accounts/', include('seahub.base.registration_urls')), @@ -507,6 +509,7 @@ urlpatterns = [ url(r'^invite/', include('seahub.invitations.urls', app_name='invitations', namespace='invitations')), url(r'^terms/', include('termsandconditions.urls')), url(r'^published/', include('seahub.wiki.urls', app_name='wiki', namespace='wiki')), + url(r'^work-weixin/', include('seahub.work_weixin.urls')), # Must specify a namespace if specifying app_name. url(r'^wikis/', include('seahub.wiki.urls', app_name='wiki', namespace='wiki-unused')), url(r'^drafts/', include('seahub.drafts.urls', app_name='drafts', namespace='drafts')), @@ -521,6 +524,11 @@ urlpatterns = [ ## admin::notifications url(r'^api/v2.1/admin/notifications/$', AdminNotificationsView.as_view(), name='api-2.1-admin-notifications'), + ## admin::work weixin departments + url(r'^api/v2.1/admin/work-weixin/departments/$', AdminWorkWeixinDepartments.as_view(), name='api-v2.1-admin-work-weixin-departments'), + url(r'^api/v2.1/admin/work-weixin/departments/(?P\d+)/members/$', AdminWorkWeixinDepartmentMembers.as_view(), name='api-v2.1-admin-work-weixin-department-members'), + url(r'^api/v2.1/admin/work-weixin/users/batch/$', AdminWorkWeixinUsersBatch.as_view(), name='api-v2.1-admin-work-weixin-users'), + ### system admin ### url(r'^sysadmin/$', sysadmin, name='sysadmin'), url(r'^sys/settings/$', sys_settings, name='sys_settings'), @@ -573,6 +581,7 @@ urlpatterns = [ url(r'^sys/invitationadmin/remove/$', sys_invitation_remove, name='sys_invitation_remove'), url(r'^sys/sudo/', sys_sudo_mode, name='sys_sudo_mode'), url(r'^sys/check-license/', sys_check_license, name='sys_check_license'), + url(r'^sys/work-weixin/departments/$', sysadmin_react_fake_view, name="sys_work_weixin_departments"), url(r'^useradmin/add/$', user_add, name="user_add"), url(r'^useradmin/remove/(?P[^/]+)/$', user_remove, name="user_remove"), url(r'^useradmin/removetrial/(?P[^/]+)/$', remove_trial, name="remove_trial"), @@ -598,7 +607,7 @@ 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"), + url(r'^sys/file-scan-records/$', sysadmin_react_fake_view, name="sys_file_scan_records"), ] from seahub.utils import EVENTS_ENABLED @@ -729,7 +738,6 @@ if getattr(settings, 'ENABLE_CAS', False): url(r'^accounts/cas-callback/$', cas_callback, name='cas_ng_proxy_callback'), ] - from seahub.social_core.views import ( weixin_work_cb, weixin_work_3rd_app_install, weixin_work_3rd_app_install_cb ) diff --git a/seahub/utils/auth.py b/seahub/utils/auth.py index 5155b078df..7cee541371 100644 --- a/seahub/utils/auth.py +++ b/seahub/utils/auth.py @@ -2,6 +2,9 @@ import os from seahub.settings import LOGIN_BG_IMAGE_PATH, MEDIA_ROOT from seahub.utils import gen_token +VIRTUAL_ID_EMAIL_DOMAIN = '@auth.local' + + def get_login_bg_image_path(): """ Return custom background image path if it exists, otherwise return default background image path. """ @@ -12,10 +15,12 @@ def get_login_bg_image_path(): login_bg_image_path = custom_login_bg_image_path return login_bg_image_path + def get_custom_login_bg_image_path(): """ Ensure consistency between utils and api. """ return 'custom/login-bg.jpg' + def gen_user_virtual_id(): - return gen_token(max_length=32) + '@auth.local' + return gen_token(max_length=32) + VIRTUAL_ID_EMAIL_DOMAIN diff --git a/seahub/views/sso.py b/seahub/views/sso.py index 1364d2f1c7..22ee8471e9 100644 --- a/seahub/views/sso.py +++ b/seahub/views/sso.py @@ -35,6 +35,9 @@ def sso(request): if getattr(settings, 'ENABLE_CAS', False): return HttpResponseRedirect(reverse('cas_ng_login') + next_param) + if getattr(settings, 'ENABLE_WORK_WEIXIN_OAUTH', False): + return HttpResponseRedirect(reverse('work_weixin_oauth_login') + next_param) + return HttpResponseRedirect(next_page) def shib_login(request): diff --git a/seahub/views/sysadmin.py b/seahub/views/sysadmin.py index 321bfd8a46..c7a07d8b33 100644 --- a/seahub/views/sysadmin.py +++ b/seahub/views/sysadmin.py @@ -98,6 +98,8 @@ try: from seahub.settings import ENABLE_FILE_SCAN except ImportError: ENABLE_FILE_SCAN = False +from seahub.work_weixin.settings import ENABLE_WORK_WEIXIN_DEPARTMENTS + logger = logging.getLogger(__name__) @@ -130,23 +132,24 @@ def sysadmin(request): 'file_audit_enabled': FILE_AUDIT_ENABLED, 'enable_limit_ipaddress': ENABLE_LIMIT_IPADDRESS, 'trash_repos_expire_days': expire_days if expire_days > 0 else 30, + 'enable_file_scan': ENABLE_FILE_SCAN, + 'enable_work_weixin_departments': ENABLE_WORK_WEIXIN_DEPARTMENTS, }) - @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, - }) +def sysadmin_react_fake_view(request): + return render(request, 'sysadmin/sysadmin_react_app.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, + 'enable_work_weixin_departments': ENABLE_WORK_WEIXIN_DEPARTMENTS, + }) @login_required @sys_staff_required @@ -2674,4 +2677,3 @@ def sys_delete_terms(request, pk): messages.success(request, _('Successfully deleted 1 item')) return HttpResponseRedirect(reverse('sys_terms_admin')) - diff --git a/seahub/work_weixin/__init__.py b/seahub/work_weixin/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/seahub/work_weixin/settings.py b/seahub/work_weixin/settings.py new file mode 100644 index 0000000000..3a5045f8d9 --- /dev/null +++ b/seahub/work_weixin/settings.py @@ -0,0 +1,32 @@ +# Copyright (c) 2012-2019 Seafile Ltd. +# encoding: utf-8 +from django.conf import settings + +# # work weixin base +WORK_WEIXIN_CORP_ID = getattr(settings, 'WORK_WEIXIN_CORP_ID', '') +WORK_WEIXIN_AGENT_SECRET = getattr(settings, 'WORK_WEIXIN_AGENT_SECRET', '') +WORK_WEIXIN_ACCESS_TOKEN_URL = getattr(settings, 'WORK_WEIXIN_ACCESS_TOKEN_URL', + 'https://qyapi.weixin.qq.com/cgi-bin/gettoken') + +# # admin work weixin departments +ENABLE_WORK_WEIXIN_DEPARTMENTS = getattr(settings, 'ENABLE_WORK_WEIXIN_DEPARTMENTS', False) +WORK_WEIXIN_DEPARTMENTS_URL = getattr(settings, 'WORK_WEIXIN_DEPARTMENTS_URL', + 'https://qyapi.weixin.qq.com/cgi-bin/department/list') +WORK_WEIXIN_DEPARTMENT_MEMBERS_URL = getattr(settings, 'WORK_WEIXIN_DEPARTMENT_MEMBERS_URL', + 'https://qyapi.weixin.qq.com/cgi-bin/user/list') + +# # work weixin oauth +WORK_WEIXIN_AGENT_ID = getattr(settings, 'WORK_WEIXIN_AGENT_ID', '') +ENABLE_WORK_WEIXIN_OAUTH = getattr(settings, 'ENABLE_WORK_WEIXIN_OAUTH', False) +WORK_WEIXIN_UID_PREFIX = WORK_WEIXIN_CORP_ID + '_' +AUTO_UPDATE_WORK_WEIXIN_USER_INFO = getattr(settings, 'AUTO_UPDATE_WORK_WEIXIN_USER_INFO', False) +WORK_WEIXIN_AUTHORIZATION_URL = getattr(settings, 'WORK_WEIXIN_AUTHORIZATION_URL', + 'https://open.work.weixin.qq.com/wwopen/sso/qrConnect') +WORK_WEIXIN_GET_USER_INFO_URL = getattr(settings, 'WORK_WEIXIN_GET_USER_INFO_URL', + 'https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo') +WORK_WEIXIN_GET_USER_PROFILE_URL = getattr(settings, 'WORK_WEIXIN_GET_USER_PROFILE_URL', + 'https://qyapi.weixin.qq.com/cgi-bin/user/get') + +# # constants + +WORK_WEIXIN_PROVIDER = 'work-weixin' diff --git a/seahub/work_weixin/urls.py b/seahub/work_weixin/urls.py new file mode 100644 index 0000000000..df0acccf9a --- /dev/null +++ b/seahub/work_weixin/urls.py @@ -0,0 +1,14 @@ +# Copyright (c) 2012-2019 Seafile Ltd. +# encoding: utf-8 + +from django.conf.urls import url +from seahub.work_weixin.views import work_weixin_oauth_login, work_weixin_oauth_callback, \ + work_weixin_oauth_connect, work_weixin_oauth_connect_callback, work_weixin_oauth_disconnect + +urlpatterns = [ + url(r'oauth-login/$', work_weixin_oauth_login, name='work_weixin_oauth_login'), + url(r'oauth-callback/$', work_weixin_oauth_callback, name='work_weixin_oauth_callback'), + url(r'oauth-connect/$', work_weixin_oauth_connect, name='work_weixin_oauth_connect'), + url(r'oauth-connect-callback/$', work_weixin_oauth_connect_callback, name='work_weixin_oauth_connect_callback'), + url(r'oauth-disconnect/$', work_weixin_oauth_disconnect, name='work_weixin_oauth_disconnect'), +] diff --git a/seahub/work_weixin/utils.py b/seahub/work_weixin/utils.py new file mode 100644 index 0000000000..c2e8a59dd2 --- /dev/null +++ b/seahub/work_weixin/utils.py @@ -0,0 +1,119 @@ +# Copyright (c) 2012-2019 Seafile Ltd. +# encoding: utf-8 + +import logging +import json +import requests + +from django.core.cache import cache +from seahub.utils import normalize_cache_key +from seahub.work_weixin.settings import WORK_WEIXIN_CORP_ID, WORK_WEIXIN_AGENT_SECRET, \ + WORK_WEIXIN_ACCESS_TOKEN_URL, ENABLE_WORK_WEIXIN_DEPARTMENTS, \ + WORK_WEIXIN_DEPARTMENTS_URL, WORK_WEIXIN_DEPARTMENT_MEMBERS_URL, \ + ENABLE_WORK_WEIXIN_OAUTH, WORK_WEIXIN_AGENT_ID, WORK_WEIXIN_AUTHORIZATION_URL, \ + WORK_WEIXIN_GET_USER_INFO_URL, WORK_WEIXIN_GET_USER_PROFILE_URL +from seahub.profile.models import Profile + +logger = logging.getLogger(__name__) +WORK_WEIXIN_ACCESS_TOKEN_CACHE_KEY = 'WORK_WEIXIN_ACCESS_TOKEN' + + +# from social_django.models import UserSocialAuth +# get access_token: https://work.weixin.qq.com/api/doc#90000/90135/91039 + + +def get_work_weixin_access_token(): + cache_key = normalize_cache_key(WORK_WEIXIN_ACCESS_TOKEN_CACHE_KEY) + access_token = cache.get(cache_key, None) + + if not access_token: + data = { + 'corpid': WORK_WEIXIN_CORP_ID, + 'corpsecret': WORK_WEIXIN_AGENT_SECRET, + } + api_response = requests.get(WORK_WEIXIN_ACCESS_TOKEN_URL, params=data) + api_response_dic = handler_work_weixin_api_response(api_response) + if not api_response_dic: + logger.error('can not get work weixin response') + return None + access_token = api_response_dic.get('access_token', None) + expires_in = api_response_dic.get('expires_in', None) + if access_token and expires_in: + cache.set(cache_key, access_token, expires_in) + + return access_token + + +def handler_work_weixin_api_response(response): + try: + response = response.json() + except ValueError: + logger.error(response) + return None + + errcode = response.get('errcode', None) + if errcode != 0: + logger.error(json.dumps(response)) + return None + return response + + +def work_weixin_base_check(): + if not WORK_WEIXIN_CORP_ID or not WORK_WEIXIN_AGENT_SECRET or not WORK_WEIXIN_ACCESS_TOKEN_URL: + logger.error('work weixin base relevant settings invalid.') + logger.error('WORK_WEIXIN_CORP_ID: %s' % WORK_WEIXIN_CORP_ID) + logger.error('WORK_WEIXIN_AGENT_SECRET: %s' % WORK_WEIXIN_AGENT_SECRET) + logger.error('WORK_WEIXIN_ACCESS_TOKEN_URL: %s' % WORK_WEIXIN_ACCESS_TOKEN_URL) + return False + return True + + +def work_weixin_oauth_check(): + if not work_weixin_base_check(): + return False + + if not ENABLE_WORK_WEIXIN_OAUTH: + logger.error('work weixin oauth not enabled.') + return False + else: + if not WORK_WEIXIN_AGENT_ID \ + or not WORK_WEIXIN_GET_USER_INFO_URL \ + or not WORK_WEIXIN_AUTHORIZATION_URL \ + or not WORK_WEIXIN_GET_USER_PROFILE_URL: + logger.error('work weixin oauth relevant settings invalid.') + logger.error('WORK_WEIXIN_AGENT_ID: %s' % WORK_WEIXIN_AGENT_ID) + logger.error('WORK_WEIXIN_GET_USER_INFO_URL: %s' % WORK_WEIXIN_GET_USER_INFO_URL) + logger.error('WORK_WEIXIN_AUTHORIZATION_URL: %s' % WORK_WEIXIN_AUTHORIZATION_URL) + logger.error('WORK_WEIXIN_GET_USER_PROFILE_URL: %s' % WORK_WEIXIN_GET_USER_PROFILE_URL) + return False + + return True + + +def admin_work_weixin_departments_check(): + if not work_weixin_base_check(): + return False + + if not ENABLE_WORK_WEIXIN_DEPARTMENTS: + logger.error('admin work weixin departments not enabled.') + return False + else: + if not WORK_WEIXIN_DEPARTMENTS_URL \ + or not WORK_WEIXIN_DEPARTMENT_MEMBERS_URL: + logger.error('admin work weixin departments relevant settings invalid.') + logger.error('WORK_WEIXIN_DEPARTMENTS_URL: %s' % WORK_WEIXIN_DEPARTMENTS_URL) + logger.error('WORK_WEIXIN_DEPARTMENT_MEMBERS_URL: %s' % WORK_WEIXIN_DEPARTMENT_MEMBERS_URL) + return False + + return True + + +def update_work_weixin_user_info(api_user): + email = api_user.get('username') + try: + # update additional user info + nickname = api_user.get("name", None) + if nickname is not None: + Profile.objects.add_or_update(email, nickname) + except Exception as e: + logger.error(e) diff --git a/seahub/work_weixin/views.py b/seahub/work_weixin/views.py new file mode 100644 index 0000000000..9abc6431bf --- /dev/null +++ b/seahub/work_weixin/views.py @@ -0,0 +1,228 @@ +# Copyright (c) 2012-2019 Seafile Ltd. +# encoding: utf-8 + +import uuid +import logging +import requests +import urllib + +from django.http import HttpResponseRedirect +from django.utils.translation import ugettext as _ +from seahub.auth.decorators import login_required +from seahub.utils import get_service_url +from seahub.api2.utils import get_api_token +from seahub import auth +from seahub.utils import render_error +from seahub.base.accounts import User +from seahub.work_weixin.settings import WORK_WEIXIN_AUTHORIZATION_URL, WORK_WEIXIN_CORP_ID, \ + WORK_WEIXIN_AGENT_ID, WORK_WEIXIN_PROVIDER, \ + WORK_WEIXIN_GET_USER_INFO_URL, WORK_WEIXIN_GET_USER_PROFILE_URL, WORK_WEIXIN_UID_PREFIX, \ + AUTO_UPDATE_WORK_WEIXIN_USER_INFO +from seahub.work_weixin.utils import work_weixin_oauth_check, get_work_weixin_access_token, \ + handler_work_weixin_api_response, update_work_weixin_user_info +from seahub.utils.auth import gen_user_virtual_id, VIRTUAL_ID_EMAIL_DOMAIN +from seahub.auth.models import SocialAuthUser +from django.core.urlresolvers import reverse + +logger = logging.getLogger(__name__) + + +# # uid = corpid + '_' + userid +# from social_django.models import UserSocialAuth + + +def work_weixin_oauth_login(request): + if not work_weixin_oauth_check(): + return render_error(request, _('Feature is not enabled.')) + + state = str(uuid.uuid4()) + request.session['work_weixin_oauth_state'] = state + request.session['work_weixin_oauth_redirect'] = request.GET.get(auth.REDIRECT_FIELD_NAME, '/') + + data = { + 'appid': WORK_WEIXIN_CORP_ID, + 'agentid': WORK_WEIXIN_AGENT_ID, + 'redirect_uri': get_service_url().rstrip('/') + reverse('work_weixin_oauth_callback'), + 'state': state, + } + authorization_url = WORK_WEIXIN_AUTHORIZATION_URL + '?' + urllib.urlencode(data) + + return HttpResponseRedirect(authorization_url) + + +def work_weixin_oauth_callback(request): + if not work_weixin_oauth_check(): + return render_error(request, _('Feature is not enabled.')) + + code = request.GET.get('code', None) + state = request.GET.get('state', None) + if state != request.session.get('work_weixin_oauth_state', None) or not code: + logger.error('can not get right code or state from work weixin request') + return render_error(request, _('Error, please contact administrator.')) + + access_token = get_work_weixin_access_token() + if not access_token: + logger.error('can not get work weixin access_token') + return render_error(request, _('Error, please contact administrator.')) + + data = { + 'access_token': access_token, + 'code': code, + } + api_response = requests.get(WORK_WEIXIN_GET_USER_INFO_URL, params=data) + api_response_dic = handler_work_weixin_api_response(api_response) + if not api_response_dic: + logger.error('can not get work weixin user info') + return render_error(request, _('Error, please contact administrator.')) + + if not api_response_dic.get('UserId', None): + logger.error('can not get UserId in work weixin user info response') + return render_error(request, _('Error, please contact administrator.')) + + user_id = api_response_dic.get('UserId') + uid = WORK_WEIXIN_UID_PREFIX + user_id + + work_weixin_user = SocialAuthUser.objects.get_by_provider_and_uid(WORK_WEIXIN_PROVIDER, uid) + if work_weixin_user: + email = work_weixin_user.username + is_new_user = False + else: + email = gen_user_virtual_id() + SocialAuthUser.objects.add(email, WORK_WEIXIN_PROVIDER, uid) + is_new_user = True + + try: + user = auth.authenticate(remote_user=email) + except User.DoesNotExist: + user = None + + if not user: + return render_error( + request, _('Error, new user registration is not allowed, please contact administrator.')) + + if is_new_user or AUTO_UPDATE_WORK_WEIXIN_USER_INFO: + # update user info + user_info_data = { + 'access_token': access_token, + 'userid': user_id, + } + user_info_api_response = requests.get(WORK_WEIXIN_GET_USER_PROFILE_URL, params=user_info_data) + user_info_api_response_dic = handler_work_weixin_api_response(user_info_api_response) + if user_info_api_response_dic: + api_user = user_info_api_response_dic + api_user['username'] = email + api_user['contact_email'] = api_user['email'] + update_work_weixin_user_info(api_user) + + if not user.is_active: + return render_error( + request, _('Your account is created successfully, please wait for administrator to activate your account.')) + + # User is valid. Set request.user and persist user in the session + # by logging the user in. + request.user = user + auth.login(request, user) + + # generate auth token for Seafile client + api_token = get_api_token(request) + + # redirect user to page + response = HttpResponseRedirect(request.session.get('work_weixin_oauth_redirect', '/')) + response.set_cookie('seahub_auth', user.username + '@' + api_token.key) + return response + + +@login_required +def work_weixin_oauth_connect(request): + if not work_weixin_oauth_check(): + return render_error(request, _('Feature is not enabled.')) + + state = str(uuid.uuid4()) + request.session['work_weixin_oauth_connect_state'] = state + request.session['work_weixin_oauth_connect_redirect'] = request.GET.get(auth.REDIRECT_FIELD_NAME, '/') + + data = { + 'appid': WORK_WEIXIN_CORP_ID, + 'agentid': WORK_WEIXIN_AGENT_ID, + 'redirect_uri': get_service_url().rstrip('/') + reverse('work_weixin_oauth_connect_callback'), + 'state': state, + } + authorization_url = WORK_WEIXIN_AUTHORIZATION_URL + '?' + urllib.urlencode(data) + + return HttpResponseRedirect(authorization_url) + + +@login_required +def work_weixin_oauth_connect_callback(request): + if not work_weixin_oauth_check(): + return render_error(request, _('Feature is not enabled.')) + + code = request.GET.get('code', None) + state = request.GET.get('state', None) + if state != request.session.get('work_weixin_oauth_connect_state', None) or not code: + logger.error('can not get right code or state from work weixin request') + return render_error(request, _('Error, please contact administrator.')) + + access_token = get_work_weixin_access_token() + if not access_token: + logger.error('can not get work weixin access_token') + return render_error(request, _('Error, please contact administrator.')) + + data = { + 'access_token': access_token, + 'code': code, + } + api_response = requests.get(WORK_WEIXIN_GET_USER_INFO_URL, params=data) + api_response_dic = handler_work_weixin_api_response(api_response) + if not api_response_dic: + logger.error('can not get work weixin user info') + return render_error(request, _('Error, please contact administrator.')) + + if not api_response_dic.get('UserId', None): + logger.error('can not get UserId in work weixin user info response') + return render_error(request, _('Error, please contact administrator.')) + + user_id = api_response_dic.get('UserId') + uid = WORK_WEIXIN_UID_PREFIX + user_id + email = request.user.username + + work_weixin_user = SocialAuthUser.objects.get_by_provider_and_uid(WORK_WEIXIN_PROVIDER, uid) + if work_weixin_user: + logger.error('work weixin account already exists %s' % user_id) + return render_error(request, '出错了,此企业微信账号已被绑定') + + SocialAuthUser.objects.add(email, WORK_WEIXIN_PROVIDER, uid) + + if AUTO_UPDATE_WORK_WEIXIN_USER_INFO: + # update user info + user_info_data = { + 'access_token': access_token, + 'userid': user_id, + } + user_info_api_response = requests.get(WORK_WEIXIN_GET_USER_PROFILE_URL, params=user_info_data) + user_info_api_response_dic = handler_work_weixin_api_response(user_info_api_response) + if user_info_api_response_dic: + api_user = user_info_api_response_dic + api_user['username'] = email + api_user['contact_email'] = api_user['email'] + update_work_weixin_user_info(api_user) + + # redirect user to page + response = HttpResponseRedirect(request.session.get('work_weixin_oauth_connect_redirect', '/')) + return response + + +@login_required +def work_weixin_oauth_disconnect(request): + if not work_weixin_oauth_check(): + return render_error(request, _('Feature is not enabled.')) + + username = request.user.username + if username[-(len(VIRTUAL_ID_EMAIL_DOMAIN)):] == VIRTUAL_ID_EMAIL_DOMAIN: + return render_error(request, '出错了,此账号不能解绑企业微信') + + SocialAuthUser.objects.delete_by_username_and_provider(username, WORK_WEIXIN_PROVIDER) + + # redirect user to page + response = HttpResponseRedirect(request.GET.get(auth.REDIRECT_FIELD_NAME, '/')) + return response