1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-08-02 07:47:32 +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.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

View File

@ -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"),

View File

@ -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}

View File

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

View File

@ -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)

View File

@ -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('',

View File

@ -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

View File

@ -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')))

View File

@ -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

View File

@ -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/')