1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-08-10 11:22:09 +00:00

support logging in from desktop client directly

This commit is contained in:
Shuai Lin 2015-04-27 14:54:12 +08:00
parent 247c6d5e31
commit c63f68dfc5
10 changed files with 139 additions and 30 deletions

View File

@ -9,6 +9,7 @@ from seahub.base.accounts import User
from seahub.constants import GUEST_USER from seahub.constants import GUEST_USER
from seahub.api2.models import Token, TokenV2 from seahub.api2.models import Token, TokenV2
from seahub.api2.utils import get_client_ip from seahub.api2.utils import get_client_ip
from seahub.utils import within_time_range
try: try:
from seahub.settings import MULTI_TENANCY from seahub.settings import MULTI_TENANCY
except ImportError: except ImportError:
@ -16,13 +17,6 @@ except ImportError:
logger = logging.getLogger(__name__) 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_CLIENT_VERSION = 'HTTP_SEAFILE_CLEINT_VERSION'
HEADER_PLATFORM_VERSION = 'HTTP_SEAFILE_PLATFORM_VERSION' HEADER_PLATFORM_VERSION = 'HTTP_SEAFILE_PLATFORM_VERSION'
@ -135,7 +129,7 @@ class TokenAuthentication(BaseAuthentication):
token.platform_version = platform_version token.platform_version = platform_version
need_save = True 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 # We only need 10min precision for the last_accessed field
need_save = True need_save = True

View File

@ -2,7 +2,7 @@ from django.conf.urls.defaults import *
from .views import * from .views import *
from .views_misc import ServerInfoView from .views_misc import ServerInfoView
from .views_auth import LogoutDeviceView from .views_auth import LogoutDeviceView, ClientLoginTokenView
urlpatterns = patterns('', urlpatterns = patterns('',
@ -11,6 +11,7 @@ urlpatterns = patterns('',
url(r'^auth-token/', ObtainAuthToken.as_view()), url(r'^auth-token/', ObtainAuthToken.as_view()),
url(r'^server-info/$', ServerInfoView.as_view()), url(r'^server-info/$', ServerInfoView.as_view()),
url(r'^logout-device/$', LogoutDeviceView.as_view()), url(r'^logout-device/$', LogoutDeviceView.as_view()),
url(r'^client-login/$', ClientLoginTokenView.as_view()),
# RESTful API # RESTful API
url(r'^accounts/$', Accounts.as_view(), name="accounts"), url(r'^accounts/$', Accounts.as_view(), name="accounts"),

View File

@ -8,6 +8,8 @@ from seahub import settings
from seahub.api2.utils import json_response, api_error from seahub.api2.utils import json_response, api_error
from seahub.api2.authentication import TokenAuthentication from seahub.api2.authentication import TokenAuthentication
from seahub.api2.models import Token, TokenV2 from seahub.api2.models import Token, TokenV2
from seahub.base.models import ClientLoginToken
from seahub.utils import gen_token
class LogoutDeviceView(APIView): class LogoutDeviceView(APIView):
"""Removes the api token of a device that has already logged in. If the device """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,) authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
throttle_classes = (UserRateThrottle,) throttle_classes = (UserRateThrottle,)
@json_response @json_response
def post(self, request, format=None): def post(self, request, format=None):
auth_token = request.auth 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) seafile_api.delete_repo_tokens_by_peer_id(request.user.username, auth_token.device_id)
auth_token.delete() auth_token.delete()
return {} 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}

View File

@ -82,4 +82,5 @@ def base(request):
'grps': grps, 'grps': grps,
'multi_tenancy': MULTI_TENANCY, 'multi_tenancy': MULTI_TENANCY,
'search_repo_id': search_repo_id, 'search_repo_id': search_repo_id,
'debug': True,
} }

View File

@ -9,7 +9,7 @@ from seaserv import seafile_api
from seahub.auth.signals import user_logged_in from seahub.auth.signals import user_logged_in
from seahub.group.models import GroupMessage 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 from fields import LowerCaseCharField
@ -209,3 +209,29 @@ class DeviceToken(models.Model):
def __unicode__(self): def __unicode__(self):
return "/".join(self.user, self.token) 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)

View File

@ -244,6 +244,8 @@ urlpatterns = patterns('',
url(r'^useradmin/batchmakeadmin/$', batch_user_make_admin, name='batch_user_make_admin'), 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'^useradmin/batchadduser/$', batch_add_user, name='batch_add_user'),
url(r'^client-login/$', client_token_login, name='client_token_login'),
) )
if settings.SERVE_STATIC: if settings.SERVE_STATIC:
@ -272,15 +274,15 @@ if getattr(settings, 'ENABLE_PAYMENT', False):
) )
if getattr(settings, 'ENABLE_SYSADMIN_EXTRA', False): # if getattr(settings, 'ENABLE_SYSADMIN_EXTRA', False):
from seahub_extra.sysadmin_extra.views import sys_login_admin, \ # from seahub_extra.sysadmin_extra.views import sys_login_admin, \
sys_log_file_audit, sys_log_file_update, sys_log_perm_audit # sys_log_file_audit, sys_log_file_update, sys_log_perm_audit
urlpatterns += patterns('', # urlpatterns += patterns('',
url(r'^sys/loginadmin/', sys_login_admin, name='sys_login_admin'), # 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/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/fileupdate/', sys_log_file_update, name='sys_log_file_update'),
url(r'^sys/log/permaudit/', sys_log_perm_audit, name='sys_log_perm_audit'), # url(r'^sys/log/permaudit/', sys_log_perm_audit, name='sys_log_perm_audit'),
) # )
if getattr(settings, 'MULTI_TENANCY', False): if getattr(settings, 'MULTI_TENANCY', False):
urlpatterns += patterns('', urlpatterns += patterns('',

View File

@ -1272,3 +1272,10 @@ def get_origin_repo_info(repo_id):
return (origin_repo_id, origin_path) return (origin_repo_id, origin_path)
return (None, None) 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

View File

@ -37,7 +37,7 @@ from seahub.auth import login as auth_login
from seahub.auth import get_backends from seahub.auth import get_backends
from seahub.base.accounts import User from seahub.base.accounts import User
from seahub.base.decorators import user_mods_check 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.contacts.models import Contact
from seahub.options.models import UserOptions, CryptoOptionNotSetError from seahub.options.models import UserOptions, CryptoOptionNotSetError
from seahub.profile.models import Profile from seahub.profile.models import Profile
@ -2093,3 +2093,25 @@ def fake_view(request, **kwargs):
""" """
pass 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')))

View File

@ -3,17 +3,24 @@
Test auth related api, such as login/logout. Test auth related api, such as login/logout.
""" """
import random import os
import re import time
from urllib import urlencode, quote 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.common.utils import randstring, urljoin
from tests.api.urls import ( 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 from tests.api.apitestbase import ApiTestBase
if not BASE_URL.endswith('/'):
BASE_URL = BASE_URL + '/'
TRAVIS = 'TRAVIS' in os.environ
def fake_ccnet_id(): def fake_ccnet_id():
return randstring(length=40) return randstring(length=40)
@ -47,6 +54,29 @@ class AuthTest(ApiTestBase):
self._do_auth_ping(token, expected=401) self._do_auth_ping(token, expected=401)
# self._get_repo_info(sync_token, repo.repo_id, expected=400) # 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): def _desktop_login(self):
data = { data = {
'username': USERNAME, 'username': USERNAME,
@ -75,3 +105,8 @@ class AuthTest(ApiTestBase):
def _logout(self, token): def _logout(self, token):
self.post(LOGOUT_DEVICE_URL, token=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

View File

@ -34,3 +34,5 @@ DOWNLOAD_REPO_URL = apiurl('api2/repos/%s/download-info/')
LOGOUT_DEVICE_URL = apiurl('api2/logout-device/') LOGOUT_DEVICE_URL = apiurl('api2/logout-device/')
SERVER_INFO_URL = apiurl('/api2/server-info/') SERVER_INFO_URL = apiurl('/api2/server-info/')
CLIENT_LOGIN_TOKEN_URL = apiurl('/api2/client-login/')