@@ -342,6 +373,50 @@ class LinkCreation extends React.Component {
);
})}
+
+
+ )}
+ {type !== 'batch' && (
+
+
+
+
+
+
+
+
+ {isEmailConfigured && (
+
+
+
+ )}
+ {this.state.currentScope === 'specific_users' &&
+
+
+
+ }
+ {this.state.currentScope === 'specific_emails' &&
+
+
+
+ }
)}
{this.state.errorInfo &&
{gettext(this.state.errorInfo)}}
diff --git a/frontend/src/components/share-link-panel/link-details.js b/frontend/src/components/share-link-panel/link-details.js
index 8082688d98c..502d5dc1b27 100644
--- a/frontend/src/components/share-link-panel/link-details.js
+++ b/frontend/src/components/share-link-panel/link-details.js
@@ -13,6 +13,8 @@ import toaster from '../toast';
import SendLink from '../send-link';
import SharedLink from '../shared-link';
import SetLinkExpiration from '../set-link-expiration';
+import ShareLinkScopeEditor from '../select-editor/share-link-scope-editor';
+import { shareLinkAPI } from '../../utils/share-link-api';
const propTypes = {
sharedLinkInfo: PropTypes.object.isRequired,
@@ -24,7 +26,8 @@ const propTypes = {
showLinkDetails: PropTypes.func.isRequired,
updateLink: PropTypes.func.isRequired,
deleteLink: PropTypes.func.isRequired,
- closeShareDialog: PropTypes.func.isRequired
+ closeShareDialog: PropTypes.func.isRequired,
+ setMode: PropTypes.func,
};
class LinkDetails extends React.Component {
@@ -40,7 +43,12 @@ class LinkDetails extends React.Component {
expDate: null,
isOpIconShown: false,
isLinkDeleteDialogOpen: false,
- isSendLinkShown: false
+ isSendLinkShown: false,
+
+ isScopeOpIconShown: false,
+ currentScope: this.props.sharedLinkInfo.user_scope, // all_users, specific_users, spcific_emails
+ selectedOption: null,
+ isSpecificUserChecked: false,
};
}
@@ -95,7 +103,7 @@ class LinkDetails extends React.Component {
const { sharedLinkInfo } = this.props;
const { expType, expireDays, expDate } = this.state;
let expirationTime = '';
- if (expType == 'by-days') {
+ if (expType === 'by-days') {
expirationTime = moment().add(parseInt(expireDays), 'days').format();
} else {
expirationTime = expDate.format();
@@ -119,6 +127,14 @@ class LinkDetails extends React.Component {
this.setState({isOpIconShown: false});
};
+ handleMouseOverScope = () => {
+ this.setState({isScopeOpIconShown: true});
+ };
+
+ handleMouseOutScope = () => {
+ this.setState({isScopeOpIconShown: false});
+ };
+
changePerm = (permOption) => {
const { sharedLinkInfo } = this.props;
const { permissionDetails } = Utils.getShareLinkPermissionObject(permOption.value);
@@ -148,15 +164,36 @@ class LinkDetails extends React.Component {
this.props.showLinkDetails(null);
};
+ changeScope = (scope) => {
+ shareLinkAPI.updateShareLinkScope(this.props.sharedLinkInfo.token, scope).then((res) => {
+ let sharedLinkInfo = new ShareLink(res.data);
+ this.setState({sharedLinkInfo: sharedLinkInfo, currentScope: sharedLinkInfo.user_scope});
+ let message = gettext('Success');
+ toaster.success(message);
+ }).catch((error) => {
+ let errMessage = Utils.getErrorMsg(error);
+ toaster.danger(errMessage);
+ });
+ };
+
+ onUserAuth = () => {
+ this.props.setMode('linkUserAuth', this.state.sharedLinkInfo);
+ };
+
+ onEmailAuth = () => {
+ this.props.setMode('linkEmailAuth', this.state.sharedLinkInfo);
+ };
+
+
render() {
const { sharedLinkInfo, permissionOptions } = this.props;
- const { isOpIconShown } = this.state;
+ const { isOpIconShown, isScopeOpIconShown, currentScope } = this.state;
const currentPermission = Utils.getShareLinkPermissionStr(sharedLinkInfo.permissions);
this.permOptions = permissionOptions.map(item => {
return {
value: item,
text: Utils.getShareLinkPermissionObject(item).text,
- isSelected: item == currentPermission
+ isSelected: item === currentPermission
};
});
const currentSelectedPermOption = this.permOptions.filter(item => item.isSelected)[0];
@@ -225,7 +262,7 @@ class LinkDetails extends React.Component {
expDate={this.state.expDate}
onExpDateChanged={this.onExpDateChanged}
/>
-
+
@@ -247,6 +284,17 @@ class LinkDetails extends React.Component {
>
)}
+ <>
+
{gettext('Scope')}
+
+
+
+ >
{(canSendShareLinkEmail && !this.state.isSendLinkShown) &&
@@ -271,6 +319,12 @@ class LinkDetails extends React.Component {
toggleDialog={this.toggleLinkDeleteDialog}
/>
}
+ {currentScope === 'specific_users' && !this.state.isSendLinkShown &&
+
+ }
+ {currentScope === 'specific_emails' && !this.state.isSendLinkShown &&
+
+ }
);
}
diff --git a/frontend/src/components/share-link-panel/link-email-auth.js b/frontend/src/components/share-link-panel/link-email-auth.js
new file mode 100644
index 00000000000..ecc99fff8d9
--- /dev/null
+++ b/frontend/src/components/share-link-panel/link-email-auth.js
@@ -0,0 +1,212 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { gettext } from '../../utils/constants';
+import { Button } from 'reactstrap';
+import { Utils } from '../../utils/utils';
+import '../../css/invitations.css';
+
+import '../../css/share-to-user.css';
+import { shareLinkAPI } from '../../utils/share-link-api';
+
+class EmailItem extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isOperationShow: false,
+ };
+ }
+
+ onMouseEnter = () => {
+ this.setState({isOperationShow: true});
+ };
+
+ onMouseLeave = () => {
+ this.setState({isOperationShow: false});
+ };
+
+ deleteItem = () => {
+ const { item } = this.props;
+ this.props.deleteItem(item);
+
+ };
+
+ render() {
+ let item = this.props.item;
+ return (
+
+
+
+ {item}
+
+ |
+
+
+
+ |
+
+ );
+ }
+}
+
+EmailItem.propTypes = {
+ repoID: PropTypes.string.isRequired,
+ item: PropTypes.object.isRequired,
+ deleteItem: PropTypes.func.isRequired,
+};
+
+class EmailList extends React.Component {
+
+ render() {
+ let items = this.props.items;
+ return (
+
+ {items.map((item, index) => {
+ return (
+
+ );
+ })}
+
+ );
+ }
+}
+
+EmailList.propTypes = {
+ repoID: PropTypes.string.isRequired,
+ items: PropTypes.array.isRequired,
+ linkToken: PropTypes.string,
+ deleteItem: PropTypes.func,
+};
+
+const propTypes = {
+ repoID: PropTypes.string.isRequired,
+ linkToken: PropTypes.string,
+ setMode: PropTypes.func,
+ path: PropTypes.string,
+ hideHead: PropTypes.bool
+};
+
+class LinkEmailAuth extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ inputEmails: null,
+ authEmails: []
+ };
+ }
+
+
+ componentDidMount() {
+ this.listLinkAuthUsers();
+ }
+
+ listLinkAuthUsers = () => {
+ const { linkToken, path } = this.props;
+ shareLinkAPI.listShareLinkAuthEmails(linkToken, path).then(res => {
+ this.setState({authEmails: res.data.auth_list});
+ });
+ };
+
+ addLinkAuthUsers = () => {
+ const { linkToken, path } = this.props;
+ const { inputEmails } = this.state;
+ shareLinkAPI.addShareLinkAuthEmails(linkToken, inputEmails, path).then(res => {
+ let authEmails = this.state.authEmails;
+ let newAuthUsers = [...authEmails, ...res.data.auth_list];
+ this.setState({
+ authEmails: newAuthUsers,
+ inputEmails: null
+ });
+ this.refs.userSelect.clearSelect();
+ });
+ };
+
+ deleteItem = (email) => {
+ const { linkToken, path } = this.props;
+ let emails = [email, ];
+ shareLinkAPI.deleteShareLinkAuthEmails(linkToken, emails, path).then(res => {
+ let authEmails = this.state.authEmails.filter(e => {
+ return e !== email;
+ });
+ this.setState({
+ authEmails: authEmails
+ });
+ });
+ };
+
+ goBack = () => {
+ this.props.setMode('displayLinkDetails');
+ };
+
+ handleInputChange = (e) => {
+ this.setState({
+ inputEmails: e.target.value
+ });
+ };
+
+ render() {
+ let { authEmails, inputEmails } = this.state;
+ const thead = (
+
+
+
+
+ {gettext('Links')}
+ |
+ |
+
+
+ );
+ return (
+
+
+
+
+ );
+ }
+}
+
+LinkEmailAuth.propTypes = propTypes;
+
+export default LinkEmailAuth;
\ No newline at end of file
diff --git a/frontend/src/components/share-link-panel/link-user-auth.js b/frontend/src/components/share-link-panel/link-user-auth.js
new file mode 100644
index 00000000000..4b8cef30ee1
--- /dev/null
+++ b/frontend/src/components/share-link-panel/link-user-auth.js
@@ -0,0 +1,224 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { gettext } from '../../utils/constants';
+import { Button } from 'reactstrap';
+import { Utils } from '../../utils/utils';
+import UserSelect from '../user-select';
+import '../../css/invitations.css';
+
+import '../../css/share-to-user.css';
+import { shareLinkAPI } from '../../utils/share-link-api';
+
+class UserItem extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isOperationShow: false,
+ };
+ }
+
+ onMouseEnter = () => {
+ this.setState({isOperationShow: true});
+ };
+
+ onMouseLeave = () => {
+ this.setState({isOperationShow: false});
+ };
+
+ deleteItem = () => {
+ const { item } = this.props;
+ this.props.deleteItem(item.username);
+
+ };
+
+ render() {
+ let item = this.props.item;
+ return (
+
+
+
+
+ {item.name}
+
+ |
+
+
+
+ |
+
+ );
+ }
+}
+
+UserItem.propTypes = {
+ repoID: PropTypes.string.isRequired,
+ item: PropTypes.object.isRequired,
+ deleteItem: PropTypes.func.isRequired,
+};
+
+class UserList extends React.Component {
+
+ render() {
+ let items = this.props.items;
+ return (
+
+ {items.map((item, index) => {
+ return (
+
+ );
+ })}
+
+ );
+ }
+}
+
+UserList.propTypes = {
+ repoID: PropTypes.string.isRequired,
+ items: PropTypes.array.isRequired,
+ linkToken: PropTypes.string,
+ deleteItem: PropTypes.func,
+};
+
+const propTypes = {
+ repoID: PropTypes.string.isRequired,
+ linkToken: PropTypes.string,
+ setMode: PropTypes.func,
+ path: PropTypes.string,
+ hideHead: PropTypes.bool
+};
+
+class LinkUserAuth extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ selectedOption: null,
+ authUsers: []
+ };
+ }
+
+ handleSelectChange = (option) => {
+ this.setState({selectedOption: option});
+ };
+
+ componentDidMount() {
+ this.listLinkAuthUsers();
+ }
+
+ listLinkAuthUsers = () => {
+ const { linkToken, path } = this.props;
+ shareLinkAPI.listShareLinkAuthUsers(linkToken, path).then(res => {
+ this.setState({authUsers: res.data.auth_list});
+ });
+ };
+
+ addLinkAuthUsers = () => {
+ const { linkToken, path } = this.props;
+ const { selectedOption } = this.state;
+ if (!selectedOption || !selectedOption.length ) {
+ return false;
+ }
+ const users = selectedOption.map((item, index) => item.email);
+ shareLinkAPI.addShareLinkAuthUsers(linkToken, users, path).then(res => {
+ let authUsers = this.state.authUsers;
+ let newAuthUsers = [...authUsers, ...res.data.auth_list];
+ this.setState({
+ authUsers: newAuthUsers,
+ selectedOption: null
+ });
+ this.refs.userSelect.clearSelect();
+ });
+ };
+
+ deleteItem = (username) => {
+ const { linkToken, path } = this.props;
+ let users = [username, ];
+ shareLinkAPI.deleteShareLinkAuthUsers(linkToken, users, path).then(res => {
+ let authUsers = this.state.authUsers.filter(user => {
+ return user.username !== username;
+ });
+ this.setState({
+ authUsers: authUsers
+ });
+ });
+ };
+
+ goBack = () => {
+ this.props.setMode('displayLinkDetails');
+ };
+
+ render() {
+ let { authUsers } = this.state;
+ const thead = (
+
+
+
+
+ {gettext('Links')}
+ |
+ |
+
+
+ );
+ return (
+
+
+ {this.props.hideHead ? null : thead}
+
+
+
+
+ |
+
+
+ |
+
+
+
+
+
+ );
+ }
+}
+
+LinkUserAuth.propTypes = propTypes;
+
+export default LinkUserAuth;
\ No newline at end of file
diff --git a/frontend/src/models/share-link.js b/frontend/src/models/share-link.js
index 80f2916d0f3..d322275aa54 100644
--- a/frontend/src/models/share-link.js
+++ b/frontend/src/models/share-link.js
@@ -18,6 +18,7 @@ class ShareLink {
this.view_cnt = object.view_cnt;
this.ctime = object.ctime;
this.password = object.password;
+ this.user_scope = object.user_scope;
}
}
diff --git a/frontend/src/utils/share-link-api.js b/frontend/src/utils/share-link-api.js
new file mode 100644
index 00000000000..c67ceea93e8
--- /dev/null
+++ b/frontend/src/utils/share-link-api.js
@@ -0,0 +1,144 @@
+import cookie from 'react-cookies';
+import {siteRoot} from './constants';
+import axios from 'axios';
+
+class ShareLinkAPI {
+ init({server, username, password, token}) {
+ this.server = server;
+ this.username = username;
+ this.password = password;
+ this.token = token; //none
+ if (this.token && this.server) {
+ this.req = axios.create({
+ baseURL: this.server,
+ headers: {'Authorization': 'Token ' + this.token},
+ });
+ }
+ return this;
+ }
+
+ initForSeahubUsage({siteRoot, xcsrfHeaders}) {
+ if (siteRoot && siteRoot.charAt(siteRoot.length - 1) === '/') {
+ let server = siteRoot.substring(0, siteRoot.length - 1);
+ this.server = server;
+ } else {
+ this.server = siteRoot;
+ }
+ this.req = axios.create({
+ headers: {
+ 'X-CSRFToken': xcsrfHeaders,
+ }
+ });
+ return this;
+ }
+
+ _sendPostRequest(url, form) {
+ if (form.getHeaders) {
+ return this.req.post(url, form, {
+ headers: form.getHeaders()
+ });
+ } else {
+ return this.req.post(url, form);
+ }
+ }
+
+ listShareLinkAuthUsers(link_token, path) {
+ const url = this.server + '/api/v2.1/share-links/' + link_token + '/user-auth/?path=' + encodeURIComponent(path);
+ return this.req.get(url);
+ }
+
+ addShareLinkAuthUsers(link_token, emails, path) {
+ const url = this.server + '/api/v2.1/share-links/' + link_token + '/user-auth/?path=' + encodeURIComponent(path);
+ const data = {
+ emails: emails,
+ };
+ return this.req.post(url, data);
+
+ }
+
+ deleteShareLinkAuthUsers(link_token, emails, path) {
+ const url = this.server + '/api/v2.1/share-links/' + link_token + '/user-auth/?path=' + encodeURIComponent(path);
+ const params = {
+ emails: emails,
+ };
+ return this.req.delete(url, {data: params});
+ }
+
+ listShareLinkAuthEmails(link_token, path) {
+ const url = this.server + '/api/v2.1/share-links/' + link_token + '/email-auth/?path=' + encodeURIComponent(path);
+ return this.req.get(url);
+ }
+
+ addShareLinkAuthEmails(link_token, emails, path) {
+ const url = this.server + '/api/v2.1/share-links/' + link_token + '/email-auth/?path=' + encodeURIComponent(path);
+ const data = {
+ emails: emails,
+ };
+ return this.req.post(url, data);
+
+ }
+
+ deleteShareLinkAuthEmails(link_token, emails, path) {
+ const url = this.server + '/api/v2.1/share-links/' + link_token + '/email-auth/?path=' + encodeURIComponent(path);
+ const params = {
+ emails: emails,
+ };
+ return this.req.delete(url, {data: params});
+ }
+
+
+ createShareLink(repoID, path, password, expirationTime, permissions, scope, users) {
+ const url = this.server + '/api/v2.1/share-links/';
+ let form = {
+ 'path': path,
+ 'repo_id': repoID,
+ 'user_scope': scope,
+ };
+ if (permissions) {
+ form['permissions'] = permissions;
+ }
+ if (password) {
+ form['password'] = password;
+ }
+ if (expirationTime) {
+ form['expiration_time'] = expirationTime;
+ }
+ if (users) {
+ form['emails'] = users;
+ }
+ return this._sendPostRequest(url, form);
+ }
+
+ updateShareLinkScope(token, scope) {
+ const url = this.server + '/api/v2.1/share-links/' + token + '/';
+ let form = {'user_scope': scope};
+ return this.req.put(url, form);
+ }
+
+ createMultiShareLink(repoID, path, password, expirationTime, permissions, scope, users) {
+ const url = this.server + '/api/v2.1/multi-share-links/';
+ let form = {
+ 'path': path,
+ 'repo_id': repoID,
+ 'user_scope': scope,
+ };
+ if (permissions) {
+ form['permissions'] = permissions;
+ }
+ if (password) {
+ form['password'] = password;
+ }
+ if (expirationTime) {
+ form['expiration_time'] = expirationTime;
+ }
+ if (users) {
+ form['emails'] = users;
+ }
+ return this._sendPostRequest(url, form);
+ }
+}
+
+let shareLinkAPI = new ShareLinkAPI();
+let xcsrfHeaders = cookie.load('sfcsrftoken');
+shareLinkAPI.initForSeahubUsage({siteRoot, xcsrfHeaders});
+export {shareLinkAPI};
diff --git a/seahub/api2/endpoints/multi_share_links.py b/seahub/api2/endpoints/multi_share_links.py
index ec32bfa6960..c01dd6be540 100644
--- a/seahub/api2/endpoints/multi_share_links.py
+++ b/seahub/api2/endpoints/multi_share_links.py
@@ -2,6 +2,7 @@
import os
import stat
import logging
+import json
import dateutil.parser
from dateutil.relativedelta import relativedelta
@@ -17,16 +18,18 @@
from seaserv import seafile_api, ccnet_api
-from seahub.api2.utils import api_error
+from seahub.api2.utils import api_error, send_share_link_emails
from seahub.api2.authentication import TokenAuthentication
from seahub.api2.throttling import UserRateThrottle
from seahub.api2.permissions import CanGenerateShareLink
+from seahub.base.accounts import User
+from seahub.base.templatetags.seahub_tags import email2nickname
from seahub.constants import PERMISSION_READ_WRITE, PERMISSION_READ, PERMISSION_PREVIEW_EDIT, PERMISSION_PREVIEW, PERMISSION_INVISIBLE
from seahub.share.models import FileShare
from seahub.share.decorators import check_share_link_count
-from seahub.share.utils import is_repo_admin
+from seahub.share.utils import is_repo_admin, VALID_SHARE_LINK_SCOPE, SCOPE_SPECIFIC_EMAILS, SCOPE_SPECIFIC_USERS
from seahub.utils import is_org_context, get_password_strength_level, \
- is_valid_password, gen_shared_link, is_pro_version
+ is_valid_password, gen_shared_link, is_pro_version, is_valid_email, string2list
from seahub.utils.timeutils import datetime_to_isoformat_timestr
from seahub.utils.repo import parse_repo_perm
from seahub.settings import SHARE_LINK_EXPIRE_DAYS_MAX, SHARE_LINK_EXPIRE_DAYS_MIN, SHARE_LINK_EXPIRE_DAYS_DEFAULT, \
@@ -255,6 +258,38 @@ def post(self, request):
password, expire_date,
permission=perm, org_id=org_id)
+ user_scope = request.data.get('user_scope', '')
+ emails_list = []
+ if user_scope and user_scope in VALID_SHARE_LINK_SCOPE:
+ if user_scope == SCOPE_SPECIFIC_USERS:
+
+ emails = request.data.get('emails', [])
+ emails_to_add = []
+
+ for username in emails:
+ try:
+ User.objects.get(email=username)
+ except User.DoesNotExist:
+ continue
+ emails_to_add.append(username)
+
+ fs.authed_details = json.dumps(
+ {'authed_users': emails_to_add}
+ )
+ elif user_scope == SCOPE_SPECIFIC_EMAILS:
+ emails_str = request.data.get('emails', '')
+ emails_list = string2list(emails_str)
+ emails_list = [e for e in emails_list if is_valid_email(e)]
+ fs.authed_details = json.dumps(
+ {'authed_emails': emails_list}
+ )
+
+ fs.user_scope = user_scope
+ fs.save()
+ if emails_list:
+ shared_from = email2nickname(username)
+ send_share_link_emails(emails_list, fs, shared_from)
+
link_info = get_share_link_info(fs)
return Response(link_info)
diff --git a/seahub/api2/endpoints/share_link_auth.py b/seahub/api2/endpoints/share_link_auth.py
new file mode 100644
index 00000000000..57666b6a9c2
--- /dev/null
+++ b/seahub/api2/endpoints/share_link_auth.py
@@ -0,0 +1,356 @@
+import logging
+import json
+from rest_framework.authentication import SessionAuthentication
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.response import Response
+from rest_framework.views import APIView
+from rest_framework import status
+from seahub.api2.utils import api_error, send_share_link_emails
+from seahub.api2.authentication import TokenAuthentication
+from seahub.api2.throttling import UserRateThrottle
+from seahub.api2.permissions import CanGenerateShareLink
+from seahub.avatar.templatetags.avatar_tags import api_avatar_url
+from seahub.base.accounts import User
+from seahub.base.templatetags.seahub_tags import email2nickname, email2contact_email
+from seahub.share.models import FileShare
+from seahub.share.utils import SCOPE_SPECIFIC_USERS, SCOPE_SPECIFIC_EMAILS
+from seahub.utils.repo import parse_repo_perm
+from seahub.utils import IS_EMAIL_CONFIGURED, string2list, is_valid_email
+from seaserv import seafile_api
+
+
+logger = logging.getLogger(__name__)
+
+
+def get_user_auth_info(username, token):
+ avatar_url, _, _ = api_avatar_url(username, 72)
+ name = email2nickname(username)
+ contact_email = email2contact_email(username)
+ return {
+ 'username': username,
+ 'name': name,
+ 'avatar_url': avatar_url,
+ 'link_token': token,
+ 'contact_email': contact_email
+ }
+
+
+def check_link_share_perms(request, repo_id, path):
+ username = request.user.username
+ repo_folder_permission = seafile_api.check_permission_by_path(repo_id, path, username)
+ if parse_repo_perm(repo_folder_permission).can_generate_share_link is False:
+ return False
+
+ return True
+
+
+
+class ShareLinkUserAuthView(APIView):
+
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated, CanGenerateShareLink)
+ throttle_classes = (UserRateThrottle,)
+
+ def get(self, request, token):
+
+ path = request.GET.get('path', None)
+ if not path:
+ error_msg = 'path invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ try:
+ file_share = FileShare.objects.get(token=token)
+ except FileShare.DoesNotExist:
+ error_msg = 'token invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if file_share.user_scope != SCOPE_SPECIFIC_USERS:
+ error_msg = 'Share scope invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ repo_id = file_share.repo_id
+ if not check_link_share_perms(request, repo_id, path):
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ try:
+ authed_details = json.loads(file_share.authed_details)
+ except:
+ authed_details = {}
+
+ try:
+ user_auth_infos = authed_details.get('authed_users', [])
+ resp = []
+ for auth_username in user_auth_infos:
+ resp.append(get_user_auth_info(auth_username, token))
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ return Response({'auth_list': resp})
+
+
+ def post(self, request, token):
+
+ try:
+ path = request.GET.get('path', None)
+ if not path:
+ error_msg = 'path invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ try:
+ file_share = FileShare.objects.get(token=token)
+ except FileShare.DoesNotExist:
+ error_msg = 'token invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if file_share.user_scope != SCOPE_SPECIFIC_USERS:
+ error_msg = 'Share scope invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ repo_id = file_share.repo_id
+ if not check_link_share_perms(request, repo_id, path):
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+
+ email_list = request.data.get('emails', [])
+ try:
+ authed_details = json.loads(file_share.authed_details)
+ except:
+ authed_details = {}
+ user_auth_infos = authed_details.get('authed_users', [])
+ exist_emails = user_auth_infos
+ auth_infos = []
+ for username in email_list:
+ if username in exist_emails:
+ continue
+ try:
+ User.objects.get(email=username)
+ except User.DoesNotExist:
+ continue
+
+ user_auth_infos.append(username)
+ auth_infos.append(get_user_auth_info(username, token))
+ authed_details['authed_users'] = user_auth_infos
+ file_share.authed_details = json.dumps(authed_details)
+ file_share.save()
+
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ return Response({'auth_list': auth_infos})
+
+
+ def delete(self, request, token):
+
+ path = request.GET.get('path', None)
+ if not path:
+ error_msg = 'path invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ try:
+ file_share = FileShare.objects.get(token=token)
+ except FileShare.DoesNotExist:
+ return Response({'success': True})
+
+ if file_share.user_scope != SCOPE_SPECIFIC_USERS:
+ error_msg = 'Share scope invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ repo_id = file_share.repo_id
+ if not check_link_share_perms(request, repo_id, path):
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ if not file_share.is_owner(request.user.username):
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ try:
+ authed_details = json.loads(file_share.authed_details)
+ except:
+ authed_details = {}
+
+ try:
+ user_auth_infos = authed_details.get('authed_users', [])
+ email_list = request.data.get('emails')
+ new_user_auth_infos = []
+ for u in user_auth_infos:
+ if u not in email_list:
+ new_user_auth_infos.append(u)
+ authed_details['authed_users'] = new_user_auth_infos
+ file_share.authed_details = json.dumps(authed_details)
+ file_share.save()
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ return Response({'success': True})
+
+class ShareLinkEmailAuthView(APIView):
+
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated, CanGenerateShareLink)
+ throttle_classes = (UserRateThrottle,)
+
+ def get(self, request, token):
+
+ if not IS_EMAIL_CONFIGURED:
+ error_msg = 'feature is not enabled.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+
+ path = request.GET.get('path', None)
+ if not path:
+ error_msg = 'path invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ try:
+ file_share = FileShare.objects.get(token=token)
+ except FileShare.DoesNotExist:
+ error_msg = 'token invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if file_share.user_scope != SCOPE_SPECIFIC_EMAILS:
+ error_msg = 'Share scope invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ repo_id = file_share.repo_id
+ if not check_link_share_perms(request, repo_id, path):
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ try:
+ authed_details = json.loads(file_share.authed_details)
+ except:
+ authed_details = {}
+
+ try:
+ email_auth_infos = authed_details.get('authed_emails', [])
+ resp = []
+ for auth_email in email_auth_infos:
+ resp.append(auth_email)
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ return Response({'auth_list': resp})
+
+
+ def post(self, request, token):
+
+ if not IS_EMAIL_CONFIGURED:
+ error_msg = 'feature is not enabled.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ try:
+ path = request.GET.get('path', None)
+ if not path:
+ error_msg = 'path invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ try:
+ file_share = FileShare.objects.get(token=token)
+ except FileShare.DoesNotExist:
+ error_msg = 'token invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if file_share.user_scope != SCOPE_SPECIFIC_EMAILS:
+ error_msg = 'Share scope invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ repo_id = file_share.repo_id
+ if not check_link_share_perms(request, repo_id, path):
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+
+ email_str = request.data.get('emails', '')
+ email_list = string2list(email_str)
+ email_list = [e for e in email_list if is_valid_email(e)]
+ try:
+ authed_details = json.loads(file_share.authed_details)
+ except:
+ authed_details = {}
+ email_auth_infos = authed_details.get('authed_emails', [])
+ exist_emails = email_auth_infos
+ new_auth_infos = []
+ for email in email_list:
+ if email in exist_emails:
+ continue
+ email_auth_infos.append(email)
+ new_auth_infos.append(email)
+
+ authed_details['authed_emails'] = email_auth_infos
+ file_share.authed_details = json.dumps(authed_details)
+ file_share.save()
+
+ if new_auth_infos:
+ shared_from = email2nickname(request.user.username)
+ send_share_link_emails(new_auth_infos, file_share, shared_from)
+
+
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ return Response({'auth_list': new_auth_infos})
+
+
+ def delete(self, request, token):
+
+ if not IS_EMAIL_CONFIGURED:
+ error_msg = 'feature is not enabled.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ path = request.GET.get('path', None)
+ if not path:
+ error_msg = 'path invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ try:
+ file_share = FileShare.objects.get(token=token)
+ except FileShare.DoesNotExist:
+ return Response({'success': True})
+
+ if file_share.user_scope != SCOPE_SPECIFIC_EMAILS:
+ error_msg = 'Share scope invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if not file_share.is_owner(request.user.username):
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ repo_id = file_share.repo_id
+ if not check_link_share_perms(request, repo_id, path):
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ try:
+ authed_details = json.loads(file_share.authed_details)
+ except:
+ authed_details = {}
+
+ try:
+ email_auth_infos = authed_details.get('authed_emails', [])
+ email_list = request.data.get('emails')
+ new_user_auth_infos = []
+ for u in email_auth_infos:
+ if u not in email_list:
+ new_user_auth_infos.append(u)
+ authed_details['authed_emails'] = new_user_auth_infos
+ file_share.authed_details = json.dumps(authed_details)
+ file_share.save()
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ return Response({'success': True})
diff --git a/seahub/api2/endpoints/share_links.py b/seahub/api2/endpoints/share_links.py
index ef82b326e5e..4ca2709cecb 100644
--- a/seahub/api2/endpoints/share_links.py
+++ b/seahub/api2/endpoints/share_links.py
@@ -24,18 +24,21 @@
from seaserv import seafile_api
from pysearpc import SearpcError
-from seahub.api2.utils import api_error
+from seahub.api2.utils import api_error, send_share_link_emails
from seahub.api2.authentication import TokenAuthentication
from seahub.api2.throttling import UserRateThrottle
from seahub.api2.permissions import CanGenerateShareLink, IsProVersion
+from seahub.base.accounts import User
+from seahub.base.templatetags.seahub_tags import email2nickname
from seahub.constants import PERMISSION_READ_WRITE, PERMISSION_READ, \
PERMISSION_PREVIEW_EDIT, PERMISSION_PREVIEW
from seahub.share.models import FileShare, UploadLinkShare, check_share_link_access
from seahub.share.decorators import check_share_link_count
+from seahub.share.utils import VALID_SHARE_LINK_SCOPE, SCOPE_SPECIFIC_USERS, SCOPE_SPECIFIC_EMAILS
from seahub.utils import gen_shared_link, is_org_context, normalize_file_path, \
- normalize_dir_path, is_pro_version, get_file_type_and_ext, \
- check_filename_with_rename, gen_file_upload_url, \
- get_password_strength_level, is_valid_password
+ normalize_dir_path, is_pro_version, get_file_type_and_ext, \
+ check_filename_with_rename, gen_file_upload_url, \
+ get_password_strength_level, is_valid_password, is_valid_email, string2list
from seahub.utils.file_op import if_locked_by_online_office
from seahub.utils.file_types import IMAGE, VIDEO, XMIND
from seahub.utils.file_tags import get_tagged_files, get_files_tags_in_dir
@@ -111,7 +114,7 @@ def get_share_link_info(fileshare):
data['is_expired'] = fileshare.is_expired()
data['permissions'] = fileshare.get_permissions()
data['password'] = fileshare.get_password()
-
+ data['user_scope'] = fileshare.user_scope
data['can_edit'] = False
if repo and path != '/' and not data['is_dir']:
try:
@@ -287,6 +290,7 @@ def get(self, request):
link_info['is_expired'] = fs.is_expired()
link_info['permissions'] = fs.get_permissions()
link_info['password'] = fs.get_password()
+ link_info['user_scope'] = fs.user_scope
tmp_key = f"{repo_id}_{path}"
link_info['repo_folder_permission'] = repo_folder_permission_dict.get(tmp_key, "")
@@ -486,6 +490,37 @@ def post(self, request):
password, expire_date,
permission=perm, org_id=org_id)
+ user_scope = request.data.get('user_scope', '')
+ emails_list = []
+ if user_scope and user_scope in VALID_SHARE_LINK_SCOPE:
+ if user_scope == SCOPE_SPECIFIC_USERS:
+
+ emails = request.data.get('emails', [])
+ emails_to_add = []
+
+ for username in emails:
+ try:
+ User.objects.get(email=username)
+ except User.DoesNotExist:
+ continue
+ emails_to_add.append(username)
+
+ fs.authed_details = json.dumps(
+ {'authed_users': emails_to_add}
+ )
+ elif user_scope == SCOPE_SPECIFIC_EMAILS:
+ emails_str = request.data.get('emails', '')
+ emails_list = string2list(emails_str)
+ emails_list = [e for e in emails_list if is_valid_email(e)]
+ fs.authed_details = json.dumps(
+ {'authed_emails': emails_list}
+ )
+
+ fs.user_scope = user_scope
+ fs.save()
+ if emails_list:
+ shared_from = email2nickname(username)
+ send_share_link_emails(emails_list, fs, shared_from)
link_info = get_share_link_info(fs)
return Response(link_info)
@@ -721,6 +756,12 @@ def put(self, request, token):
fs.expire_date = expire_date
fs.save()
+ user_scope = request.data.get('user_scope', '')
+ if user_scope and user_scope in VALID_SHARE_LINK_SCOPE:
+ fs.user_scope = user_scope
+ fs.authed_details = None
+ fs.save()
+
link_info = get_share_link_info(fs)
return Response(link_info)
diff --git a/seahub/api2/utils.py b/seahub/api2/utils.py
index 5cd255bf458..879d0e7089f 100644
--- a/seahub/api2/utils.py
+++ b/seahub/api2/utils.py
@@ -10,6 +10,8 @@
from collections import defaultdict
from functools import wraps
+from django.core.cache import cache
+
from django.http import HttpResponse
from rest_framework.authentication import SessionAuthentication
@@ -28,6 +30,7 @@
from seahub.avatar.settings import AVATAR_DEFAULT_SIZE
from seahub.avatar.templatetags.avatar_tags import api_avatar_url
from seahub.utils import get_user_repos
+from seahub.utils.mail import send_html_email_with_dj_template
logger = logging.getLogger(__name__)
@@ -301,3 +304,16 @@ def get_search_repos(username, org_id):
repos.append((repo.id, repo.origin_repo_id, repo.origin_path, repo.name))
return repos
+
+def send_share_link_emails(emails, fs, shared_from):
+ subject = "Share links"
+ for email in emails:
+ c = {'url': "%s?email=%s" % (fs.get_full_url(), email), 'shared_from': shared_from}
+ send_success = send_html_email_with_dj_template(
+ email,
+ subject=subject,
+ dj_template='share/share_link_email.html',
+ context=c)
+ if not send_success:
+ logger.error('Failed to send code via email to %s' % email)
+ continue
diff --git a/seahub/share/decorators.py b/seahub/share/decorators.py
index ef6b672f2f0..fca6f9e4278 100644
--- a/seahub/share/decorators.py
+++ b/seahub/share/decorators.py
@@ -1,4 +1,5 @@
# Copyright (c) 2012-2016 Seafile Ltd.
+import json
from django.core.cache import cache
from django.conf import settings
from django.shortcuts import render
@@ -8,17 +9,57 @@
from seahub.api2.utils import api_error
from seahub.share.models import FileShare, UploadLinkShare
+from seahub.share.utils import SCOPE_SPECIFIC_EMAILS, SCOPE_ALL_USERS, SCOPE_SPECIFIC_USERS
from seahub.utils import render_error
from seahub.utils import normalize_cache_key, is_pro_version, redirect_to_login
from seahub.constants import REPO_SHARE_LINK_COUNT_LIMIT
+def _share_link_auth_email_entry(request, fileshare, func, *args, **kwargs):
+ if request.user.username == fileshare.username:
+ return func(request, fileshare, *args, **kwargs)
+
+ session_key = "link_authed_email_%s" % fileshare.token
+ if request.session.get(session_key) is not None:
+ request.user.username = request.session.get(session_key)
+ return func(request, fileshare, *args, **kwargs)
+
+ if request.method == 'GET':
+ email = request.GET.get('email', '')
+ return render(request, 'share/share_link_email_audit.html', {'email': email, 'token': fileshare.token})
+
+ elif request.method == 'POST':
+ code_post = request.POST.get('code', '')
+ email_post = request.POST.get('email', '')
+ cache_key = normalize_cache_key(email_post, 'share_link_email_auth_', token=fileshare.token)
+ code = cache.get(cache_key)
+
+ authed_details = json.loads(fileshare.authed_details)
+ if code == code_post and email_post in authed_details.get('authed_emails'):
+ request.session[session_key] = email_post
+ request.user.username = request.session.get(session_key)
+ cache.delete(cache_key)
+ return func(request, fileshare, *args, **kwargs)
+ else:
+ return render(request, 'share/share_link_email_audit.html', {
+ 'err_msg': 'Invalid token, please try again.',
+ 'email': email_post,
+ 'code': code,
+ 'token': fileshare.token,
+ 'code_verify': False
+
+ })
+ else:
+ assert False, 'TODO'
+
+
def share_link_audit(func):
def _decorated(request, token, *args, **kwargs):
assert token is not None # Checked by URLconf
-
+
+ is_for_upload = False
try:
fileshare = FileShare.objects.get(token=token)
except FileShare.DoesNotExist:
@@ -27,57 +68,27 @@ def _decorated(request, token, *args, **kwargs):
if not fileshare:
try:
fileshare = UploadLinkShare.objects.get(token=token)
+ is_for_upload = True
except UploadLinkShare.DoesNotExist:
fileshare = None
-
+
if not fileshare:
return render_error(request, _('Link does not exist.'))
-
+
if fileshare.is_expired():
return render_error(request, _('Link is expired.'))
-
- if not is_pro_version() or not settings.ENABLE_SHARE_LINK_AUDIT:
- return func(request, fileshare, *args, **kwargs)
-
- # no audit for authenticated user, since we've already got email address
- if request.user.is_authenticated:
+
+ if is_for_upload:
return func(request, fileshare, *args, **kwargs)
-
- # anonymous user
- if request.session.get('anonymous_email') is not None:
- request.user.username = request.session.get('anonymous_email')
+
+ if fileshare.user_scope in [SCOPE_ALL_USERS, SCOPE_SPECIFIC_USERS]:
return func(request, fileshare, *args, **kwargs)
- if request.method == 'GET':
- return render(request, 'share/share_link_audit.html', {
- 'token': token,
- })
- elif request.method == 'POST':
-
- code = request.POST.get('code', '')
- email = request.POST.get('email', '')
-
- cache_key = normalize_cache_key(email, 'share_link_audit_')
- if code == cache.get(cache_key):
- # code is correct, add this email to session so that he will
- # not be asked again during this session, and clear this code.
- request.session['anonymous_email'] = email
- request.user.username = request.session.get('anonymous_email')
- cache.delete(cache_key)
- return func(request, fileshare, *args, **kwargs)
- else:
- return render(request, 'share/share_link_audit.html', {
- 'err_msg': 'Invalid token, please try again.',
- 'email': email,
- 'code': code,
- 'token': token,
- })
- else:
- assert False, 'TODO'
-
+ if fileshare.user_scope == SCOPE_SPECIFIC_EMAILS:
+ return _share_link_auth_email_entry(request, fileshare, func, *args, **kwargs)
+
return _decorated
-
def share_link_login_required(func):
def _decorated(request, *args, **kwargs):
diff --git a/seahub/share/models.py b/seahub/share/models.py
index b1ef714341d..dd67b9144ac 100644
--- a/seahub/share/models.py
+++ b/seahub/share/models.py
@@ -345,6 +345,9 @@ class FileShare(models.Model):
choices=PERMISSION_CHOICES,
default=PERM_VIEW_DL)
+ user_scope = models.CharField(max_length=255, default='all_users')
+ authed_details = models.TextField(default='')
+
objects = FileShareManager()
def is_file_share_link(self):
diff --git a/seahub/share/templates/share/share_link_email.html b/seahub/share/templates/share/share_link_email.html
new file mode 100644
index 00000000000..13b179be043
--- /dev/null
+++ b/seahub/share/templates/share/share_link_email.html
@@ -0,0 +1,19 @@
+{% extends 'email_base.html' %}
+{% load i18n %}
+
+{% block email_con %}
+
+{% autoescape off %}
+
+
{% trans "Hi," %}
+
+
+{% blocktrans %}
+ {{ shared_from }} has shared a library with you.
+ Please click here to verify your email.
+{% endblocktrans%}
+
+
+{% endautoescape %}
+
+{% endblock %}
diff --git a/seahub/share/templates/share/share_link_email_audit.html b/seahub/share/templates/share/share_link_email_audit.html
new file mode 100644
index 00000000000..bd5f21a43c5
--- /dev/null
+++ b/seahub/share/templates/share/share_link_email_audit.html
@@ -0,0 +1,82 @@
+{% extends "base.html" %}
+{% load i18n %}
+
+{% block main_panel %}
+
+
{% trans "Email Verification" %}
+
+
+{% endblock %}
+{% block extra_script %}
+
+{% endblock %}
diff --git a/seahub/share/urls.py b/seahub/share/urls.py
index f06a2ce5ea2..15acce635ca 100644
--- a/seahub/share/urls.py
+++ b/seahub/share/urls.py
@@ -10,4 +10,5 @@
path('upload_link/send/', send_shared_upload_link, name='send_shared_upload_link'),
path('ajax/private-share-dir/', ajax_private_share_dir, name='ajax_private_share_dir'),
path('ajax/get-link-audit-code/', ajax_get_link_audit_code, name='ajax_get_link_audit_code'),
+ path('ajax/get-link-email-audit-code/', ajax_get_link_email_audit_code, name='ajax_get_link_email_audit_code'),
]
diff --git a/seahub/share/utils.py b/seahub/share/utils.py
index 53b562d53e9..b9b20173993 100644
--- a/seahub/share/utils.py
+++ b/seahub/share/utils.py
@@ -1,5 +1,5 @@
import logging
-
+import json
from seahub.group.utils import is_group_admin
from seahub.constants import PERMISSION_ADMIN, PERMISSION_READ_WRITE, CUSTOM_PERMISSION_PREFIX
from seahub.share.models import ExtraSharePermission, ExtraGroupsSharePermission, CustomSharePermissions
@@ -11,6 +11,17 @@
logger = logging.getLogger(__name__)
+SCOPE_ALL_USERS = 'all_users'
+SCOPE_SPECIFIC_USERS = 'specific_users'
+SCOPE_SPECIFIC_EMAILS = 'specific_emails'
+
+VALID_SHARE_LINK_SCOPE = [
+ SCOPE_ALL_USERS,
+ SCOPE_SPECIFIC_USERS,
+ SCOPE_SPECIFIC_EMAILS
+]
+
+
def normalize_custom_permission_name(permission):
try:
if CUSTOM_PERMISSION_PREFIX in permission:
@@ -255,3 +266,26 @@ def has_shared_to_group(repo_id, path, gid, org_id=None):
path, repo_owner)
return gid in [item.group_id for item in share_items]
+
+
+def check_share_link_user_access(share, username):
+ if share.user_scope == 'all_users':
+ return True
+ if username == share.username:
+ return True
+ try:
+ authed_details = json.loads(share.authed_details)
+ except:
+ authed_details = {}
+
+ if share.user_scope == SCOPE_SPECIFIC_USERS:
+ authed_users = authed_details.get('authed_users', [])
+ if username in authed_users:
+ return True
+
+ if share.user_scope == SCOPE_SPECIFIC_EMAILS:
+ auhhed_emails = authed_details.get('authed_emails', [])
+ if username in auhhed_emails:
+ return True
+
+ return False
diff --git a/seahub/share/views.py b/seahub/share/views.py
index 921d2ea0fa8..07595a3518b 100644
--- a/seahub/share/views.py
+++ b/seahub/share/views.py
@@ -30,8 +30,8 @@
from seahub.utils.ms_excel import write_xls
from seahub.utils.timeutils import datetime_to_isoformat_timestr
from seahub.settings import SITE_ROOT, REPLACE_FROM_EMAIL, \
- ADD_REPLY_TO_HEADER, SHARE_LINK_EMAIL_LANGUAGE, \
- SHARE_LINK_AUDIT_CODE_TIMEOUT
+ ADD_REPLY_TO_HEADER, SHARE_LINK_EMAIL_LANGUAGE, \
+ SHARE_LINK_AUDIT_CODE_TIMEOUT
from seahub.profile.models import Profile
# Get an instance of a logger
@@ -560,3 +560,49 @@ def ajax_get_link_audit_code(request):
return HttpResponse(json.dumps({'success': True}), status=200,
content_type=content_type)
+
+
+def ajax_get_link_email_audit_code(request):
+ content_type = 'application/json; charset=utf-8'
+
+ token = request.POST.get('token')
+ email = request.POST.get('email')
+ if not is_valid_email(email):
+ return HttpResponse(json.dumps({
+ 'error': _('Email address is not valid')
+ }), status=400, content_type=content_type)
+
+ fs = FileShare.objects.get_valid_file_link_by_token(token)
+ if fs is None:
+ return HttpResponse(json.dumps({
+ 'error': _('Share link is not found')
+ }), status=400, content_type=content_type)
+
+ authed_details = json.loads(fs.authed_details)
+ authed_emails = authed_details.get('authed_emails', [])
+ if email not in authed_emails:
+ return HttpResponse(json.dumps({
+ 'error': _('Email address is not valid')
+ }), status=400, content_type=content_type)
+
+ code = gen_token(max_length=6)
+ cache_key = normalize_cache_key(email, 'share_link_email_auth_', token=fs.token)
+ cache.set(cache_key, code, 60 * 60)
+
+ # send code to user via email
+ subject = _("Verification code for visiting share links")
+ c = {'code': code}
+
+ send_success = send_html_email_with_dj_template(email,
+ subject=subject,
+ dj_template='share/audit_code_email.html',
+ context=c)
+
+ if not send_success:
+ logger.error('Failed to send audit code via email to %s')
+ return HttpResponse(json.dumps({
+ "error": _("Failed to send a verification code, please try again later.")
+ }), status=500, content_type=content_type)
+
+ return HttpResponse(json.dumps({'success': True}), status=200,
+ content_type=content_type)
diff --git a/seahub/urls.py b/seahub/urls.py
index 21b147cf022..57fae2d0a5f 100644
--- a/seahub/urls.py
+++ b/seahub/urls.py
@@ -2,6 +2,7 @@
from django.urls import include, path, re_path
from django.views.generic import TemplateView
+from seahub.api2.endpoints.share_link_auth import ShareLinkUserAuthView, ShareLinkEmailAuthView
from seahub.auth.views import multi_adfs_sso
from seahub.views import *
from seahub.views.mobile import mobile_login
@@ -390,6 +391,8 @@
re_path(r'^api/v2.1/share-links/(?P
[a-f0-9]+)/repo-tags/$', ShareLinkRepoTags.as_view(), name='api-v2.1-share-link-repo-tags'),
re_path(r'^api/v2.1/share-links/(?P[a-f0-9]+)/tagged-files/(?P\d+)/$', ShareLinkRepoTagsTaggedFiles.as_view(), name='api-v2.1-share-link-repo-tags-tagged-files'),
+ re_path(r'^api/v2.1/share-links/(?P[a-f0-9]+)/user-auth/$', ShareLinkUserAuthView.as_view(), name='api-v2.1-share-link-user-auth'),
+ re_path(r'^api/v2.1/share-links/(?P[a-f0-9]+)/email-auth/$', ShareLinkEmailAuthView.as_view(), name='api-v2.1-share-link-user-auth'),
## user::shared-upload-links
re_path(r'^api/v2.1/upload-links/$', UploadLinks.as_view(), name='api-v2.1-upload-links'),
diff --git a/seahub/views/file.py b/seahub/views/file.py
index 8e82eab8240..1ab1522373b 100644
--- a/seahub/views/file.py
+++ b/seahub/views/file.py
@@ -37,6 +37,7 @@
seafserv_threaded_rpc
from seahub.settings import SITE_ROOT
+from seahub.share.utils import check_share_link_user_access
from seahub.tags.models import FileUUIDMap
from seahub.wopi.utils import get_wopi_dict
from seahub.onlyoffice.utils import get_onlyoffice_dict
@@ -1158,8 +1159,13 @@ def view_shared_file(request, fileshare):
Download share file if `dl` in request param.
View raw share file if `raw` in request param.
"""
-
+ from seahub.utils import redirect_to_login
token = fileshare.token
+ if not check_share_link_user_access(fileshare, request.user.username):
+ if not request.user.username:
+ return redirect_to_login(request)
+ error_msg = _('Permission denied')
+ return render_error(request, error_msg)
# check if share link is encrypted
password_check_passed, err_msg = check_share_link_common(request, fileshare)
@@ -1387,8 +1393,14 @@ def online_office_lock_or_refresh_lock(repo_id, path, username):
@share_link_audit
@share_link_login_required
def view_file_via_shared_dir(request, fileshare):
-
+ from seahub.utils import redirect_to_login
token = fileshare.token
+
+ if not check_share_link_user_access(fileshare, request.user.username):
+ if not request.user.username:
+ return redirect_to_login(request)
+ error_msg = _('Permission denied')
+ return render_error(request, error_msg)
# argument check
req_path = request.GET.get('p', '')
diff --git a/seahub/views/repo.py b/seahub/views/repo.py
index e808de3c967..27972ba96e9 100644
--- a/seahub/views/repo.py
+++ b/seahub/views/repo.py
@@ -19,12 +19,13 @@
from seahub.share.decorators import share_link_audit, share_link_login_required
from seahub.share.models import FileShare, UploadLinkShare, \
check_share_link_common
+from seahub.share.utils import check_share_link_user_access
from seahub.views import gen_path_link, get_repo_dirents, \
check_folder_permission
from seahub.utils import gen_dir_share_link, \
gen_shared_upload_link, render_error, \
- get_file_type_and_ext, get_service_url, normalize_dir_path
+ get_file_type_and_ext, get_service_url, normalize_dir_path, redirect_to_login
from seahub.utils.repo import is_repo_owner, get_repo_owner
from seahub.settings import ENABLE_UPLOAD_FOLDER, \
ENABLE_RESUMABLE_FILEUPLOAD, ENABLE_VIDEO_THUMBNAIL, \
@@ -252,6 +253,11 @@ def view_lib_as_wiki(request, repo_id, path):
def view_shared_dir(request, fileshare):
token = fileshare.token
+ if not check_share_link_user_access(fileshare, request.user.username):
+ if not request.user.username:
+ return redirect_to_login(request)
+ error_msg = _('Permission denied')
+ return render_error(request, error_msg)
password_check_passed, err_msg = check_share_link_common(request, fileshare)
if not password_check_passed:
diff --git a/tests/seahub/share/test_decorators.py b/tests/seahub/share/test_decorators.py
deleted file mode 100644
index 963d5b1d4ec..00000000000
--- a/tests/seahub/share/test_decorators.py
+++ /dev/null
@@ -1,141 +0,0 @@
-from mock import patch
-
-from django.core.cache import cache
-from django.http import HttpResponse
-from django.test import override_settings
-from django.test.client import RequestFactory
-
-from seahub.auth.models import AnonymousUser
-from seahub.test_utils import BaseTestCase
-from seahub.share.decorators import share_link_audit
-from seahub.share.models import FileShare
-from seahub.utils import gen_token, normalize_cache_key
-
-
-class ShareLinkAuditTest(BaseTestCase):
-
- def setUp(self):
- share_file_info = {
- 'username': self.user.username,
- 'repo_id': self.repo.id,
- 'path': self.file,
- 'password': None,
- 'expire_date': None,
- }
- self.fs = FileShare.objects.create_file_link(**share_file_info)
-
- # Every test needs access to the request factory.
- self.factory = RequestFactory()
-
- @property
- def _request(self, session={}):
- request = self.factory.get('/rand')
- request.user = self.user
- request.session = session
- request.cloud_mode = False
- return request
-
- def _anon_request(self, session={}):
- request = self.factory.get('/rand')
- request.user = AnonymousUser()
- request.session = session
- request.cloud_mode = False
- return request
-
- def _anon_post_request(self, data={}, session={}):
- request = self.factory.post('/rand', data)
- request.user = AnonymousUser()
- request.session = session
- request.cloud_mode = False
- return request
-
- def _fake_view_shared_file(self, request, token):
- @share_link_audit
- def fake_view_shared_file(request, fileshare):
- return HttpResponse()
- return fake_view_shared_file(request, token)
-
- def test_bad_share_token(self):
- resp = self._fake_view_shared_file(self._request, 'fake-token')
- self.assertEqual(resp.status_code, 200)
- self.assertIn(b'Link does not exist', resp.content)
-
- def test_non_pro_version(self):
- """
- Check that share_link_audit works as nomal view_shared_file on
- non-pro version.
- """
- resp = self._fake_view_shared_file(self._request, self.fs.token)
- self.assertEqual(resp.status_code, 200)
-
- def test_shared_link_audit_not_enabled(self):
- resp = self._fake_view_shared_file(self._request, self.fs.token)
- self.assertEqual(resp.status_code, 200)
-
- @override_settings(ENABLE_SHARE_LINK_AUDIT=True)
- @patch('seahub.share.decorators.is_pro_version')
- def test_audit_authenticated_user(self, mock_is_pro_version):
- mock_is_pro_version.return_value = True
-
- resp = self._fake_view_shared_file(self._request, self.fs.token)
- self.assertEqual(resp.status_code, 200)
-
- @override_settings(ENABLE_SHARE_LINK_AUDIT=True)
- @patch('seahub.share.decorators.is_pro_version')
- def test_audit_anonymous_user_with_mail_in_session(self, mock_is_pro_version):
- mock_is_pro_version.return_value = True
-
- anon_req = self._anon_request(session={'anonymous_email': 'a@a.com'})
- resp = self._fake_view_shared_file(anon_req, self.fs.token)
- self.assertEqual(resp.status_code, 200)
-
- @override_settings(ENABLE_SHARE_LINK_AUDIT=True)
- @patch('seahub.share.decorators.is_pro_version')
- def test_audit_anonymous_user_without_mail_in_session(self, mock_is_pro_version):
- """
- Check that share_link_audit works on pro version and setting enabled,
- which show a page that let user input email and verification code.
- """
- mock_is_pro_version.return_value = True
-
- anon_req = self._anon_request()
- resp = self._fake_view_shared_file(anon_req, self.fs.token)
- self.assertEqual(resp.status_code, 200)
- self.assertIn(b'Please provide your email address to continue.
', resp.content)
-
- @override_settings(ENABLE_SHARE_LINK_AUDIT=True)
- @patch('seahub.share.decorators.is_pro_version')
- def test_anonymous_user_post_wrong_token(self, mock_is_pro_version):
- """
- Check that anonnymous user input email and wrong verification code.
- """
- mock_is_pro_version.return_value = True
-
- anon_req = self._anon_post_request(data={'code': 'xx'}, session={})
- self.assertEqual(anon_req.session.get('anonymous_email'), None)
- resp = self._fake_view_shared_file(anon_req, self.fs.token)
-
- self.assertEqual(resp.status_code, 200)
- self.assertIn(b'Invalid token, please try again.', resp.content)
-
- @override_settings(ENABLE_SHARE_LINK_AUDIT=True)
- @patch('seahub.share.decorators.is_pro_version')
- def test_anonymous_user_post_correct_token(self, mock_is_pro_version):
- """
- Check that anonnymous user input email and correct verification code.
- """
- mock_is_pro_version.return_value = True
-
- code = gen_token(max_length=6)
- email = 'a@a.com'
- cache_key = normalize_cache_key(email, 'share_link_audit_')
- cache.set(cache_key, code, timeout=60)
- assert cache.get(cache_key) == code
-
- anon_req = self._anon_post_request(data={'code': code, 'email': email})
- self.assertEqual(anon_req.session.get('anonymous_email'), None)
- resp = self._fake_view_shared_file(anon_req, self.fs.token)
-
- self.assertEqual(resp.status_code, 200)
- self.assertEqual(anon_req.session.get('anonymous_email'), email) # email is set in session
- assert cache.get(cache_key) is None # token is delete after used