diff --git a/seahub/api2/authentication.py b/seahub/api2/authentication.py index 496cc7f204..28997f7f57 100644 --- a/seahub/api2/authentication.py +++ b/seahub/api2/authentication.py @@ -9,6 +9,7 @@ from seahub.base.accounts import User from seahub.constants import GUEST_USER from seahub.api2.models import Token, TokenV2 from seahub.api2.utils import get_client_ip +from seahub.utils import within_time_range try: from seahub.settings import MULTI_TENANCY except ImportError: @@ -16,13 +17,6 @@ except ImportError: logger = logging.getLogger(__name__) -def within_ten_min(d1, d2): - '''Return true if two datetime.datetime object differs less than ten minutes''' - delta = d2 - d1 if d2 > d1 else d1 - d2 - interval = 60 * 10 - # delta.total_seconds() is only available in python 2.7+ - seconds = (delta.microseconds + (delta.seconds + delta.days*24*3600) * 1e6) / 1e6 - return seconds < interval HEADER_CLIENT_VERSION = 'HTTP_SEAFILE_CLEINT_VERSION' HEADER_PLATFORM_VERSION = 'HTTP_SEAFILE_PLATFORM_VERSION' @@ -135,7 +129,7 @@ class TokenAuthentication(BaseAuthentication): token.platform_version = platform_version need_save = True - if not within_ten_min(token.last_accessed, datetime.datetime.now()): + if not within_time_range(token.last_accessed, datetime.datetime.now(), 10 * 60): # We only need 10min precision for the last_accessed field need_save = True diff --git a/seahub/api2/urls.py b/seahub/api2/urls.py index 1679a61815..44241cf2b6 100644 --- a/seahub/api2/urls.py +++ b/seahub/api2/urls.py @@ -2,7 +2,7 @@ from django.conf.urls.defaults import * from .views import * from .views_misc import ServerInfoView -from .views_auth import LogoutDeviceView +from .views_auth import LogoutDeviceView, ClientLoginTokenView urlpatterns = patterns('', @@ -11,6 +11,7 @@ urlpatterns = patterns('', url(r'^auth-token/', ObtainAuthToken.as_view()), url(r'^server-info/$', ServerInfoView.as_view()), url(r'^logout-device/$', LogoutDeviceView.as_view()), + url(r'^client-login/$', ClientLoginTokenView.as_view()), # RESTful API url(r'^accounts/$', Accounts.as_view(), name="accounts"), diff --git a/seahub/api2/views_auth.py b/seahub/api2/views_auth.py index 123c08562c..069e1e8fea 100644 --- a/seahub/api2/views_auth.py +++ b/seahub/api2/views_auth.py @@ -8,6 +8,8 @@ from seahub import settings from seahub.api2.utils import json_response, api_error from seahub.api2.authentication import TokenAuthentication from seahub.api2.models import Token, TokenV2 +from seahub.base.models import ClientLoginToken +from seahub.utils import gen_token class LogoutDeviceView(APIView): """Removes the api token of a device that has already logged in. If the device @@ -17,6 +19,7 @@ class LogoutDeviceView(APIView): authentication_classes = (TokenAuthentication,) permission_classes = (IsAuthenticated,) throttle_classes = (UserRateThrottle,) + @json_response def post(self, request, format=None): auth_token = request.auth @@ -24,3 +27,19 @@ class LogoutDeviceView(APIView): seafile_api.delete_repo_tokens_by_peer_id(request.user.username, auth_token.device_id) auth_token.delete() return {} + +class ClientLoginTokenView(APIView): + """Removes the api token of a device that has already logged in. If the device + is a desktop client, also remove all sync tokens of repos synced on that + client . + """ + authentication_classes = (TokenAuthentication,) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle,) + + @json_response + def post(self, request, format=None): + randstr = gen_token(max_length=32) + token = ClientLoginToken(randstr, request.user.username) + token.save() + return {'token': randstr} diff --git a/seahub/base/context_processors.py b/seahub/base/context_processors.py index 671651ca67..c7c6f2c856 100644 --- a/seahub/base/context_processors.py +++ b/seahub/base/context_processors.py @@ -82,4 +82,5 @@ def base(request): 'grps': grps, 'multi_tenancy': MULTI_TENANCY, 'search_repo_id': search_repo_id, + 'debug': True, } diff --git a/seahub/base/models.py b/seahub/base/models.py index afe1e5ac7c..05103e7f5c 100644 --- a/seahub/base/models.py +++ b/seahub/base/models.py @@ -9,20 +9,20 @@ from seaserv import seafile_api from seahub.auth.signals import user_logged_in from seahub.group.models import GroupMessage -from seahub.utils import calc_file_path_hash +from seahub.utils import calc_file_path_hash, within_time_range from fields import LowerCaseCharField # Get an instance of a logger logger = logging.getLogger(__name__) -class UuidObjidMap(models.Model): +class UuidObjidMap(models.Model): """ Model used for store crocdoc uuid and file object id mapping. """ uuid = models.CharField(max_length=40) obj_id = models.CharField(max_length=40, unique=True) - + class FileDiscuss(models.Model): """ Model used to represents the relationship between group message and file/dir. @@ -67,7 +67,7 @@ class StarredFile(object): class UserStarredFilesManager(models.Manager): def get_starred_files_by_username(self, username): """Get a user's starred files. - + Arguments: - `self`: - `username`: @@ -151,7 +151,7 @@ class GroupEnabledModule(models.Model): group_id = models.CharField(max_length=10, db_index=True) module_name = models.CharField(max_length=20) -########## misc +########## misc class UserLastLogin(models.Model): username = models.CharField(max_length=255, db_index=True) last_login = models.DateTimeField(default=timezone.now) @@ -186,7 +186,7 @@ class InnerPubMsg(models.Model): class Meta: ordering = ['-timestamp'] - + class InnerPubMsgReply(models.Model): reply_to = models.ForeignKey(InnerPubMsg) from_email = models.EmailField() @@ -209,3 +209,29 @@ class DeviceToken(models.Model): def __unicode__(self): return "/".join(self.user, self.token) + +_CLIENT_LOGIN_TOKEN_EXPIRATION_SECONDS = 30 + +class ClientLoginTokenManager(models.Manager): + def get_username(self, tokenstr): + try: + token = super(ClientLoginTokenManager, self).get(token=tokenstr) + except ClientLoginToken.DoesNotExist: + return None + username = token.username + token.delete() + if not within_time_range(token.timestamp, timezone.now(), + _CLIENT_LOGIN_TOKEN_EXPIRATION_SECONDS): + return None + return username + +class ClientLoginToken(models.Model): + # TODO: update sql/mysql.sql and sql/sqlite3.sql + token = models.CharField(max_length=32, primary_key=True) + username = models.CharField(max_length=255, db_index=True) + timestamp = models.DateTimeField(default=timezone.now) + + objects = ClientLoginTokenManager() + + def __unicode__(self): + return "/".join(self.username, self.token) diff --git a/seahub/urls.py b/seahub/urls.py index 3a697e3d93..acfa8e7bfd 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -244,6 +244,8 @@ urlpatterns = patterns('', url(r'^useradmin/batchmakeadmin/$', batch_user_make_admin, name='batch_user_make_admin'), url(r'^useradmin/batchadduser/$', batch_add_user, name='batch_add_user'), + + url(r'^client-login/$', client_token_login, name='client_token_login'), ) if settings.SERVE_STATIC: @@ -272,15 +274,15 @@ if getattr(settings, 'ENABLE_PAYMENT', False): ) -if getattr(settings, 'ENABLE_SYSADMIN_EXTRA', False): - from seahub_extra.sysadmin_extra.views import sys_login_admin, \ - sys_log_file_audit, sys_log_file_update, sys_log_perm_audit - urlpatterns += patterns('', - url(r'^sys/loginadmin/', sys_login_admin, name='sys_login_admin'), - url(r'^sys/log/fileaudit/', sys_log_file_audit, name='sys_log_file_audit'), - url(r'^sys/log/fileupdate/', sys_log_file_update, name='sys_log_file_update'), - url(r'^sys/log/permaudit/', sys_log_perm_audit, name='sys_log_perm_audit'), - ) +# if getattr(settings, 'ENABLE_SYSADMIN_EXTRA', False): +# from seahub_extra.sysadmin_extra.views import sys_login_admin, \ +# sys_log_file_audit, sys_log_file_update, sys_log_perm_audit +# urlpatterns += patterns('', +# url(r'^sys/loginadmin/', sys_login_admin, name='sys_login_admin'), +# url(r'^sys/log/fileaudit/', sys_log_file_audit, name='sys_log_file_audit'), +# url(r'^sys/log/fileupdate/', sys_log_file_update, name='sys_log_file_update'), +# url(r'^sys/log/permaudit/', sys_log_perm_audit, name='sys_log_perm_audit'), +# ) if getattr(settings, 'MULTI_TENANCY', False): urlpatterns += patterns('', diff --git a/seahub/utils/__init__.py b/seahub/utils/__init__.py index 728b469446..2eea00203e 100644 --- a/seahub/utils/__init__.py +++ b/seahub/utils/__init__.py @@ -1272,3 +1272,10 @@ def get_origin_repo_info(repo_id): return (origin_repo_id, origin_path) return (None, None) + +def within_time_range(d1, d2, maxdiff_seconds): + '''Return true if two datetime.datetime object differs less than the given seconds''' + delta = d2 - d1 if d2 > d1 else d1 - d2 + # delta.total_seconds() is only available in python 2.7+ + diff = (delta.microseconds + (delta.seconds + delta.days*24*3600) * 1e6) / 1e6 + return diff < maxdiff_seconds diff --git a/seahub/views/__init__.py b/seahub/views/__init__.py index a172388e5d..5a22465014 100644 --- a/seahub/views/__init__.py +++ b/seahub/views/__init__.py @@ -37,7 +37,7 @@ from seahub.auth import login as auth_login from seahub.auth import get_backends from seahub.base.accounts import User from seahub.base.decorators import user_mods_check -from seahub.base.models import UserStarredFiles +from seahub.base.models import UserStarredFiles, ClientLoginToken from seahub.contacts.models import Contact from seahub.options.models import UserOptions, CryptoOptionNotSetError from seahub.profile.models import Profile @@ -2093,3 +2093,25 @@ def fake_view(request, **kwargs): """ pass +def client_token_login(request): + """Login from desktop client with a generated token. + """ + tokenstr = request.GET.get('token', '') + user = None + if len(tokenstr) == 32: + try: + username = ClientLoginToken.objects.get_username(tokenstr) + except ClientLoginToken.DoesNotExist: + pass + else: + try: + user = User.objects.get(email=username) + for backend in get_backends(): + user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__) + except User.DoesNotExist: + pass + + if user: + auth_login(request, user) + + return HttpResponseRedirect(request.GET.get("next", reverse('libraries'))) diff --git a/tests/api/test_auth.py b/tests/api/test_auth.py index e09f26b4a2..a4d0538f3a 100644 --- a/tests/api/test_auth.py +++ b/tests/api/test_auth.py @@ -3,17 +3,24 @@ Test auth related api, such as login/logout. """ -import random -import re -from urllib import urlencode, quote +import os +import time +import requests +import pytest -from tests.common.common import USERNAME, PASSWORD, SEAFILE_BASE_URL +from tests.common.common import USERNAME, PASSWORD, BASE_URL, SEAFILE_BASE_URL from tests.common.utils import randstring, urljoin from tests.api.urls import ( - AUTH_PING_URL, TOKEN_URL, DOWNLOAD_REPO_URL, LOGOUT_DEVICE_URL + AUTH_PING_URL, TOKEN_URL, DOWNLOAD_REPO_URL, LOGOUT_DEVICE_URL, + CLIENT_LOGIN_TOKEN_URL ) from tests.api.apitestbase import ApiTestBase +if not BASE_URL.endswith('/'): + BASE_URL = BASE_URL + '/' + +TRAVIS = 'TRAVIS' in os.environ + def fake_ccnet_id(): return randstring(length=40) @@ -47,6 +54,29 @@ class AuthTest(ApiTestBase): self._do_auth_ping(token, expected=401) # self._get_repo_info(sync_token, repo.repo_id, expected=400) + def test_generate_client_login_token(self): + url = self._get_client_login_url() + r = requests.get(url) + assert r.url == BASE_URL + + r = requests.get(url) + assert r.url == urljoin(BASE_URL, 'accounts/login/?next=/'), \ + 'a client login token can only be used once' + + @pytest.mark.skipif(not TRAVIS, reason="only run this test on travis builds") # pylint: disable=E1101 + def test_client_login_token_should_expire_shortly(self): + url = self._get_client_login_url() + time.sleep(30) + r = requests.get(url) + assert r.url == urljoin(BASE_URL, 'accounts/login/?next=/'), \ + 'a client login should be expired after 30 seconds' + + def test_client_login_token_redirect_to_next_url(self): + url = self._get_client_login_url() + url += '&next=/profile/' + r = requests.get(url) + assert r.url == urljoin(BASE_URL, '/profile/') + def _desktop_login(self): data = { 'username': USERNAME, @@ -75,3 +105,8 @@ class AuthTest(ApiTestBase): def _logout(self, token): self.post(LOGOUT_DEVICE_URL, token=token) + + def _get_client_login_url(self): + token = self.post(CLIENT_LOGIN_TOKEN_URL).json()['token'] + assert len(token) == 32 + return urljoin(BASE_URL, 'client-login/') + '?token=' + token diff --git a/tests/api/urls.py b/tests/api/urls.py index b01151fd75..8d18c06c38 100644 --- a/tests/api/urls.py +++ b/tests/api/urls.py @@ -34,3 +34,5 @@ DOWNLOAD_REPO_URL = apiurl('api2/repos/%s/download-info/') LOGOUT_DEVICE_URL = apiurl('api2/logout-device/') SERVER_INFO_URL = apiurl('/api2/server-info/') + +CLIENT_LOGIN_TOKEN_URL = apiurl('/api2/client-login/')