1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-13 05:39:59 +00:00

Merge branch '6.3'

This commit is contained in:
zhengxie
2019-01-02 12:53:36 +08:00
15 changed files with 286 additions and 27 deletions

View File

@@ -34,18 +34,16 @@ logger = logging.getLogger(__name__)
def get_org_user_info(org_id, email):
user_info = {}
user_obj = User.objects.get(email=email)
user_info['org_id'] = org_id
user_info['active'] = user_obj.is_active
user_info['email'] = email
user_info['name'] = email2nickname(email)
user_info['contact_email'] = email2contact_email(email)
org_user_quota = seafile_api.get_org_user_quota(org_id, email)
user_info['quota_total'] = org_user_quota / get_file_size_unit('MB')
user_info['quota_total'] = org_user_quota
org_user_quota_usage = seafile_api.get_org_user_quota_usage(org_id, email)
user_info['quota_usage'] = org_user_quota_usage / get_file_size_unit('MB')
user_info['quota_usage'] = org_user_quota_usage
return user_info
@@ -94,6 +92,32 @@ class AdminOrgUsers(APIView):
throttle_classes = (UserRateThrottle,)
permission_classes = (IsAdminUser, IsProVersion)
def get(self, request, org_id):
""" Get all users in an org.
Permission checking:
1. only admin can perform this action.
"""
# argument check
org_id = int(org_id)
if org_id == 0:
error_msg = 'org_id invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
org = ccnet_api.get_org_by_id(org_id)
if not org:
error_msg = 'Organization %d not found.' % org_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
result = []
org_users = ccnet_api.get_org_emailusers(org.url_prefix, -1, -1)
for org_user in org_users:
user_info = get_org_user_info(org_id, org_user.email)
user_info['active'] = org_user.is_active
result.append(user_info)
return Response({'users': result})
def post(self, request, org_id):
""" Add new user to org.
@@ -121,6 +145,14 @@ class AdminOrgUsers(APIView):
error_msg = 'password invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
active = request.POST.get('active', 'true')
active = active.lower()
if active not in ('true', 'false'):
error_msg = 'active invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
is_active = active == 'true'
try:
User.objects.get(email=email)
user_exists = True
@@ -147,7 +179,8 @@ class AdminOrgUsers(APIView):
# create user
try:
User.objects.create_user(email, password, is_staff=False, is_active=True)
User.objects.create_user(email, password, is_staff=False,
is_active=is_active)
except User.DoesNotExist as e:
logger.error(e)
error_msg = 'Fail to add user %s.' % email
@@ -170,6 +203,7 @@ class AdminOrgUsers(APIView):
UserOptions.objects.set_force_passwd_change(email)
user_info = get_org_user_info(org_id, email)
user_info['active'] = is_active
return Response(user_info)
@@ -188,7 +222,24 @@ class AdminOrgUser(APIView):
"""
# argument check
org_id = int(org_id)
if org_id == 0:
error_msg = 'org_id invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
org = ccnet_api.get_org_by_id(org_id)
if not org:
error_msg = 'Organization %d not found.' % org_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
try:
user_obj = User.objects.get(email=email)
except User.DoesNotExist:
error_msg = 'User %s not found.' % email
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
user_info = get_org_user_info(org_id, email)
user_info['active'] = user_obj.is_active
return Response(user_info)
@check_org_user
@@ -199,6 +250,12 @@ class AdminOrgUser(APIView):
1. only admin can perform this action.
"""
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
error_msg = 'User %s not found.' % email
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# update active
active = request.data.get('active', None)
if active:
@@ -207,7 +264,6 @@ class AdminOrgUser(APIView):
error_msg = "active invalid, should be 'true' or 'false'."
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
user = User.objects.get(email=email)
if active == 'true':
user.is_active = True
else:
@@ -267,6 +323,7 @@ class AdminOrgUser(APIView):
seafile_api.set_org_user_quota(org_id, email, user_quota)
user_info = get_org_user_info(org_id, email)
user_info['active'] = user.is_active
return Response(user_info)
@check_org_user

View File

@@ -9,8 +9,6 @@ from rest_framework import status
from seaserv import ccnet_api, seafile_api
from seaserv import seafserv_threaded_rpc
from seahub.utils.file_size import get_file_size_unit
from seahub.utils.timeutils import timestamp_to_isoformat_timestr
from seahub.base.templatetags.seahub_tags import email2nickname, \
@@ -40,12 +38,11 @@ except ImportError:
logger = logging.getLogger(__name__)
def get_org_info(org_id):
def get_org_info(org):
org_id = org.org_id
org_info = {}
org = ccnet_api.get_org_by_id(org_id)
org_info['org_id'] = org_id
org_info['org_name'] = org.org_name
org_info['ctime'] = timestamp_to_isoformat_timestr(org.ctime)
@@ -57,12 +54,44 @@ def get_org_info(org_id):
org_info['creator_contact_email'] = email2contact_email(creator)
org_info['quota'] = seafile_api.get_org_quota(org_id)
org_info['quota_usage'] = seafile_api.get_org_quota_usage(org_id)
if ORG_MEMBER_QUOTA_ENABLED:
org_info['max_user_number'] = OrgMemberQuota.objects.get_quota(org_id)
return org_info
class AdminOrganizations(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAdminUser, IsProVersion)
throttle_classes = (UserRateThrottle,)
def get(self, request):
""" Get all organizations
Permission checking:
1. only admin can perform this action.
"""
if not (CLOUD_MODE and MULTI_TENANCY):
error_msg = 'Feature is not enabled.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
try:
orgs = ccnet_api.get_all_orgs(-1, -1)
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
result = []
for org in orgs:
org_info = get_org_info(org)
result.append(org_info)
return Response({'organizations': result})
class AdminOrganization(APIView):
@@ -92,7 +121,7 @@ class AdminOrganization(APIView):
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
try:
org_info = get_org_info(org_id)
org_info = get_org_info(org)
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
@@ -174,7 +203,7 @@ class AdminOrganization(APIView):
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
org_info = get_org_info(org_id)
org_info = get_org_info(org)
return Response(org_info)
def delete(self, request, org_id):

View File

@@ -8,6 +8,7 @@ from seahub.auth import authenticate
from seahub.api2.models import DESKTOP_PLATFORMS
from seahub.api2.utils import get_token_v1, get_token_v2
from seahub.profile.models import Profile
from seahub.two_factor.models import default_device
from seahub.two_factor.views.login import is_device_remembered
from seahub.utils.two_factor_auth import has_two_factor_auth, \
two_factor_auth_enabled, verify_two_factor_token
@@ -118,10 +119,13 @@ class AuthTokenSerializer(serializers.Serializer):
token = request.META.get('HTTP_X_SEAFILE_OTP', '')
if not token:
# Generate challenge(send sms/call/...) if token is not provided.
default_device(user).generate_challenge()
self.two_factor_auth_failed = True
msg = 'Two factor auth token is missing.'
raise serializers.ValidationError(msg)
if not verify_two_factor_token(user.username, token):
if not verify_two_factor_token(user, token):
self.two_factor_auth_failed = True
msg = 'Two factor auth token is invalid.'
raise serializers.ValidationError(msg)

View File

@@ -8,5 +8,9 @@ class CheckPasswordHash(object):
"""Logout user if value of hash key in session is not equal to current password hash"""
def process_view(self, request, *args, **kwargs):
if getattr(request.user, 'is_authenticated') and request.user.is_authenticated():
if request.user.enc_password == '!':
# Disable for LDAP/Shibboleth/SAML/... users.
return None
if request.session.get(PASSWORD_HASH_KEY) != get_password_hash(request.user):
logout(request)

View File

@@ -5,6 +5,8 @@ from django.utils.translation import ugettext_lazy as _
from seahub.profile.models import Profile, DetailedProfile
from seahub.settings import ENABLE_UPDATE_USER_INFO
class ProfileForm(forms.Form):
nickname = forms.CharField(max_length=64, required=False)
intro = forms.CharField(max_length=256, required=False)
@@ -13,6 +15,9 @@ class ProfileForm(forms.Form):
"""
Validates that nickname should not include '/'
"""
if not ENABLE_UPDATE_USER_INFO:
raise forms.ValidationError(_(u"Permission denied."))
if "/" in self.cleaned_data["nickname"]:
raise forms.ValidationError(_(u"Name should not include '/'."))

View File

@@ -29,7 +29,9 @@
<li class="tab"><a href="#two-factor-auth">{% trans "Two-Factor Authentication" %}</a></li>
{% endif %}
{% if ENABLE_DELETE_ACCOUNT %}
<li class="tab" id="del-account-nav"><a href="#del-account">{% trans "Delete Account" %}</a></li>
{% endif %}
</ul>
</div>
@@ -48,10 +50,13 @@
</div>
<form action="" method="post">{% csrf_token %}
<label>{% trans "Name:" context "true name" %}</label><input type="text" name="nickname" value="{{ form.data.nickname }}" class="input" />
<label>{% trans "Name:" context "true name" %}</label><input type="text" name="nickname" value="{{ form.data.nickname }}" class="input" {% if not ENABLE_UPDATE_USER_INFO %} disabled {% endif %}/>
{% if ENABLE_UPDATE_USER_INFO %}
{% for error in form.nickname.errors %}
<span class="error">{{ error|escape }}</span>
{% endfor %}
{% endif %}
<br/>
{% if form.data.login_id %}
@@ -67,14 +72,15 @@
{% endif %}
{% if form.telephone %}
<label>{% trans "Telephone:" %}</label><input type="text" name="telephone" value="{{ form.data.telephone }}" class="input" />
<label>{% trans "Telephone:" %}</label><input type="text" name="telephone" value="{{ form.data.telephone }}" class="input" {% if not ENABLE_UPDATE_USER_INFO %} disabled {% endif %} />
{% for error in form.telephone.errors %}
<span class="error">{{ error|escape }}</span>
{% endfor %}
<br/>
{% endif %}
{% if ENABLE_UPDATE_USER_INFO %}
<input type="submit" value="{% trans "Submit" %}" class="submit vh" />
{% endif %}
</form>
</div>
@@ -165,6 +171,7 @@
</div>
{% endif %}
{% if ENABLE_DELETE_ACCOUNT %}
<div class="setting-item" id="del-account">
<h3>{% trans "Delete Account" %}</h3>
<p class="txt-before-btn">{% trans "This operation will not be reverted. Please think twice!" %}</p>
@@ -173,6 +180,7 @@
<input type="submit" value="{% trans "Delete" %}" class="submit" />
</form>
</div>
{% endif %}
</div> <!-- right-panel -->
</div> <!-- row -->
@@ -228,6 +236,7 @@ $('#user-avatar-input').on('change', function() {
$('#user-basic-info .submit').css({'margin-left': $('#user-basic-info label').outerWidth(true)}).removeClass('vh');
{% if ENABLE_DELETE_ACCOUNT %}
$('#account-delete-btn').on('click', function () {
var title = "{% trans "Delete Account" %}",
con = "{% trans "Really want to delete your account?" %}";
@@ -240,6 +249,7 @@ $('#account-delete-btn').on('click', function () {
$('#account-delete-form').trigger('submit');
});
});
{% endif %}
{% if ENABLE_ADDRESSBOOK_OPT_IN %}
$("#list-in-address-book input[type='checkbox']").on('change', function() {

View File

@@ -23,6 +23,8 @@ from seahub.utils import is_ldap_user
from seahub.utils.two_factor_auth import has_two_factor_auth
from seahub.views import get_owned_repo_list
from seahub.settings import ENABLE_DELETE_ACCOUNT, ENABLE_UPDATE_USER_INFO
@login_required
def edit_profile(request):
"""
@@ -96,6 +98,8 @@ def edit_profile(request):
'two_factor_auth_enabled': has_two_factor_auth(),
'ENABLE_CHANGE_PASSWORD': settings.ENABLE_CHANGE_PASSWORD,
'ENABLE_WEBDAV_SECRET': settings.ENABLE_WEBDAV_SECRET,
'ENABLE_DELETE_ACCOUNT': ENABLE_DELETE_ACCOUNT,
'ENABLE_UPDATE_USER_INFO': ENABLE_UPDATE_USER_INFO,
'webdav_passwd': webdav_passwd,
'email_notification_interval': email_inverval,
}
@@ -177,6 +181,11 @@ def get_user_profile(request, user):
@login_required
def delete_user_account(request):
if not ENABLE_DELETE_ACCOUNT:
messages.error(request, _(u'Permission denied.'))
next = request.META.get('HTTP_REFERER', settings.SITE_ROOT)
return HttpResponseRedirect(next)
if request.method != 'POST':
raise Http404

View File

@@ -365,6 +365,9 @@ FORCE_PASSWORD_CHANGE = True
# Enable a user to change password in 'settings' page.
ENABLE_CHANGE_PASSWORD = True
ENABLE_DELETE_ACCOUNT = True
ENABLE_UPDATE_USER_INFO = True
# Enable or disable repo history setting
ENABLE_REPO_HISTORY_SETTING = True

View File

@@ -3,6 +3,9 @@
watermark.load({
{% if request.user.username %}
watermark_txt: "{{site_name}} {{request.user.username|email2nickname|escapejs}}",
{# used when view shared file #}
{% elif shared_by %}
watermark_txt: "{{site_name}} {{shared_by|email2nickname|escapejs}}",
{% else %}
watermark_txt: "{% trans "Anonymous User" %}",
{% endif %}

View File

@@ -0,0 +1,26 @@
# Copyright (c) 2012-2016 Seafile Ltd.
import logging
from django.conf import settings
import requests
logger = logging.getLogger(__name__)
class SeafMessenger(object):
@staticmethod
def make_call(device, token):
logger.info('Fake call to %s: "Your token is: %s"', device.number, token)
@staticmethod
def send_sms(device, token):
api_token = settings.SEAF_MESSAGER_API_TOKEN
url = settings.SEAF_MESSAGER_SMS_API
values = {
'phone_num': device.number,
'code': token,
}
requests.post(url, data=values,
headers={'Authorization': 'Token %s' % api_token})

View File

@@ -215,14 +215,13 @@ def handle_two_factor_auth(request, user, redirect_to):
request.session[SESSION_KEY_TWO_FACTOR_FAILED_ATTEMPT] = 0
return redirect(reverse('two_factor_auth'))
def verify_two_factor_token(username, token):
def verify_two_factor_token(user, token):
"""
This function is called when doing the api authentication. We only support
totp here to simply the case. Backup token is not supported, because if the
user has the backup token, he can always login the website and re-setup the
totp.
This function is called when doing the api authentication.
Backup token is not supported, because if the user has the backup token,
he can always login the website and re-setup the totp.
"""
device = TOTPDevice.objects.device_for_user(username)
device = default_device(user)
if device:
return device.verify_token(token)

View File

@@ -113,7 +113,7 @@ from seahub.api2.endpoints.admin.upload_links import AdminUploadLink, \
AdminUploadLinkUpload, AdminUploadLinkCheckPassword
from seahub.api2.endpoints.admin.users_batch import AdminUsersBatch
from seahub.api2.endpoints.admin.operation_logs import AdminOperationLogs
from seahub.api2.endpoints.admin.organizations import AdminOrganization
from seahub.api2.endpoints.admin.organizations import AdminOrganizations, AdminOrganization
from seahub.api2.endpoints.admin.org_users import AdminOrgUsers, AdminOrgUser
from seahub.api2.endpoints.admin.org_stats import AdminOrgStatsTraffic
from seahub.api2.endpoints.admin.logo import AdminLogo
@@ -457,6 +457,7 @@ urlpatterns = [
url(r'^api/v2.1/admin/admin-role/$', AdminAdminRole.as_view(), name='api-v2.1-admin-admin-role'),
## admin::organizations
url(r'^api/v2.1/admin/organizations/$', AdminOrganizations.as_view(), name='api-v2.1-admin-organizations'),
url(r'^api/v2.1/admin/organizations/(?P<org_id>\d+)/$', AdminOrganization.as_view(), name='api-v2.1-admin-organization'),
url(r'^api/v2.1/admin/organizations/(?P<org_id>\d+)/users/$', AdminOrgUsers.as_view(), name='api-v2.1-admin-org-users'),
url(r'^api/v2.1/admin/organizations/(?P<org_id>\d+)/users/(?P<email>[^/]+)/$', AdminOrgUser.as_view(), name='api-v2.1-admin-org-user'),

View File

@@ -133,7 +133,7 @@ define([
if (this.currentView == this.dirView) {
if ($('#upload-file-dialog').is(':visible') &&
$('#upload-file-dialog .status').text() == window.fileuploading) {
if (!window.confirm('A file is being uploaded. Are you sure you want to leave this page?')) {
if (!window.confirm(gettext('A file is being uploaded. Are you sure you want to leave this page?'))) {
return false;
}
}

View File

@@ -57,6 +57,30 @@ class OrgUsersTest(BaseTestCase):
remove_org(self.org_id)
self.remove_user(self.org_creator)
def test_can_get_users(self):
if not LOCAL_PRO_DEV_ENV:
return
self.login_as(self.admin)
resp = self.client.get(self.org_users_url)
json_resp = json.loads(resp.content)
self.assertEqual(200, resp.status_code)
users = json_resp['organizaton_members']
assert len(users) > 0
assert users[0]['org_id'] == self.org_id
assert users[0]['email'] == self.org_creator
def test_can_not_get_users_if_not_admin(self):
if not LOCAL_PRO_DEV_ENV:
return
self.login_as(self.user)
resp = self.client.get(self.org_users_url)
self.assertEqual(403, resp.status_code)
def test_can_create(self):
if not LOCAL_PRO_DEV_ENV:

View File

@@ -0,0 +1,85 @@
import json
from seaserv import ccnet_api
from django.core.urlresolvers import reverse
from seahub.test_utils import BaseTestCase
from tests.common.utils import randstring
from seaserv import seafserv_threaded_rpc
try:
from seahub.settings import LOCAL_PRO_DEV_ENV
except ImportError:
LOCAL_PRO_DEV_ENV = False
def remove_org(org_id):
org_id = int(org_id)
org = ccnet_api.get_org_by_id(org_id)
if org:
users =ccnet_api.get_org_emailusers(org.url_prefix, -1, -1)
for u in users:
ccnet_api.remove_org_user(org_id, u.email)
groups = ccnet_api.get_org_groups(org.org_id, -1, -1)
for g in groups:
ccnet_api.remove_org_group(org_id, g.gid)
# remove org repos
seafserv_threaded_rpc.remove_org_repo_by_org_id(org_id)
# remove org
ccnet_api.remove_org(org_id)
class OrgsTest(BaseTestCase):
def setUp(self):
self.user_name = self.user.username
self.admin_name = self.admin.username
if LOCAL_PRO_DEV_ENV:
self.org_name = randstring(6)
self.org_url_prefix = randstring(6)
tmp_user = self.create_user(email='%s@%s.com' % (randstring(6), randstring(6)))
self.org_creator = tmp_user.username
self.org_id = ccnet_api.create_org(self.org_name,
self.org_url_prefix, self.org_creator)
self.orgs_url = reverse('api-v2.1-admin-organizations')
def tearDown(self):
self.remove_group()
self.remove_repo()
if LOCAL_PRO_DEV_ENV:
remove_org(self.org_id)
self.remove_user(self.org_creator)
def test_can_get_orgs(self):
if not LOCAL_PRO_DEV_ENV:
return
self.login_as(self.admin)
resp = self.client.get(self.orgs_url)
json_resp = json.loads(resp.content)
self.assertEqual(200, resp.status_code)
users = json_resp['organizations']
assert len(users) > 0
assert users[0].has_key('org_id')
assert users[0].has_key('org_name')
assert users[0].has_key('ctime')
assert users[0].has_key('org_url_prefix')
assert users[0].has_key('creator_email')
assert users[0].has_key('creator_name')
assert users[0].has_key('creator_contact_email')
assert users[0].has_key('quota')
def test_can_not_get_orgs_if_not_admin(self):
if not LOCAL_PRO_DEV_ENV:
return
self.login_as(self.user)
resp = self.client.get(self.orgs_url)
self.assertEqual(403, resp.status_code)