From bdfc98d41ead79ec8cfd17dd8a686a6d43c173eb Mon Sep 17 00:00:00 2001 From: zhengxie Date: Fri, 22 Apr 2016 15:07:39 +0800 Subject: [PATCH 1/5] Add share link audit --- seahub/settings.py | 3 + seahub/share/decorators.py | 55 ++++++++++++++ seahub/share/models.py | 10 ++- .../templates/share/audit_code_email.html | 16 +++++ .../templates/share/share_link_audit.html | 72 +++++++++++++++++++ seahub/share/urls.py | 1 + seahub/share/views.py | 56 ++++++++++++++- seahub/views/file.py | 13 ++-- seahub/views/repo.py | 19 ++--- 9 files changed, 225 insertions(+), 20 deletions(-) create mode 100644 seahub/share/decorators.py create mode 100644 seahub/share/templates/share/audit_code_email.html create mode 100644 seahub/share/templates/share/share_link_audit.html diff --git a/seahub/settings.py b/seahub/settings.py index 2d82ecef10..c095af16da 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -253,6 +253,9 @@ REPO_PASSWORD_MIN_LENGTH = 8 # mininum length for the password of a share link SHARE_LINK_PASSWORD_MIN_LENGTH = 8 +# enable or disable share link audit +ENABLE_SHARE_LINK_AUDIT = False + # mininum length for user's password USER_PASSWORD_MIN_LENGTH = 6 diff --git a/seahub/share/decorators.py b/seahub/share/decorators.py new file mode 100644 index 0000000000..3e79f024ae --- /dev/null +++ b/seahub/share/decorators.py @@ -0,0 +1,55 @@ +from django.core.cache import cache +from django.conf import settings +from django.http import Http404 +from django.shortcuts import render_to_response +from django.template import RequestContext + +from seahub.share.models import FileShare, UploadLinkShare +from seahub.utils import normalize_cache_key, is_pro_version + +def share_link_audit(func): + def _decorated(request, token, *args, **kwargs): + assert token is not None # Checked by URLconf + + fileshare = FileShare.objects.get_valid_file_link_by_token(token) + if fileshare is None: + fileshare = UploadLinkShare.objects.get_valid_upload_link_by_token(token) + if fileshare is None: + raise Http404 + + 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(): + return func(request, fileshare, *args, **kwargs) + + # anonymous user + if request.session.get('anonymous_email') is not None: + request.user.email = request.session.get('anonymous_email') + return func(request, fileshare, *args, **kwargs) + + if request.method == 'GET': + return render_to_response('share/share_link_audit.html', { + 'token': token, + }, context_instance=RequestContext(request)) + 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): + request.session['anonymous_email'] = email + request.user.email = request.session.get('anonymous_email') + return func(request, fileshare, *args, **kwargs) + else: + return render_to_response('share/share_link_audit.html', { + 'err_msg': 'Invalid token, please try again.', + 'email': email, + 'code': code, + 'token': token, + }, context_instance=RequestContext(request)) + else: + assert False, 'TODO' + + return _decorated diff --git a/seahub/share/models.py b/seahub/share/models.py index 6befd5088d..3259491536 100644 --- a/seahub/share/models.py +++ b/seahub/share/models.py @@ -7,7 +7,8 @@ from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.hashers import make_password, check_password from seahub.base.fields import LowerCaseCharField -from seahub.utils import normalize_file_path, normalize_dir_path, gen_token +from seahub.utils import normalize_file_path, normalize_dir_path, gen_token,\ + get_service_url # Get an instance of a logger logger = logging.getLogger(__name__) @@ -179,6 +180,13 @@ class FileShare(models.Model): def is_owner(self, owner): return owner == self.username + def get_full_url(self): + service_url = get_service_url().rstrip('/') + if self.is_file_share_link(): + return '%s/f/%s/' % (service_url, self.token) + else: + return '%s/d/%s/' % (service_url, self.token) + class OrgFileShareManager(models.Manager): def set_org_file_share(self, org_id, file_share): """Set a share link as org share link. diff --git a/seahub/share/templates/share/audit_code_email.html b/seahub/share/templates/share/audit_code_email.html new file mode 100644 index 0000000000..beb6d691bd --- /dev/null +++ b/seahub/share/templates/share/audit_code_email.html @@ -0,0 +1,16 @@ +{% extends 'email_base.html' %} +{% load i18n %} + +{% block email_con %} + +{% autoescape off %} + +

{% trans "Hi," %}

+ +

+{% blocktrans %}Your code is {{code}}, this code will be valid for one hour.{% endblocktrans%} +

+ +{% endautoescape %} + +{% endblock %} diff --git a/seahub/share/templates/share/share_link_audit.html b/seahub/share/templates/share/share_link_audit.html new file mode 100644 index 0000000000..d2ba39595a --- /dev/null +++ b/seahub/share/templates/share/share_link_audit.html @@ -0,0 +1,72 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block main_panel %} +
+

{% trans "Please provide your email address if you want to continue." %}

+ + +
+{% endblock %} +{% block extra_script %} + +{% endblock %} diff --git a/seahub/share/urls.py b/seahub/share/urls.py index 5837f097a8..ecb1f36fb2 100644 --- a/seahub/share/urls.py +++ b/seahub/share/urls.py @@ -18,4 +18,5 @@ urlpatterns = patterns('', url(r'^ajax/get-download-link/$', ajax_get_download_link, name='ajax_get_download_link'), url(r'^ajax/get-upload-link/$', ajax_get_upload_link, name='ajax_get_upload_link'), url(r'^ajax/private-share-dir/$', ajax_private_share_dir, name='ajax_private_share_dir'), + url(r'^ajax/get-link-audit-code/$', ajax_get_link_audit_code, name='ajax_get_link_audit_code'), ) diff --git a/seahub/share/views.py b/seahub/share/views.py index 04c5124864..2a741f9e05 100644 --- a/seahub/share/views.py +++ b/seahub/share/views.py @@ -5,6 +5,7 @@ import json from dateutil.relativedelta import relativedelta from constance import config +from django.core.cache import cache from django.core.urlresolvers import reverse from django.http import HttpResponse, HttpResponseRedirect, Http404, \ HttpResponseBadRequest @@ -33,7 +34,8 @@ from seahub.utils import render_permission_error, string2list, render_error, \ gen_shared_link, gen_shared_upload_link, gen_dir_share_link, \ gen_file_share_link, IS_EMAIL_CONFIGURED, check_filename_with_rename, \ is_valid_username, is_valid_email, send_html_email, is_org_context, \ - send_perm_audit_msg, get_origin_repo_info + send_perm_audit_msg, get_origin_repo_info, gen_token, normalize_cache_key +from seahub.utils.mail import send_html_email_with_dj_template, MAIL_PRIORITY from seahub.settings import SITE_ROOT, REPLACE_FROM_EMAIL, ADD_REPLY_TO_HEADER from seahub.profile.models import Profile @@ -1187,3 +1189,55 @@ def ajax_private_share_dir(request): # for case: only share to users and the emails are not valid data = json.dumps({"error": _("Please check the email(s) you entered")}) return HttpResponse(data, status=400, content_type=content_type) + +def ajax_get_link_audit_code(request): + """ + Generate a token, and record that token with email in cache, expires in + one hour, send token to that email address. + + User provide token and email at share link page, if the token and email + are valid, record that email in session. + """ + 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) + + dfs = FileShare.objects.get_valid_file_link_by_token(token) + ufs = UploadLinkShare.objects.get_valid_upload_link_by_token(token) + + fs = dfs if dfs else ufs + if fs is None: + return HttpResponse(json.dumps({ + 'error': _('Share link is not found') + }), status=400, content_type=content_type) + + cache_key = normalize_cache_key(email, 'share_link_audit_') + timeout = 60 * 60 # one hour + code = cache.get(cache_key) # get code from cache + if not code: # or generate new code + code = gen_token(max_length=6) + cache.set(cache_key, code, timeout) + + # send code to user via email + subject = _("Audit code for link: %s") % fs.get_full_url() + c = { + 'code': code, + } + try: + send_html_email_with_dj_template( + email, dj_template='share/audit_code_email.html', + context=c, subject=subject, priority=MAIL_PRIORITY.now + ) + return HttpResponse(json.dumps({'success': True}), status=200, + content_type=content_type) + except Exception as e: + logger.error('Failed to send audit code via email to %s') + logger.error(e) + return HttpResponse(json.dumps({ + "error": _("Failed to send audit code, please try again later.") + }), status=500, content_type=content_type) diff --git a/seahub/views/file.py b/seahub/views/file.py index 9c9c83ba0f..5a6ab8ef6f 100644 --- a/seahub/views/file.py +++ b/seahub/views/file.py @@ -47,6 +47,7 @@ from seahub.auth.decorators import login_required from seahub.base.decorators import repo_passwd_set_required from seahub.contacts.models import Contact from seahub.share.models import FileShare, check_share_link_common +from seahub.share.decorators import share_link_audit from seahub.wiki.utils import get_wiki_dirent from seahub.wiki.models import WikiDoesNotExist, WikiPageMissing from seahub.utils import show_delete_days, render_error, is_org_context, \ @@ -746,17 +747,14 @@ def _download_file_from_share_link(request, fileshare): use_onetime=False) return HttpResponseRedirect(gen_file_get_url(dl_token, filename)) -def view_shared_file(request, token): +@share_link_audit +def view_shared_file(request, fileshare): """ View file via shared link. Download share file if `dl` in request param. View raw share file if `raw` in request param. """ - assert token is not None # Checked by URLconf - - fileshare = FileShare.objects.get_valid_file_link_by_token(token) - if fileshare is None: - raise Http404 + token = fileshare.token password_check_passed, err_msg = check_share_link_common(request, fileshare) if not password_check_passed: @@ -1235,6 +1233,9 @@ def send_file_access_msg(request, repo, path, access_from): - `access_from`: web or api """ username = request.user.username + if not username: + username = request.user.email + ip = get_remote_ip(request) user_agent = request.META.get("HTTP_USER_AGENT") diff --git a/seahub/views/repo.py b/seahub/views/repo.py index ce421d3ab1..290684550c 100644 --- a/seahub/views/repo.py +++ b/seahub/views/repo.py @@ -16,6 +16,7 @@ from seaserv import seafile_api from seahub.auth.decorators import login_required from seahub.options.models import UserOptions, CryptoOptionNotSetError +from seahub.share.decorators import share_link_audit from seahub.share.models import FileShare, UploadLinkShare, \ check_share_link_common from seahub.views import gen_path_link, get_repo_dirents, \ @@ -196,12 +197,9 @@ def _download_dir_from_share_link(request, fileshare, repo, real_path): return HttpResponseRedirect(gen_file_get_url(token, dirname)) -def view_shared_dir(request, token): - assert token is not None # Checked by URLconf - - fileshare = FileShare.objects.get_valid_dir_link_by_token(token) - if fileshare is None: - raise Http404 +@share_link_audit +def view_shared_dir(request, fileshare): + token = fileshare.token password_check_passed, err_msg = check_share_link_common(request, fileshare) if not password_check_passed: @@ -293,12 +291,9 @@ def view_shared_dir(request, token): 'thumbnail_size': thumbnail_size, }, context_instance=RequestContext(request)) -def view_shared_upload_link(request, token): - assert token is not None # Checked by URLconf - - uploadlink = UploadLinkShare.objects.get_valid_upload_link_by_token(token) - if uploadlink is None: - raise Http404 +@share_link_audit +def view_shared_upload_link(request, uploadlink): + token = uploadlink.token password_check_passed, err_msg = check_share_link_common(request, uploadlink, From c69aad81b944027780a515168292826f4105b922 Mon Sep 17 00:00:00 2001 From: zhengxie Date: Sat, 23 Apr 2016 16:41:23 +0800 Subject: [PATCH 2/5] Remove used audit token --- seahub/share/decorators.py | 3 +++ seahub/share/views.py | 6 ++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/seahub/share/decorators.py b/seahub/share/decorators.py index 3e79f024ae..8d0bd704c6 100644 --- a/seahub/share/decorators.py +++ b/seahub/share/decorators.py @@ -39,8 +39,11 @@ def share_link_audit(func): 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.email = request.session.get('anonymous_email') + cache.delete(cache_key) return func(request, fileshare, *args, **kwargs) else: return render_to_response('share/share_link_audit.html', { diff --git a/seahub/share/views.py b/seahub/share/views.py index 2a741f9e05..6903d819a9 100644 --- a/seahub/share/views.py +++ b/seahub/share/views.py @@ -1218,10 +1218,8 @@ def ajax_get_link_audit_code(request): cache_key = normalize_cache_key(email, 'share_link_audit_') timeout = 60 * 60 # one hour - code = cache.get(cache_key) # get code from cache - if not code: # or generate new code - code = gen_token(max_length=6) - cache.set(cache_key, code, timeout) + code = gen_token(max_length=6) + cache.set(cache_key, code, timeout) # send code to user via email subject = _("Audit code for link: %s") % fs.get_full_url() From ae00156cdbc7bdf12281e1ebd7f54b222560a981 Mon Sep 17 00:00:00 2001 From: Daniel Pan Date: Sat, 23 Apr 2016 18:06:26 +0800 Subject: [PATCH 3/5] Improve UI --- media/css/seahub.css | 9 ++++++++ .../templates/share/share_link_audit.html | 22 ++++++++++--------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/media/css/seahub.css b/media/css/seahub.css index 276e45da6c..9f13a28092 100644 --- a/media/css/seahub.css +++ b/media/css/seahub.css @@ -869,6 +869,11 @@ textarea:-moz-placeholder {/* for FF */ font-size:14px; margin:5px 0 20px; } +.new-narrow-panel .notice { + text-align: center; + margin-top:20px; + color:#666; +} /**** wide panel ****/ /* e.g. repo decrypt page */ .wide-panel { width: 928px; @@ -991,6 +996,10 @@ textarea:-moz-placeholder {/* for FF */ .op-confirm button { margin-right:8px; } +/**** forms ****/ +.field-feedback { + color:#666; +} /**** simplemodal ****/ #basic-modal-content { display:none; diff --git a/seahub/share/templates/share/share_link_audit.html b/seahub/share/templates/share/share_link_audit.html index d2ba39595a..3e5d021888 100644 --- a/seahub/share/templates/share/share_link_audit.html +++ b/seahub/share/templates/share/share_link_audit.html @@ -2,16 +2,18 @@ {% load i18n %} {% block main_panel %} -
-

{% trans "Please provide your email address if you want to continue." %}

+
+

{% trans "Email verification" %}

+

{% trans "Please provide your email address to continue." %}

-