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:
parent
247c6d5e31
commit
c63f68dfc5
@ -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
|
||||
|
||||
|
@ -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"),
|
||||
|
@ -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}
|
||||
|
@ -82,4 +82,5 @@ def base(request):
|
||||
'grps': grps,
|
||||
'multi_tenancy': MULTI_TENANCY,
|
||||
'search_repo_id': search_repo_id,
|
||||
'debug': True,
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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('',
|
||||
|
@ -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
|
||||
|
@ -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')))
|
||||
|
@ -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
|
||||
|
@ -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/')
|
||||
|
Loading…
Reference in New Issue
Block a user