1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-20 19:08:21 +00:00

[passwd strength] add STRONG password check(default not enabled) when user sign-up or change-password

This commit is contained in:
imwhatiam
2014-07-29 16:42:08 +08:00
parent c8cf757191
commit 90ba936514
11 changed files with 330 additions and 215 deletions

View File

@@ -7,10 +7,13 @@ from seahub.base.accounts import User
from seahub.auth import authenticate
from seahub.auth.tokens import default_token_generator
from seahub.utils import IS_EMAIL_CONFIGURED, send_html_email, \
is_valid_username, is_ldap_user
is_valid_username, is_ldap_user, is_user_password_strong
from captcha.fields import CaptchaField
from seahub.settings import USER_STRONG_PASSWORD_REQUIRED, \
USER_PASSWORD_STRENGTH_LEVEL, USER_PASSWORD_MIN_LENGTH
class AuthenticationForm(forms.Form):
"""
Base class for authenticating users. Extend this to get a form that accepts
@@ -64,7 +67,7 @@ class AuthenticationForm(forms.Form):
class CaptchaAuthenticationForm(AuthenticationForm):
captcha = CaptchaField()
class PasswordResetForm(forms.Form):
email = forms.EmailField(label=_("E-mail"), max_length=255)
@@ -74,7 +77,7 @@ class PasswordResetForm(forms.Form):
"""
if not IS_EMAIL_CONFIGURED:
raise forms.ValidationError(_(u'Failed to send email, email service is not properly configured, please contact administrator.'))
email = self.cleaned_data["email"].lower().strip()
# TODO: add filter method to UserManager
@@ -121,9 +124,21 @@ class SetPasswordForm(forms.Form):
def __init__(self, user, *args, **kwargs):
self.user = user
super(SetPasswordForm, self).__init__(*args, **kwargs)
def clean_new_password1(self):
if 'new_password1' in self.cleaned_data:
pwd = self.cleaned_data['new_password1']
if USER_STRONG_PASSWORD_REQUIRED is True:
if is_user_password_strong(pwd) is True:
return pwd
else:
raise forms.ValidationError(_("%s characters or more, include %s types or more of these: letters(case sensitive), numbers, and symbols") % (USER_PASSWORD_MIN_LENGTH, USER_PASSWORD_STRENGTH_LEVEL))
else:
return pwd
def clean_new_password2(self):
password1 = self.cleaned_data.get('new_password1')
password2 = self.cleaned_data.get('new_password2')

View File

@@ -25,6 +25,8 @@ from seahub.auth.tokens import default_token_generator
from seahub.base.accounts import User
from seahub.utils import is_ldap_user
from seahub.utils.ip import get_remote_ip
from seahub.settings import USER_PASSWORD_MIN_LENGTH, \
USER_STRONG_PASSWORD_REQUIRED, USER_PASSWORD_STRENGTH_LEVEL
# Get an instance of a logger
logger = logging.getLogger(__name__)
@@ -35,9 +37,9 @@ def log_user_in(request, user, redirect_to):
# Light security check -- make sure redirect_to isn't garbage.
if not redirect_to or ' ' in redirect_to:
redirect_to = settings.LOGIN_REDIRECT_URL
# Heavier security check -- redirects to http://example.com should
# not be allowed, but things like /view/?param=http://example.com
# Heavier security check -- redirects to http://example.com should
# not be allowed, but things like /view/?param=http://example.com
# should be allowed. This regex checks if there is a '//' *before* a
# question mark.
elif '//' in redirect_to and re.match(r'[^\?]*//', redirect_to):
@@ -103,7 +105,7 @@ def _incr_login_faied_attempts(username=None, ip=None):
def _clear_login_failed_attempts(request):
"""Clear login failed attempts records.
Arguments:
- `request`:
"""
@@ -167,9 +169,9 @@ def login(request, template_name='registration/login.html',
form = CaptchaAuthenticationForm(request)
else:
form = authentication_form(request)
request.session.set_test_cookie()
if Site._meta.installed:
current_site = Site.objects.get_current()
else:
@@ -213,7 +215,7 @@ def login_simple_check(request):
user = User.objects.get(email=username)
except User.DoesNotExist:
raise Http404
for backend in get_backends():
user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
@@ -223,7 +225,7 @@ def login_simple_check(request):
else:
raise Http404
def logout(request, next_page=None, template_name='registration/logged_out.html', redirect_field_name=REDIRECT_FIELD_NAME):
"Logs out the user and displays 'You are logged out' message."
from seahub.auth import logout
@@ -255,7 +257,7 @@ def redirect_to_login(next, login_url=None, redirect_field_name=REDIRECT_FIELD_N
# 4 views for password reset:
# - password_reset sends the mail
# - password_reset_done shows a success message for the above
# - password_reset_confirm checks the link the user clicked and
# - password_reset_confirm checks the link the user clicked and
# prompts for a new password
# - password_reset_complete shows a success message for the above
@@ -352,8 +354,12 @@ def password_change(request, template_name='registration/password_change_form.ht
return HttpResponseRedirect(post_change_redirect)
else:
form = password_change_form(user=request.user)
return render_to_response(template_name, {
'form': form,
'min_len': USER_PASSWORD_MIN_LENGTH,
'strong_pwd_required': USER_STRONG_PASSWORD_REQUIRED,
'level': USER_PASSWORD_STRENGTH_LEVEL,
}, context_instance=RequestContext(request))
def password_change_done(request, template_name='registration/password_change_done.html'):

View File

@@ -13,12 +13,14 @@ from registration import signals
from seaserv import ccnet_threaded_rpc, unset_repo_passwd, is_passwd_set
from seahub.profile.models import Profile, DetailedProfile
from seahub.utils import is_valid_username
from seahub.utils import is_valid_username, is_user_password_strong
try:
from seahub.settings import CLOUD_MODE
except ImportError:
CLOUD_MODE = False
from seahub.settings import USER_STRONG_PASSWORD_REQUIRED, \
USER_PASSWORD_MIN_LENGTH, USER_PASSWORD_STRENGTH_LEVEL
UNUSABLE_PASSWORD = '!' # This will never be a valid hash
@@ -463,40 +465,14 @@ class RegistrationForm(forms.Form):
def clean_password1(self):
if 'password1' in self.cleaned_data:
pwd = self.cleaned_data['password1']
if len(pwd) < 6:
raise forms.ValidationError(
_("Passwords must have at least 6 characters."))
if USER_STRONG_PASSWORD_REQUIRED is True:
if is_user_password_strong(pwd) is True:
return pwd
else:
raise forms.ValidationError(_("%s characters or more, include %s types or more of these: letters(case sensitive), numbers, and symbols") % (USER_PASSWORD_MIN_LENGTH, USER_PASSWORD_STRENGTH_LEVEL))
else:
num = 0
for letter in pwd:
# get ascii dec
# bitwise OR
num |= self.get_char_mode(ord(letter))
level = self.caculate_bitwise(num)
if level == 1:
raise forms.ValidationError(_("Passwords must contain at least 2 types: uppercase letters, lowercase letters, numbers, and symbols"))
return self.cleaned_data['password1']
def get_char_mode(self, n):
if (n >= 48 and n <= 57): #nums
return 1;
if (n >= 65 and n <= 90): #uppers
return 2;
if (n >= 97 and n <= 122): #lowers
return 4;
else:
return 8;
def caculate_bitwise(self, num):
level = 0
for i in range(4):
# bitwise AND
if (num&1):
level += 1
# Right logical shift
num = num >> 1
return level
return pwd
def clean_password2(self):
"""

View File

@@ -104,10 +104,10 @@ MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'seahub.auth.middleware.AuthenticationMiddleware',
'seahub.base.middleware.BaseMiddleware',
'seahub.base.middleware.BaseMiddleware',
'seahub.base.middleware.InfobarMiddleware',
)
@@ -161,7 +161,7 @@ LOCALE_PATHS = (
TEMPLATE_CONTEXT_PROCESSORS = (
'django.contrib.auth.context_processors.auth',
'django.core.context_processors.debug',
'django.core.context_processors.i18n',
'django.core.context_processors.i18n',
'django.core.context_processors.media',
'djblets.util.context_processors.siteRoot',
'django.core.context_processors.request',
@@ -207,6 +207,18 @@ SHOW_REPO_DOWNLOAD_BUTTON = False
# mininum length for password of encrypted library
REPO_PASSWORD_MIN_LENGTH = 8
# mininum length for user's password
USER_PASSWORD_MIN_LENGTH = 6
# LEVEL based on four types of input:
# num, upper letter, lower letter, other symbols
# '3' means password must have at least 3 types of the above.
USER_PASSWORD_STRENGTH_LEVEL = 3
# default False, only check USER_PASSWORD_MIN_LENGTH
# when True, check password strength level, STRONG(or above) is allowed
USER_STRONG_PASSWORD_REQUIRED = False
# Using server side crypto by default, otherwise, let user choose crypto method.
FORCE_SERVER_CRYPTO = True
@@ -219,7 +231,7 @@ OFFICE_PREVIEW_MAX_SIZE = 2 * 1024 * 1024
USE_PDFJS = True
FILE_ENCODING_LIST = ['auto', 'utf-8', 'gbk', 'ISO-8859-1', 'ISO-8859-5']
FILE_ENCODING_TRY_LIST = ['utf-8', 'gbk']
HIGHLIGHT_KEYWORD = False # If True, highlight the keywords in the file when the visit is via clicking a link in 'search result' page.
HIGHLIGHT_KEYWORD = False # If True, highlight the keywords in the file when the visit is via clicking a link in 'search result' page.
# Common settings(file extension, storage) for avatar and group avatar.
AVATAR_FILE_STORAGE = '' # Replace with 'seahub.base.database_storage.DatabaseStorage' if save avatar files to database
@@ -346,7 +358,7 @@ LOGGING = {
'filename': os.path.join(LOG_DIR, 'seahub.log'),
'maxBytes': 1024*1024*10, # 10 MB
'formatter':'standard',
},
},
'request_handler': {
'level':'WARN',
'class':'logging.handlers.RotatingFileHandler',
@@ -452,7 +464,7 @@ def load_local_settings(module):
elif re.search('^[A-Z]', attr):
globals()[attr] = getattr(module, attr)
# Load seahub_extra_settings.py
try:
from seahub_extra import seahub_extra_settings
@@ -478,7 +490,7 @@ try:
except ImportError:
pass
else:
# In server release, sqlite3 db file is <topdir>/seahub.db
# In server release, sqlite3 db file is <topdir>/seahub.db
DATABASES['default']['NAME'] = os.path.join(install_topdir, 'seahub.db')
if 'win32' not in sys.platform:
# In server release, gunicorn is used to deploy seahub

View File

@@ -7,9 +7,13 @@
<h2 class="hd">{% trans "Password Modification" %}</h2>
<form action="" method="post" class="con">{% csrf_token %}
<label for="id_old_password">{% trans "Current Password: " %}</label>
{{ form.old_password }} {{ form.old_password.errors }}
{{ form.old_password }} {{ form.old_password.errors }}
<label for="id_new_password1">{% trans "New Password: " %}</label>
{{ form.new_password1 }} {{ form.new_password1.errors }}
{% if strong_pwd_required %}
<span class="icon-question-sign" title="{% blocktrans %}{{min_len}} characters or more, include {{level}} types or more of these: letters(case sensitive), numbers, and symbols"{% endblocktrans%}></span>
{% endif %}
{{ form.new_password1 }} {{ form.new_password1.errors }}
<div id="pwd_strength"></div>
<label for="id_new_password2">{% trans "Confirm Password: " %}</label>
{{ form.new_password2 }} {{ form.new_password2.errors }}
@@ -22,6 +26,47 @@
{% block extra_script %}
<script type="text/javascript">
$('[type="password"]').addClass('input');
$('[type="password"]').addClass('input');
{% include "snippets/password_strength_js.html" %}
$("#id_new_password1").keyup(function() {
var pwd = $(this).val(),
level = getStrengthLevel(pwd);
if (pwd.length > 0) {
showStrength(level);
};
});
$('form').submit(function(){
var old_pwd = $.trim($('input[name="old_password"]').val()),
pwd1 = $.trim($('input[name="new_password1"]').val()),
pwd2 = $.trim($('input[name="new_password2"]').val()),
level = getStrengthLevel(pwd1);
if (!old_pwd) {
$('.error').removeClass('hide').html("{% trans "Current password cannot be blank" %}");
return false;
}
if (!pwd1) {
$('.error').removeClass('hide').html("{% trans "Password cannot be blank" %}");
return false;
}
if (!pwd2) {
$('.error').removeClass('hide').html("{% trans "Please enter the password again" %}");
return false;
}
if (pwd1 != pwd2) {
$('.error').removeClass('hide').html("{% trans "Passwords don't match" %}");
return false;
}
{% if strong_pwd_required %}
if (level < {{level}}) {
$('.error').removeClass('hide').html("{% blocktrans %}{{min_len}} characters or more, include {{level}} types or more of these: letters(case sensitive), numbers, and symbols{% endblocktrans %}");
return false;
}
{% endif %}
});
</script>
{% endblock %}

View File

@@ -16,7 +16,10 @@
<label for="id_email">{% trans "Email" %}</label>
{{ form.email }} {{ form.email.errors }}
<label for="id_password1">{% trans "Password" %}</label> <span class="icon-question-sign" title="{% trans "6 characters or more, include 2 types or more of these: letters(case sensitive), numbers, and symbols" %}"></span>
<label for="id_password1">{% trans "Password" %}</label>
{% if strong_pwd_required %}
<span class="icon-question-sign" title="{% blocktrans %}{{min_len}} characters or more, include {{level}} types or more of these: letters(case sensitive), numbers, and symbols"{% endblocktrans%}></span>
{% endif %}
{{ form.password1 }} {{ form.password1.errors }}
<div id="pwd_strength"></div>
<label for="id_password2">{% trans "Confirm Password" %}</label>
@@ -40,73 +43,17 @@
{% block extra_script %}
<script type="text/javascript">
{% include "snippets/password_strength_js.html" %}
$("#id_password1").keyup(function() {
var level = getStrengthLevel($(this).val());
showStrength(level);
});
var pwd = $(this).val(),
level = getStrengthLevel(pwd);
function getStrengthLevel(pwd) {
var num = 0,
min_passwd_len = 6;
if (pwd.length < min_passwd_len) {
return 0;
} else {
for (var i = 0; i < pwd.length; i++) {
// return the unicode
// bitwise OR
num |= getCharMode(pwd.charCodeAt(i));
};
return calculateBitwise(num);
if (pwd.length > 0) {
showStrength(level);
};
}
function getCharMode(n) {
if (n >= 48 && n <= 57) // nums
return 1;
if (n >= 65 && n <= 90) // uppers
return 2;
if (n >= 97 && n <= 122) // lowers
return 4;
else
return 8;
}
function calculateBitwise(num) {
var level = 0;
for (var i = 0; i < 4; i++){
// bitwise AND
if (num&1) level++;
// Right logical shift
num>>>=1;
}
return level;
}
function showStrength(level) {
var strength_ct = $("#pwd_strength");
var strength = [
"{% trans "too weak" %}",
"{% trans "weak" %}",
"{% trans "medium" %}",
"{% trans "strong" %}"
];
switch (level) {
case 0:
strength_ct.html(strength[0]).css("color", '#F00000');
break;
case 1:
strength_ct.html('<span style="background:#db4747;">' + strength[1] + '</span><span>' + strength[2] + '</span><span>' + strength[3] + '</span>');
break;
case 2:
strength_ct.html('<span>' + strength[1] + '</span><span style="background:#fdd64d;">' + strength[2] + '</span><span>' + strength[3] + '</span>');
break;
case 3:
case 4:
strength_ct.html('<span>' + strength[1] + '</span><span>' + strength[2] + '</span><span style="background:#4aa323;">' + strength[3] + '</span>');
break;
}
}
});
$('form').submit(function(){
var email = $.trim($('input[name="email"]').val()),
@@ -130,14 +77,12 @@ $('form').submit(function(){
$('.error').removeClass('hide').html("{% trans "Passwords don't match" %}");
return false;
}
if (pwd1.length < 6) {
$('.error').removeClass('hide').html("{% trans "Passwords must have at least 6 characters" %}");
return false;
}
if (level <= 2) {
$('.error').removeClass('hide').html("{% trans "Passwords contain at least 2 types: uppercase letters, lowercase letters, numbers, and symbols" %}");
return false;
}
{% if strong_pwd_required %}
if (level < {{level}}) {
$('.error').removeClass('hide').html("{% blocktrans %}{{min_len}} characters or more, include {{level}} types or more of these: letters(case sensitive), numbers, and symbols{% endblocktrans %}");
return false;
}
{% endif %}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,62 @@
{% load i18n %}
function getStrengthLevel(pwd) {
var num = 0;
if (pwd.length < {{min_len}}) {
return 0;
} else {
for (var i = 0; i < pwd.length; i++) {
// return the unicode
// bitwise OR
num |= getCharMode(pwd.charCodeAt(i));
};
return calculateBitwise(num);
};
}
function getCharMode(n) {
if (n >= 48 && n <= 57) // nums
return 1;
if (n >= 65 && n <= 90) // uppers
return 2;
if (n >= 97 && n <= 122) // lowers
return 4;
else
return 8;
}
function calculateBitwise(num) {
var level = 0;
for (var i = 0; i < 4; i++){
// bitwise AND
if (num&1) level++;
// Right logical shift
num>>>=1;
}
return level;
}
function showStrength(level) {
var strength_ct = $("#pwd_strength");
var strength = [
"{% trans "too weak" %}",
"{% trans "weak" %}",
"{% trans "medium" %}",
"{% trans "strong" %}"
];
switch (level) {
case 0:
strength_ct.html(strength[0]).css("color", '#F00000');
break;
case 1:
strength_ct.html('<span style="background:#db4747;">' + strength[1] + '</span><span>' + strength[2] + '</span><span>' + strength[3] + '</span>');
break;
case 2:
strength_ct.html('<span>' + strength[1] + '</span><span style="background:#fdd64d;">' + strength[2] + '</span><span>' + strength[3] + '</span>');
break;
case 3:
case 4:
strength_ct.html('<span>' + strength[1] + '</span><span>' + strength[2] + '</span><span style="background:#4aa323;">' + strength[3] + '</span>');
break;
}
}

View File

@@ -15,7 +15,7 @@ $(function() {
$('#encrypt-switch').click(function () {
var form = $('#repo-create-form'),
pwd_input = $('input[type="password"]', form);
if ($(this).attr('checked')) {
pwd_input.attr('disabled', false).removeClass('input-disabled');
} else {
@@ -23,7 +23,7 @@ $('#encrypt-switch').click(function () {
}
});
$('#repo-create-form').submit(function() {
var form = $(this),
var form = $(this),
form_id = form.attr('id'),
name = $('[name="repo_name"]', form).val(),
desc = $('[name="repo_desc"]', form).val(),

View File

@@ -29,7 +29,8 @@ from seaserv import seafserv_rpc, seafserv_threaded_rpc, get_repo, get_commits,\
list_personal_repos_by_owner, get_group_repos, \
list_inner_pub_repos, CCNET_CONF_PATH, SERVICE_URL
import seahub.settings
from seahub.settings import SITE_NAME, MEDIA_URL, LOGO_PATH
from seahub.settings import SITE_NAME, MEDIA_URL, LOGO_PATH, \
USER_PASSWORD_STRENGTH_LEVEL, USER_PASSWORD_MIN_LENGTH
try:
from seahub.settings import EVENTS_CONFIG_FILE
except ImportError:
@@ -78,7 +79,7 @@ PREVIEW_FILEEXT = {
def gen_fileext_type_map():
"""
Generate previewed file extension and file type relation map.
"""
d = {}
for filetype in PREVIEW_FILEEXT.keys():
@@ -156,7 +157,7 @@ def gen_token(max_length=5):
Generate a random token.
"""
return uuid.uuid4().hex[:max_length]
def normalize_cache_key(value, prefix=None):
@@ -165,7 +166,7 @@ def normalize_cache_key(value, prefix=None):
"""
key = value if prefix is None else prefix + value
return urlquote(key)
def get_repo_last_modify(repo):
""" Get last modification time for a repo.
@@ -332,7 +333,7 @@ def get_file_revision_id_size(repo_id, commit_id, path):
def new_merge_with_no_conflict(commit):
"""Check whether a commit is a new merge, and no conflict.
Arguments:
- `commit`:
"""
@@ -358,7 +359,7 @@ def get_commit_before_new_merge(commit):
commit = p1 if p1.ctime > p2.ctime else p2
assert new_merge_with_no_conflict(commit) is False
return commit
def gen_inner_file_get_url(token, filename):
@@ -386,7 +387,7 @@ def get_max_upload_file_size():
Returns ``None`` if this value is not set.
"""
return seaserv.MAX_UPLOAD_FILE_SIZE
def gen_block_get_url(token, blkid):
"""
Generate fileserver block url.
@@ -425,7 +426,7 @@ def string2list(string):
continue
s.add(e)
return [ x for x in s ]
# def get_cur_ctx(request):
# ctx_dict = request.session.get('current_context', {
# 'base_template': 'myhome_base.html',
@@ -439,12 +440,12 @@ def string2list(string):
def is_org_context(request):
"""An organization context is a virtual private Seafile instance on cloud
service.
Arguments:
- `request`:
"""
return request.cloud_mode and request.user.org is not None
# def check_and_get_org_by_repo(repo_id, user):
# """
# Check whether repo is org repo, get org info if it is, and set
@@ -460,7 +461,7 @@ def is_org_context(request):
# else:
# org = None
# base_template = 'myhome_base.html'
# return org, base_template
def check_and_get_org_by_group(group_id, user):
@@ -478,9 +479,9 @@ def check_and_get_org_by_group(group_id, user):
else:
org = None
base_template = 'myhome_base.html'
return org, base_template
# events related
if EVENTS_CONFIG_FILE:
import seafevents
@@ -510,7 +511,7 @@ if EVENTS_CONFIG_FILE:
events = _get_events_inner(ev_session, username, next_start, count)
if not events:
break
for e1 in events:
duplicate = False
for e2 in valid_events:
@@ -519,13 +520,13 @@ if EVENTS_CONFIG_FILE:
new_merge = False
if hasattr(e1, 'commit') and new_merge_with_no_conflict(e1.commit):
new_merge = True
if not duplicate and not new_merge:
valid_events.append(e1)
total_used = total_used + 1
if len(valid_events) == count:
break
if len(valid_events) == count:
break
next_start = next_start + len(events)
@@ -569,14 +570,14 @@ if EVENTS_CONFIG_FILE:
break
if len(valid_events) == limit:
break
break
next_start = next_start + len(valid_events)
return valid_events
def get_user_events(username, start, count):
"""Return user events list and a new start.
For example:
``get_user_events('foo@example.com', 0, 10)`` returns the first 10
events.
@@ -584,7 +585,7 @@ if EVENTS_CONFIG_FILE:
15th events.
"""
return _get_events(username, start, count)
def get_org_user_events(org_id, username, start, count):
return _get_events(username, start, count, org_id=org_id)
@@ -600,7 +601,7 @@ def calc_file_path_hash(path, bits=12):
path = path.encode('UTF-8')
path_hash = hashlib.md5(urllib2.quote(path)).hexdigest()[:bits]
return path_hash
def get_service_url():
@@ -616,7 +617,7 @@ def get_server_id():
def get_site_scheme_and_netloc():
"""Return a string contains site scheme and network location part from
service url.
For example:
>>> get_site_scheme_and_netloc("https://example.com:8000/seafile/")
https://example.com:8000
@@ -629,7 +630,7 @@ def send_html_email(subject, con_template, con_context, from_email, to_email):
"""Send HTML email
"""
base_context = {
'url_base': get_site_scheme_and_netloc(),
'url_base': get_site_scheme_and_netloc(),
'site_name': SITE_NAME,
'media_url': MEDIA_URL,
'logo_path': LOGO_PATH,
@@ -639,7 +640,7 @@ def send_html_email(subject, con_template, con_context, from_email, to_email):
msg = EmailMessage(subject, t.render(Context(con_context)), from_email, to_email)
msg.content_subtype = "html"
msg.send()
def gen_dir_share_link(token):
"""Generate directory share link.
"""
@@ -727,7 +728,7 @@ def convert_cmmt_desc_link(commit):
"""
repo_id = commit.repo_id
cmmt_id = commit.id
conv_link_url = reverse('convert_cmmt_desc_link')
conv_link_url = reverse('convert_cmmt_desc_link')
def link_repl(matchobj):
op = matchobj.group(1)
@@ -776,7 +777,7 @@ def api_convert_desc_link(e):
if file_or_dir not in d.name:
# skip to next diff_result if file/folder user clicked does not
# match the diff_result
continue
continue
if d.status == 'add' or d.status == 'mod':
e.link = "api://repo/%s/files/?p=/%s" % (repo_id, d.name)
@@ -820,7 +821,7 @@ if EVENTS_CONFIG_FILE:
return seafevents.get_office_converter_limit(config)
HAS_OFFICE_CONVERTER = check_office_converter_enabled()
if HAS_OFFICE_CONVERTER:
OFFICE_HTML_DIR = get_office_converter_html_dir()
@@ -849,7 +850,7 @@ if HAS_OFFICE_CONVERTER:
rpc = _get_office_converter_rpc()
return rpc.query_file_pages(file_id)
def get_converted_html_detail(file_id):
def get_converted_html_detail(file_id):
d = {}
outline_file = os.path.join(OFFICE_HTML_DIR, file_id, 'file.outline')
@@ -945,3 +946,51 @@ def user_traffic_over_limit(username):
month_traffic = stat['file_view'] + stat['file_download'] + stat['dir_download']
return True if month_traffic >= traffic_limit else False
def is_user_password_strong(password):
"""Return ``True`` if user's password is STRONG, otherwise ``False``.
STRONG means password has at least USER_PASSWORD_STRENGTH_LEVEL(3) types of the bellow:
num, upper letter, lower letter, other symbols
"""
if len(password) < USER_PASSWORD_MIN_LENGTH:
return False
else:
num = 0
for letter in password:
# get ascii dec
# bitwise OR
num |= get_char_mode(ord(letter))
if calculate_bitwise(num) < USER_PASSWORD_STRENGTH_LEVEL:
return False
else:
return True
def get_char_mode(n):
"""Return different num according to the type of given letter:
'1': num,
'2': upper_letter,
'4': lower_letter,
'8': other symbols
"""
if (n >= 48 and n <= 57): #nums
return 1;
if (n >= 65 and n <= 90): #uppers
return 2;
if (n >= 97 and n <= 122): #lowers
return 4;
else:
return 8;
def calculate_bitwise(num):
"""Return different level according to the given num:
"""
level = 0
for i in range(4):
# bitwise AND
if (num&1):
level += 1
# Right logical shift
num = num >> 1
return level

View File

@@ -79,7 +79,7 @@ def root(request):
def validate_owner(request, repo_id):
"""
Check whether user in the request owns the repo.
"""
ret = is_repo_owner(request.user.username, repo_id)
@@ -131,7 +131,7 @@ def get_system_default_repo_id():
def check_repo_access_permission(repo_id, user):
"""Check repo access permission of a user, always return 'rw' when repo is
system repo and user is admin.
Arguments:
- `repo_id`:
- `user`:
@@ -140,13 +140,13 @@ def check_repo_access_permission(repo_id, user):
return 'rw'
else:
return seafile_api.check_repo_access_permission(repo_id, user.username)
def get_file_access_permission(repo_id, path, username):
"""Check user has permission to view the file.
1. check whether this file is private shared.
2. if failed, check whether the parent of this directory is private shared.
"""
pfs = PrivateFileDirShare.objects.get_private_share_in_file(username,
repo_id, path)
if pfs is None:
@@ -157,11 +157,11 @@ def get_file_access_permission(repo_id, path, username):
return None
else:
return pfs.permission
def gen_path_link(path, repo_name):
"""
Generate navigate paths and links in repo page.
"""
if path and path[-1] != '/':
path += '/'
@@ -178,9 +178,9 @@ def gen_path_link(path, repo_name):
if repo_name:
paths.insert(0, repo_name)
links.insert(0, '/')
zipped = zip(paths, links)
return zipped
def get_repo_dirents(request, repo, commit, path, offset=-1, limit=-1):
@@ -584,7 +584,7 @@ def repo_change_passwd(request, repo_id):
{'repo_name': repo.name})
return HttpResponse(json.dumps({'success': True}),
content_type=content_type)
def upload_error_msg (code):
err_msg = _(u'Internal Server Error')
if (code == 0):
@@ -644,7 +644,7 @@ def update_file_error(request, repo_id):
'zipped': zipped,
'err_msg': err_msg,
}, context_instance=RequestContext(request))
@login_required
def repo_history(request, repo_id):
"""
@@ -663,8 +663,8 @@ def repo_history(request, repo_id):
server_crypto = UserOptions.objects.is_server_crypto(username)
except CryptoOptionNotSetError:
# Assume server_crypto is ``False`` if this option is not set.
server_crypto = False
server_crypto = False
password_set = False
if repo.props.encrypted and \
(repo.enc_version == 1 or (repo.enc_version == 2 and server_crypto)):
@@ -723,8 +723,8 @@ def repo_view_snapshot(request, repo_id):
server_crypto = UserOptions.objects.is_server_crypto(username)
except CryptoOptionNotSetError:
# Assume server_crypto is ``False`` if this option is not set.
server_crypto = False
server_crypto = False
password_set = False
if repo.props.encrypted and \
(repo.enc_version == 1 or (repo.enc_version == 2 and server_crypto)):
@@ -779,8 +779,8 @@ def repo_history_revert(request, repo_id):
server_crypto = UserOptions.objects.is_server_crypto(username)
except CryptoOptionNotSetError:
# Assume server_crypto is ``False`` if this option is not set.
server_crypto = False
server_crypto = False
password_set = False
if repo.props.encrypted and \
(repo.enc_version == 1 or (repo.enc_version == 2 and server_crypto)):
@@ -889,7 +889,7 @@ def create_default_library(request):
default_repo, '/', obj_name, username, 0)
except SearpcError as e:
logger.error(e)
return
return
UserOptions.objects.set_default_repo(username, default_repo)
@@ -984,7 +984,7 @@ def myhome(request):
@user_mods_check
def starred(request):
"""List starred files.
Arguments:
- `request`:
"""
@@ -995,7 +995,7 @@ def starred(request):
return render_to_response('starred.html', {
"starred_files": starred_files,
}, context_instance=RequestContext(request))
@login_required
@user_mods_check
@@ -1007,24 +1007,24 @@ def devices(request):
return render_to_response('devices.html', {
"devices": user_devices,
}, context_instance=RequestContext(request))
@login_required_ajax
def unlink_device(request):
content_type = 'application/json; charset=utf-8'
platform = request.POST.get('platform', '')
device_id = request.POST.get('device_id', '')
if not platform or not device_id:
return HttpResponseBadRequest(json.dumps({'error': _(u'Argument missing')}),
content_type=content_type)
try:
do_unlink_device(request.user.username, platform, device_id)
except:
return HttpResponse(json.dumps({'error': _(u'Internal server error')}),
status=500, content_type=content_type)
return HttpResponse(json.dumps({'success': True}), content_type=content_type)
@login_required
@@ -1078,7 +1078,7 @@ def unsetinnerpub(request, repo_id):
# quota_usage = seafserv_threaded_rpc.get_user_quota_usage(owner_name)
# user_dict = user_info(request, owner_name)
# return render_to_response('ownerhome.html', {
# "owned_repos": owned_repos,
# "quota_usage": quota_usage,
@@ -1090,10 +1090,10 @@ def unsetinnerpub(request, repo_id):
def repo_set_access_property(request, repo_id):
ap = request.GET.get('ap', '')
seafserv_threaded_rpc.repo_set_access_property(repo_id, ap)
return HttpResponseRedirect(reverse('repo', args=[repo_id]))
@login_required
@login_required
def repo_del_file(request, repo_id):
if check_repo_access_permission(repo_id, request.user) != 'rw':
return render_permission_error(request, _('Failed to delete file.'))
@@ -1109,7 +1109,7 @@ def repo_del_file(request, repo_id):
url = reverse('repo', args=[repo_id]) + ('?p=%s' % urllib2.quote(parent_dir.encode('utf-8')))
return HttpResponseRedirect(url)
def repo_access_file(request, repo_id, obj_id):
"""Delete or download file.
TODO: need to be rewrite.
@@ -1190,7 +1190,7 @@ def file_upload_progress_page(request):
'upload_progress_con_id': upload_progress_con_id,
}, context_instance=RequestContext(request))
@login_required
@login_required
def validate_filename(request):
repo_id = request.GET.get('repo_id')
filename = request.GET.get('filename')
@@ -1240,7 +1240,7 @@ def render_file_revisions (request, repo_id):
if not commits:
return render_error(request)
# Check whether user is repo owner
if validate_owner(request, repo_id):
is_owner = True
@@ -1400,10 +1400,10 @@ def view_shared_dir(request, token):
repo_id = fileshare.repo_id
path = request.GET.get('p', '')
path = fileshare.path if not path else path
if path[-1] != '/': # Normalize dir path
if path[-1] != '/': # Normalize dir path
path += '/'
if not path.startswith(fileshare.path):
if not path.startswith(fileshare.path):
path = fileshare.path # Can not view upper dir of shared dir
repo = get_repo(repo_id)
@@ -1417,13 +1417,13 @@ def view_shared_dir(request, token):
zipped = gen_path_link(path, '')
if path == fileshare.path: # When user view the shared dir..
# increase shared link view_cnt,
# increase shared link view_cnt,
fileshare = FileShare.objects.get(token=token)
fileshare.view_cnt = F('view_cnt') + 1
fileshare.save()
traffic_over_limit = user_traffic_over_limit(fileshare.username)
return render_to_response('view_shared_dir.html', {
'repo': repo,
'token': token,
@@ -1461,7 +1461,7 @@ def view_shared_upload_link(request, token):
else:
return render_to_response('share_access_validation.html', d,
context_instance=RequestContext(request))
username = uploadlink.username
repo_id = uploadlink.repo_id
path = uploadlink.path
@@ -1525,9 +1525,9 @@ def pubrepo(request):
"""
if not request.user.permissions.can_view_org():
raise Http404
username = request.user.username
if request.cloud_mode and request.user.org is not None:
org_id = request.user.org.org_id
public_repos = seaserv.list_org_inner_pub_repos(org_id, username)
@@ -1538,7 +1538,7 @@ def pubrepo(request):
'public_repos': public_repos,
'create_shared_repo': True,
}, context_instance=RequestContext(request))
if not request.cloud_mode:
public_repos = seaserv.list_inner_pub_repos(username)
for r in public_repos:
@@ -1558,14 +1558,14 @@ def pubgrp(request):
"""
if not request.user.permissions.can_view_org():
raise Http404
if request.cloud_mode and request.user.org is not None:
org_id = request.user.org.org_id
groups = seaserv.get_org_groups(org_id, -1, -1)
return render_to_response('organizations/pubgrp.html', {
'groups': groups,
}, context_instance=RequestContext(request))
if not request.cloud_mode:
groups = seaserv.get_personal_groups(-1, -1)
return render_to_response('pubgrp.html', {
@@ -1579,7 +1579,7 @@ def get_pub_users(request, start, limit):
url_prefix = request.user.org.url_prefix
users_plus_one = seaserv.get_org_users_by_url_prefix(url_prefix,
start, limit)
elif request.cloud_mode:
raise Http404 # no pubuser in cloud mode
@@ -1608,7 +1608,7 @@ def pubuser(request):
"""
if not request.user.permissions.can_view_org():
raise Http404
# Make sure page request is an int. If not, deliver first page.
try:
current_page = int(request.GET.get('page', '1'))
@@ -1629,21 +1629,21 @@ def pubuser(request):
users = users_plus_one[:per_page]
username = request.user.username
contacts = Contact.objects.get_contacts_by_user(username)
contact_emails = []
contact_emails = []
for c in contacts:
contact_emails.append(c.contact_email)
for u in users:
if u.email == username or u.email in contact_emails:
u.can_be_contact = False
else:
u.can_be_contact = True
u.can_be_contact = True
return render_to_response('pubuser.html', {
'users': users,
'current_page': current_page,
'has_prev': has_prev,
'has_next': has_next,
'page_range': page_range,
'page_range': page_range,
}, context_instance=RequestContext(request))
@login_required_ajax
@@ -1693,7 +1693,7 @@ def repo_download_dir(request, repo_id):
dirname = os.path.basename(path.rstrip('/')) # Here use `rstrip` to cut out last '/' in path
else:
dirname = repo.name
allow_download = False
fileshare_token = request.GET.get('t', '')
from_shared_link = False
@@ -1789,18 +1789,18 @@ def group_events_data(events):
utc = dt.replace(tzinfo=timezone.utc)
local = timezone.make_naive(utc, tz)
return local
event_groups = []
for e in events:
e.time = utc_to_local(e.timestamp)
e.date = e.time.strftime("%Y-%m-%d")
e.date = e.time.strftime("%Y-%m-%d")
if e.etype == 'repo-update':
e.author = e.commit.creator_name
elif e.etype == 'repo-create':
e.author = e.creator
else:
e.author = e.repo_owner
if len(event_groups) == 0 or \
len(event_groups) > 0 and e.date != event_groups[-1]['date']:
event_group = {}
@@ -1814,7 +1814,7 @@ def group_events_data(events):
def pdf_full_view(request):
'''For pdf view with pdf.js.'''
repo_id = request.GET.get('repo_id', '')
obj_id = request.GET.get('obj_id', '')
file_name = request.GET.get('file_name', '')
@@ -1840,7 +1840,7 @@ def convert_cmmt_desc_link(request):
# perm check
if check_repo_access_permission(repo_id, request.user) is None:
raise Http404
diff_result = seafserv_threaded_rpc.get_diff(repo_id, '', cmmt_id)
if not diff_result:
raise Http404
@@ -1849,7 +1849,7 @@ def convert_cmmt_desc_link(request):
if name not in d.name:
# skip to next diff_result if file/folder user clicked does not
# match the diff_result
continue
continue
if d.status == 'add' or d.status == 'mod': # Add or modify file
return HttpResponseRedirect(reverse('repo_view_file', args=[repo_id]) + \
@@ -1879,7 +1879,7 @@ def toggle_modules(request):
referer = request.META.get('HTTP_REFERER', None)
next = settings.SITE_ROOT if referer is None else referer
username = request.user.username
personal_wiki = request.POST.get('personal_wiki', 'off')
if personal_wiki == 'on':

View File

@@ -9,6 +9,8 @@ from django.shortcuts import render_to_response
from django.template import RequestContext
from registration.backends import get_backend
from seahub.settings import USER_PASSWORD_MIN_LENGTH, \
USER_STRONG_PASSWORD_REQUIRED, USER_PASSWORD_STRENGTH_LEVEL
def activate(request, backend,
template_name='registration/activate.html',
@@ -204,5 +206,8 @@ def register(request, backend, success_url=None, form_class=None,
form = form_class(initial={'email': src})
return render_to_response(template_name,
{ 'form': form },
context_instance=context)
{ 'form': form,
'min_len': USER_PASSWORD_MIN_LENGTH,
'strong_pwd_required': USER_STRONG_PASSWORD_REQUIRED,
'level': USER_PASSWORD_STRENGTH_LEVEL,
}, context_instance=context)