diff --git a/seahub/api2/endpoints/admin/org_users.py b/seahub/api2/endpoints/admin/org_users.py
index e40c609b2a..b80aefd1f5 100644
--- a/seahub/api2/endpoints/admin/org_users.py
+++ b/seahub/api2/endpoints/admin/org_users.py
@@ -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
diff --git a/seahub/api2/endpoints/admin/organizations.py b/seahub/api2/endpoints/admin/organizations.py
index fc3efdb663..43cd226559 100644
--- a/seahub/api2/endpoints/admin/organizations.py
+++ b/seahub/api2/endpoints/admin/organizations.py
@@ -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):
diff --git a/seahub/api2/serializers.py b/seahub/api2/serializers.py
index 99bfee3bf4..1378fb8e94 100644
--- a/seahub/api2/serializers.py
+++ b/seahub/api2/serializers.py
@@ -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)
diff --git a/seahub/password_session/middleware.py b/seahub/password_session/middleware.py
index 10893dc2f9..37cf7b0f7b 100644
--- a/seahub/password_session/middleware.py
+++ b/seahub/password_session/middleware.py
@@ -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)
\ No newline at end of file
+ logout(request)
diff --git a/seahub/profile/forms.py b/seahub/profile/forms.py
index 7d50814f45..3a53d05fa7 100644
--- a/seahub/profile/forms.py
+++ b/seahub/profile/forms.py
@@ -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 '/'."))
diff --git a/seahub/profile/templates/profile/set_profile.html b/seahub/profile/templates/profile/set_profile.html
index ac32178ab5..34de47d804 100644
--- a/seahub/profile/templates/profile/set_profile.html
+++ b/seahub/profile/templates/profile/set_profile.html
@@ -29,7 +29,9 @@
{% trans "Two-Factor Authentication" %}
{% endif %}
+ {% if ENABLE_DELETE_ACCOUNT %}
{% trans "Delete Account" %}
+ {% endif %}
@@ -48,10 +50,13 @@
@@ -165,6 +171,7 @@
{% endif %}
+{% if ENABLE_DELETE_ACCOUNT %}
+{% endif %}
@@ -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() {
diff --git a/seahub/profile/views.py b/seahub/profile/views.py
index a8e28add54..cd1b8cced0 100644
--- a/seahub/profile/views.py
+++ b/seahub/profile/views.py
@@ -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
diff --git a/seahub/settings.py b/seahub/settings.py
index f695ef62c1..e5883b2938 100644
--- a/seahub/settings.py
+++ b/seahub/settings.py
@@ -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
diff --git a/seahub/templates/snippets/add_watermark.html b/seahub/templates/snippets/add_watermark.html
index ec38070262..e972b85efc 100644
--- a/seahub/templates/snippets/add_watermark.html
+++ b/seahub/templates/snippets/add_watermark.html
@@ -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 %}
diff --git a/seahub/two_factor/gateways/seaf_messenger.py b/seahub/two_factor/gateways/seaf_messenger.py
new file mode 100644
index 0000000000..7f380259e4
--- /dev/null
+++ b/seahub/two_factor/gateways/seaf_messenger.py
@@ -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})
diff --git a/seahub/two_factor/views/login.py b/seahub/two_factor/views/login.py
index 52c17f1289..53c83532ac 100644
--- a/seahub/two_factor/views/login.py
+++ b/seahub/two_factor/views/login.py
@@ -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)
diff --git a/seahub/urls.py b/seahub/urls.py
index 559124c83c..44277b98f5 100644
--- a/seahub/urls.py
+++ b/seahub/urls.py
@@ -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\d+)/$', AdminOrganization.as_view(), name='api-v2.1-admin-organization'),
url(r'^api/v2.1/admin/organizations/(?P\d+)/users/$', AdminOrgUsers.as_view(), name='api-v2.1-admin-org-users'),
url(r'^api/v2.1/admin/organizations/(?P\d+)/users/(?P[^/]+)/$', AdminOrgUser.as_view(), name='api-v2.1-admin-org-user'),
diff --git a/static/scripts/app/router.js b/static/scripts/app/router.js
index 2baf222570..76ad2fcdf1 100644
--- a/static/scripts/app/router.js
+++ b/static/scripts/app/router.js
@@ -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;
}
}
diff --git a/tests/api/endpoints/admin/test_org_users.py b/tests/api/endpoints/admin/test_org_users.py
index a562e689d6..a2516942ce 100644
--- a/tests/api/endpoints/admin/test_org_users.py
+++ b/tests/api/endpoints/admin/test_org_users.py
@@ -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:
diff --git a/tests/api/endpoints/admin/test_orgs.py b/tests/api/endpoints/admin/test_orgs.py
new file mode 100644
index 0000000000..8275eb3ee1
--- /dev/null
+++ b/tests/api/endpoints/admin/test_orgs.py
@@ -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)