diff --git a/seahub/api2/endpoints/admin/two_factor_auth.py b/seahub/api2/endpoints/admin/two_factor_auth.py index 6fe060df2f..5220bff6f3 100644 --- a/seahub/api2/endpoints/admin/two_factor_auth.py +++ b/seahub/api2/endpoints/admin/two_factor_auth.py @@ -9,7 +9,7 @@ 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.two_factor import devices_for_user +from seahub.two_factor.models import devices_for_user class TwoFactorAuthView(APIView): diff --git a/seahub/auth/__init__.py b/seahub/auth/__init__.py index a0ef78d497..3cbfe9bf64 100644 --- a/seahub/auth/__init__.py +++ b/seahub/auth/__init__.py @@ -1,12 +1,12 @@ # Copyright (c) 2012-2016 Seafile Ltd. import datetime +from importlib import import_module from warnings import warn + from django.conf import settings from django.core.exceptions import ImproperlyConfigured -from django.utils.importlib import import_module from seahub.auth.signals import user_logged_in -from seahub.utils import is_org_context from constance import config @@ -99,8 +99,10 @@ def logout(request): """ request.session.flush() if hasattr(request, 'user'): - from seahub.base.accounts import User + from seahub.base.accounts import User if isinstance(request.user, User): + # Do not directly/indirectly import models in package root level. + from seahub.utils import is_org_context if is_org_context(request): org_id = request.user.org.org_id request.user.remove_org_repo_passwds(org_id) diff --git a/seahub/profile/views.py b/seahub/profile/views.py index 655704482c..ff03ac5487 100644 --- a/seahub/profile/views.py +++ b/seahub/profile/views.py @@ -88,8 +88,7 @@ def edit_profile(request): } if has_two_factor_auth(): - from seahub.two_factor.models import StaticDevice - from seahub.two_factor.utils import default_device + from seahub.two_factor.models import StaticDevice, default_device try: backup_tokens = StaticDevice.objects.get( diff --git a/seahub/two_factor/__init__.py b/seahub/two_factor/__init__.py index a89ef35e32..c30088be85 100644 --- a/seahub/two_factor/__init__.py +++ b/seahub/two_factor/__init__.py @@ -1,9 +1,4 @@ # Copyright (c) 2012-2016 Seafile Ltd. -import sys - -from seahub.auth.signals import user_logged_in -from seahub.two_factor.models import (StaticDevice, TOTPDevice, - PhoneDevice) ############################## django_otp ############################## DEVICE_ID_SESSION_KEY = 'otp_device_id' @@ -29,82 +24,3 @@ def login(request, device): if (user is not None) and (device is not None) and (device.user == user.email): request.session[DEVICE_ID_SESSION_KEY] = device.persistent_id request.user.otp_device = device - - -def _handle_auth_login(sender, request, user, **kwargs): - """ - Automatically persists an OTP device that was set by an OTP-aware - AuthenticationForm. - """ - if hasattr(user, 'otp_device'): - login(request, user.otp_device) - -user_logged_in.connect(_handle_auth_login) - -def match_token(user, token): - """ - Attempts to verify a :term:`token` on every device attached to the given - user until one of them succeeds. When possible, you should prefer to verify - tokens against specific devices. - - :param user: The user supplying the token. - :type user: :class:`~django.contrib.auth.models.User` - - :param string token: An OTP token to verify. - - :returns: The device that accepted ``token``, if any. - :rtype: :class:`~django_otp.models.Device` or ``None`` - """ - matches = (d for d in devices_for_user(user) if d.verify_token(token)) - - return next(matches, None) - -def devices_for_user(user): - """ - Return an iterable of all devices registered to the given user. - - Returns an empty iterable for anonymous users. - - :param user: standard or custom user object. - :type user: :class:`~seahub.auth.models.User` - - :rtype: iterable - """ - if user.is_anonymous(): - return - - for model in TOTPDevice, PhoneDevice, StaticDevice: - device = model.objects.device_for_user(user.username) - if device: - yield device - -def user_has_device(user): - """ - Return ``True`` if the user has at least one device. - - Returns ``False`` for anonymous users. - - :param user: standard or custom user object. - :type user: :class:`~seahub.auth.models.User` - - """ - try: - next(devices_for_user(user)) - except StopIteration: - has_device = False - else: - has_device = True - - return has_device - -def import_class(path): - """ - Imports a class based on a full Python path ('pkg.pkg.mod.Class'). This - does not trap any exceptions if the path is not valid. - """ - module, name = path.rsplit('.', 1) - __import__(module) - mod = sys.modules[module] - cls = getattr(mod, name) - - return cls diff --git a/seahub/two_factor/decorators.py b/seahub/two_factor/decorators.py index 7b8353bca5..810ef6ad0c 100644 --- a/seahub/two_factor/decorators.py +++ b/seahub/two_factor/decorators.py @@ -3,7 +3,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera from seahub.auth.decorators import user_passes_test -from seahub.two_factor import user_has_device +from seahub.two_factor.models import user_has_device from seahub.two_factor.conf import settings def otp_required(view=None, redirect_field_name='next', login_url=None, if_configured=False): diff --git a/seahub/two_factor/forms.py b/seahub/two_factor/forms.py index 922e008f4c..af2478aa2e 100644 --- a/seahub/two_factor/forms.py +++ b/seahub/two_factor/forms.py @@ -6,11 +6,9 @@ from django import forms from django.forms import ModelForm, Form from django.utils.translation import ugettext_lazy as _ -from seahub.two_factor import devices_for_user, match_token from seahub.two_factor.oath import totp from seahub.two_factor.models import (Device, TOTPDevice, StaticDevice, - PhoneDevice) - + PhoneDevice, devices_for_user, match_token) from .models import get_available_methods from .utils import totp_digits diff --git a/seahub/two_factor/management/commands/two_factor_disable.py b/seahub/two_factor/management/commands/two_factor_disable.py index 329f810864..05edf1984f 100644 --- a/seahub/two_factor/management/commands/two_factor_disable.py +++ b/seahub/two_factor/management/commands/two_factor_disable.py @@ -7,7 +7,7 @@ except ImportError: else: User = get_user_model() -from seahub.two_factor import devices_for_user +from seahub.two_factor.models import devices_for_user class Command(BaseCommand): diff --git a/seahub/two_factor/management/commands/two_factor_status.py b/seahub/two_factor/management/commands/two_factor_status.py index ba88c242b9..0557ba9921 100644 --- a/seahub/two_factor/management/commands/two_factor_status.py +++ b/seahub/two_factor/management/commands/two_factor_status.py @@ -2,7 +2,7 @@ from django.core.management.base import BaseCommand, CommandError from django.contrib.auth import get_user_model -from ...utils import default_device +from seahub.two_factor.models import default_device User = get_user_model() diff --git a/seahub/two_factor/models/__init__.py b/seahub/two_factor/models/__init__.py index 9cae113b2a..ab32f407a0 100644 --- a/seahub/two_factor/models/__init__.py +++ b/seahub/two_factor/models/__init__.py @@ -3,3 +3,81 @@ from seahub.two_factor.models.base import Device, get_available_methods from seahub.two_factor.models.totp import TOTPDevice from seahub.two_factor.models.phone import PhoneDevice from seahub.two_factor.models.static import StaticDevice, StaticToken + +def devices_for_user(user): + """ + Return an iterable of all devices registered to the given user. + + Returns an empty iterable for anonymous users. + + :param user: standard or custom user object. + :type user: :class:`~seahub.auth.models.User` + + :rtype: iterable + """ + if user.is_anonymous(): + return + + for model in TOTPDevice, PhoneDevice, StaticDevice: + device = model.objects.device_for_user(user.username) + if device: + yield device + +def user_has_device(user): + """ + Return ``True`` if the user has at least one device. + + Returns ``False`` for anonymous users. + + :param user: standard or custom user object. + :type user: :class:`~seahub.auth.models.User` + + """ + try: + next(devices_for_user(user)) + except StopIteration: + has_device = False + else: + has_device = True + + return has_device + +def default_device(user): + if not user or user.is_anonymous(): + return + + for device in devices_for_user(user): + if device: + return device + +def match_token(user, token): + """ + Attempts to verify a :term:`token` on every device attached to the given + user until one of them succeeds. When possible, you should prefer to verify + tokens against specific devices. + + :param user: The user supplying the token. + :type user: :class:`~django.contrib.auth.models.User` + + :param string token: An OTP token to verify. + + :returns: The device that accepted ``token``, if any. + :rtype: :class:`~django_otp.models.Device` or ``None`` + """ + matches = (d for d in devices_for_user(user) if d.verify_token(token)) + + return next(matches, None) + +########## handle signals +from django.dispatch import receiver +from seahub.auth.signals import user_logged_in +from seahub.two_factor import login + +@receiver(user_logged_in) +def _handle_auth_login(sender, request, user, **kwargs): + """ + Automatically persists an OTP device that was set by an OTP-aware + AuthenticationForm. + """ + if hasattr(user, 'otp_device'): + login(request, user.otp_device) diff --git a/seahub/two_factor/models/base.py b/seahub/two_factor/models/base.py index d1e0bf8e38..922dd82a8c 100644 --- a/seahub/two_factor/models/base.py +++ b/seahub/two_factor/models/base.py @@ -75,7 +75,7 @@ class Device(models.Model): device == Device.from_persistent_id(device.persistent_id) """ - from seahub.two_factor import import_class + from seahub.two_factor.utils import import_class try: device_type, device_id = path.rsplit('/', 1) diff --git a/seahub/two_factor/utils.py b/seahub/two_factor/utils.py index dfd09da1c5..230986ae77 100644 --- a/seahub/two_factor/utils.py +++ b/seahub/two_factor/utils.py @@ -1,6 +1,7 @@ # Copyright (c) 2012-2016 Seafile Ltd. from __future__ import absolute_import, division, print_function, unicode_literals +import sys from binascii import hexlify, unhexlify from os import urandom @@ -13,17 +14,6 @@ from django.conf import settings from django.core.exceptions import ValidationError from django.utils import six - -def default_device(user): - if not user or user.is_anonymous(): - return - - # to avoid circular import - from seahub.two_factor import devices_for_user - for device in devices_for_user(user): - if device: - return device - def get_otpauth_url(accountname, secret, issuer=None, digits=None): # For a complete run-through of all the parameters, have a look at the # specs at: @@ -116,3 +106,18 @@ def random_hex(length=20): :rtype: str """ return hexlify(urandom(length)) + +def import_class(path): + """ + Imports a class based on a full Python path ('pkg.pkg.mod.Class'). This + does not trap any exceptions if the path is not valid. + """ + module, name = path.rsplit('.', 1) + __import__(module) + mod = sys.modules[module] + cls = getattr(mod, name) + + return cls + +# # Move here to avoid circular import +# from seahub.two_factor.models import (StaticDevice, TOTPDevice, PhoneDevice) diff --git a/seahub/two_factor/views/core.py b/seahub/two_factor/views/core.py index 7c3953d238..6a488eca0f 100644 --- a/seahub/two_factor/views/core.py +++ b/seahub/two_factor/views/core.py @@ -32,13 +32,12 @@ from seahub.auth.forms import AuthenticationForm from seahub.two_factor import login as two_factor_login from seahub.two_factor.decorators import otp_required -from seahub.two_factor.models import (StaticToken, StaticDevice, - PhoneDevice, get_available_methods) -from seahub.two_factor.utils import random_hex, totp_digits +from seahub.two_factor.models import (StaticDevice, PhoneDevice, + get_available_methods, default_device) +from seahub.two_factor.utils import random_hex, totp_digits, get_otpauth_url from seahub.two_factor.forms import (MethodForm, TOTPDeviceForm, PhoneNumberForm, DeviceValidationForm) -from seahub.two_factor.utils import get_otpauth_url, default_device from seahub.two_factor.views.utils import (class_view_decorator, CheckTwoFactorEnabledMixin, IdempotentSessionWizardView) diff --git a/seahub/two_factor/views/login.py b/seahub/two_factor/views/login.py index 9b379d134c..52c17f1289 100644 --- a/seahub/two_factor/views/login.py +++ b/seahub/two_factor/views/login.py @@ -28,11 +28,10 @@ from seahub.utils.ip import get_remote_ip from seahub.profile.models import Profile from seahub.two_factor import login as two_factor_login -from seahub.two_factor import user_has_device -from seahub.two_factor.models import StaticDevice, TOTPDevice, PhoneDevice +from seahub.two_factor.models import (StaticDevice, TOTPDevice, default_device, + user_has_device) from seahub.two_factor.forms import TOTPTokenAuthForm, BackupTokenAuthForm, AuthenticationTokenForm -from seahub.two_factor.utils import default_device from seahub.two_factor.views.utils import class_view_decorator from seahub.utils.auth import get_login_bg_image_path diff --git a/seahub/two_factor/views/mixins.py b/seahub/two_factor/views/mixins.py index c7472bd372..6c80390e69 100644 --- a/seahub/two_factor/views/mixins.py +++ b/seahub/two_factor/views/mixins.py @@ -10,7 +10,7 @@ from seahub.auth import REDIRECT_FIELD_NAME from django.core.exceptions import PermissionDenied from django.shortcuts import redirect -from ..utils import default_device +from seahub.two_factor.models import default_device class OTPRequiredMixin(object): diff --git a/seahub/two_factor/views/profile.py b/seahub/two_factor/views/profile.py index de05bc4dfe..f2b26a7c75 100644 --- a/seahub/two_factor/views/profile.py +++ b/seahub/two_factor/views/profile.py @@ -8,10 +8,9 @@ from django.views.generic import TemplateView, FormView from seahub.auth.decorators import login_required -from seahub.two_factor import user_has_device, devices_for_user from seahub.two_factor.forms import DisableForm -from seahub.two_factor.models import StaticDevice -from seahub.two_factor.utils import default_device +from seahub.two_factor.models import (StaticDevice, devices_for_user, + user_has_device, default_device) from seahub.two_factor.views.utils import class_view_decorator, CheckTwoFactorEnabledMixin diff --git a/seahub/views/sysadmin.py b/seahub/views/sysadmin.py index e152fcce8d..1ebf35592b 100644 --- a/seahub/views/sysadmin.py +++ b/seahub/views/sysadmin.py @@ -44,7 +44,7 @@ from seahub.invitations.models import Invitation from seahub.role_permissions.utils import get_available_roles, \ get_available_admin_roles from seahub.role_permissions.models import AdminRole -from seahub.two_factor.utils import default_device +from seahub.two_factor.models import default_device from seahub.utils import IS_EMAIL_CONFIGURED, string2list, is_valid_username, \ is_pro_version, send_html_email, get_user_traffic_list, get_server_id, \ handle_virus_record, get_virus_record_by_id, \ diff --git a/tests/api/endpoints/admin/test_two_factor_auth.py b/tests/api/endpoints/admin/test_two_factor_auth.py index d34a170673..be159b32c0 100644 --- a/tests/api/endpoints/admin/test_two_factor_auth.py +++ b/tests/api/endpoints/admin/test_two_factor_auth.py @@ -3,6 +3,7 @@ import pytest from django.core.urlresolvers import reverse from seahub.test_utils import BaseTestCase +from seahub.two_factor.models import TOTPDevice, devices_for_user TRAVIS = 'TRAVIS' in os.environ @@ -13,12 +14,9 @@ class TwoFactorAuthViewTest(BaseTestCase): self.login_as(self.admin) def test_can_disable_two_factor_auth(self): - from seahub.two_factor.models import (StaticDevice, TOTPDevice, - PhoneDevice) totp = TOTPDevice(user=self.admin, name="", confirmed=1) totp.save() - from seahub.two_factor import devices_for_user devices = devices_for_user(self.admin) i = 0 for device in devices_for_user(self.admin): diff --git a/tests/seahub/two_factor/views/test_backup_tokens_view.py b/tests/seahub/two_factor/views/test_backup_tokens_view.py index 831608cb68..dd599bb212 100644 --- a/tests/seahub/two_factor/views/test_backup_tokens_view.py +++ b/tests/seahub/two_factor/views/test_backup_tokens_view.py @@ -1,8 +1,7 @@ from django.core.urlresolvers import reverse from constance import config -from seahub.two_factor.models import StaticDevice -from seahub.two_factor import user_has_device +from seahub.two_factor.models import StaticDevice, user_has_device from seahub.test_utils import BaseTestCase