diff --git a/seahub/api2/endpoints/admin/two_factor_auth.py b/seahub/api2/endpoints/admin/two_factor_auth.py new file mode 100644 index 0000000000..a04882a0f0 --- /dev/null +++ b/seahub/api2/endpoints/admin/two_factor_auth.py @@ -0,0 +1,34 @@ +# Copyright (c) 2012-2016 Seafile Ltd. +from rest_framework import status +from rest_framework.permissions import IsAdminUser +from rest_framework.authentication import SessionAuthentication +from rest_framework.response import Response + +from seahub.base.accounts import User +from seahub.api2.base import APIView +from seahub.api2.throttling import UserRateThrottle +from seahub.api2.utils import json_response, api_error +from seahub.api2.authentication import TokenAuthentication +from seahub.utils.two_factor_auth import has_two_factor_auth, two_factor_auth_enabled +from seahub_extra.two_factor import devices_for_user + + +class TwoFactorAuthView(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + throttle_classes = (UserRateThrottle,) + permission_classes = (IsAdminUser,) + + def delete(self, request, email): + if not email: + error_msg = "email can not be empty" + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + try: + _user = User.objects.get(email=email) + except User.DoesNotExist: + error_msg = "User %s not found" % email + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + devices = devices_for_user(_user) + if devices: + for device in devices: + device.delete() + return Response({'success':True}, status=status.HTTP_200_OK) diff --git a/seahub/api2/urls.py b/seahub/api2/urls.py index ddad0c9475..a2e39e262b 100644 --- a/seahub/api2/urls.py +++ b/seahub/api2/urls.py @@ -4,6 +4,7 @@ from django.conf.urls import patterns, url, include from .views import * from .views_misc import ServerInfoView from .views_auth import LogoutDeviceView, ClientLoginTokenView +from .endpoints.admin.two_factor_auth import TwoFactorAuthView from .endpoints.dir_shared_items import DirSharedItemsEndpoint from .endpoints.account import Account from .endpoints.shared_upload_links import SharedUploadLinksView @@ -24,6 +25,7 @@ urlpatterns = patterns('', url(r'^server-info/$', ServerInfoView.as_view()), url(r'^logout-device/$', LogoutDeviceView.as_view()), url(r'^client-login/$', ClientLoginTokenView.as_view()), + url(r'^two-factor-auth/(?P\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+)/$', TwoFactorAuthView.as_view(), name="two-factor-auth-view"), url(r'^device-wiped/$', RemoteWipeReportView.as_view()), url(r'^wopi/', include('seahub.wopi.urls')), diff --git a/seahub/templates/sysadmin/userinfo.html b/seahub/templates/sysadmin/userinfo.html index 997e3e3826..bd4afe09bc 100644 --- a/seahub/templates/sysadmin/userinfo.html +++ b/seahub/templates/sysadmin/userinfo.html @@ -94,6 +94,17 @@ + +
{% trans "Two-Factor Authentication" %}
+
+ {% if two_factor_auth_enabled and default_device %} + + {% else %} + + {% endif %} +

+
+
{% csrf_token %} @@ -331,6 +342,28 @@ $('#set-quota').click(function() { $("#set-quota-form").modal({appendTo: "#main"}); $('#simplemodal-container').css({'width':'auto', 'height':'auto'}); }); +$("#disable_auth").click(function(){ + var email = "{{ email }}"; + var $error = $('.error', $("#factor")); + $.ajax({ + url: '{% url 'two-factor-auth-view' email %}', + type: 'DELETE', + cache: false, + beforeSend: prepareCSRFToken, + success:function(data){ + location.reload(true); + }, + error: function(xhr, textStatus, errorThrown) { + var err_msg; + if (xhr.responseText) { + err_msg = $.parseJSON(xhr.responseText).error_msg; + } else { + err_msg = "{% trans "Failed. Please check the network." %}"; + } + $error.html(err_msg).show(); + } + }) +}) $('#set-name-form').submit(function() { var nickname = $.trim($('[name="nickname"]', $(this)).val()); diff --git a/seahub/views/sysadmin.py b/seahub/views/sysadmin.py index 3677e43bcd..2468b067d5 100644 --- a/seahub/views/sysadmin.py +++ b/seahub/views/sysadmin.py @@ -76,7 +76,7 @@ try: from seahub.settings import MULTI_TENANCY except ImportError: MULTI_TENANCY = False -from seahub.utils.two_factor_auth import HAS_TWO_FACTOR_AUTH +from seahub.utils.two_factor_auth import has_two_factor_auth, HAS_TWO_FACTOR_AUTH from termsandconditions.models import TermsAndConditions logger = logging.getLogger(__name__) @@ -618,6 +618,13 @@ def user_info(request, email): else: g.role = _('Member') + _default_device = False + _has_two_factor_auth = has_two_factor_auth() + if _has_two_factor_auth: + from seahub_extra.two_factor.utils import default_device + _user = User.objects.get(email=email) + _default_device = default_device(_user) + return render_to_response( 'sysadmin/userinfo.html', { 'owned_repos': owned_repos, @@ -631,6 +638,8 @@ def user_info(request, email): 'user_shared_links': user_shared_links, 'enable_sys_admin_view_repo': ENABLE_SYS_ADMIN_VIEW_REPO, 'personal_groups': personal_groups, + 'two_factor_auth_enabled': _has_two_factor_auth, + 'default_device': _default_device, }, context_instance=RequestContext(request)) @login_required_ajax diff --git a/tests/api/endpoints/admin/test_two_factor_auth.py b/tests/api/endpoints/admin/test_two_factor_auth.py new file mode 100644 index 0000000000..3a28eb8e90 --- /dev/null +++ b/tests/api/endpoints/admin/test_two_factor_auth.py @@ -0,0 +1,37 @@ + +from django.core.urlresolvers import reverse + +from seahub.test_utils import BaseTestCase +from seahub_extra.two_factor import devices_for_user +from seahub_extra.two_factor.models import (StaticDevice, TOTPDevice, + PhoneDevice) + + +class TwoFactorAuthViewTest(BaseTestCase): + def setUp(self): + self.login_as(self.admin) + + def test_can_disable_two_factor_auth(self): + totp = TOTPDevice(user=self.admin, name="", confirmed=1) + totp.save() + + devices = devices_for_user(self.admin) + i = 0 + for device in devices_for_user(self.admin): + if device: + i+=1 + assert i > 0 + resp = self.client.delete(reverse('two-factor-auth-view', args=[str(self.admin.username)])) + assert resp.status_code == 200 + i = 0 + for device in devices_for_user(self.admin): + if device: + i+=1 + assert i == 0 + + def tearDown(self): + try: + for device in devices_for_user(self.admin): + device.delete() + except: + pass