1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-16 23:29:49 +00:00

[api] use device specific token

This commit is contained in:
lins05
2014-03-25 11:23:31 +08:00
parent bbdfa51e49
commit 28bd1f9d85
10 changed files with 494 additions and 28 deletions

View File

@@ -1,7 +1,21 @@
import datetime
import logging
from rest_framework.authentication import BaseAuthentication
from models import Token
from seahub.base.accounts import User
from seahub.api2.models import Token, TokenV2
from seahub.api2.utils import get_client_ip
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
interval = 60 * 10
return abs(delta.total_seconds()) < interval
HEADER_CLIENT_VERSION = 'HTTP_SEAFILE_CLEINT_VERSION'
HEADER_PLATFORM_VERSION = 'HTTP_SEAFILE_PLATFORM_VERSION'
class TokenAuthentication(BaseAuthentication):
"""
@@ -11,10 +25,7 @@ class TokenAuthentication(BaseAuthentication):
HTTP header, prepended with the string "Token ". For example:
Authorization: Token 401f7ac837da42b97f613d789819ff93537bee6a
"""
model = Token
"""
A custom token model may be used, but must have the following properties.
* key -- The string identifying the token
@@ -23,17 +34,70 @@ class TokenAuthentication(BaseAuthentication):
def authenticate(self, request):
auth = request.META.get('HTTP_AUTHORIZATION', '').split()
key = None
if len(auth) == 2 and auth[0].lower() == "token":
key = auth[1]
try:
token = self.model.objects.get(key=key)
except self.model.DoesNotExist:
if not key:
return None
ret = self.authenticate_v2(request, key)
if ret:
return ret
return self.authenticate_v1(request, key)
def authenticate_v1(self, request, key):
try:
token = Token.objects.get(key=key)
except Token.DoesNotExist:
return None
try:
user = User.objects.get(email=token.user)
except User.DoesNotExist:
return None
if user.is_active:
return (user, token)
def authenticate_v2(self, request, key):
try:
token = TokenV2.objects.get(key=key)
except TokenV2.DoesNotExist:
return None
try:
user = User.objects.get(email=token.user)
except User.DoesNotExist:
return None
if user.is_active:
need_save = False
# We update the device's last_login_ip, client_version, platform_version if changed
ip = get_client_ip(request)
if ip and ip != token.last_login_ip:
token.last_login_ip = ip
need_save = True
client_version = request.META.get(HEADER_CLIENT_VERSION, '')
if client_version and client_version != token.client_version:
token.client_version = client_version
need_save = True
platform_version = request.META.get(HEADER_PLATFORM_VERSION, '')
if platform_version and platform_version != token.platform_version:
token.platform_version = platform_version
need_save = True
if not within_ten_min(token.last_accessed, datetime.datetime.now()):
# We only need 10min precision for the last_accessed field
need_save = True
if need_save:
try:
token.save()
except:
logger.exception('error when save token v2:')
return (user, token)

View File

@@ -3,9 +3,10 @@ import hmac
from hashlib import sha1
from django.db import models
from seahub.base.accounts import User
from seahub.base.fields import LowerCaseCharField
DESKTOP_PLATFORMS = ('windows', 'linux', 'mac')
class Token(models.Model):
"""
The default authorization token model.
@@ -26,4 +27,127 @@ class Token(models.Model):
def __unicode__(self):
return self.key
class TokenV2Manager(models.Manager):
def get_user_devices(self, username):
'''List user devices, most recently used first'''
devices = super(TokenV2Manager, self).filter(user=username)
platform_priorities = {
'windows': 0,
'linux': 0,
'mac': 0,
'android': 1,
'ios': 1,
}
def sort_devices(d1, d2):
'''Desktop clients are listed before mobile clients. Devices of
the same category are listed by most recently used first
'''
ret = cmp(platform_priorities[d1.platform], platform_priorities[d2.platform])
if ret != 0:
return ret
return cmp(d2.last_accessed, d1.last_accessed)
return [ d.as_dict() for d in sorted(devices, sort_devices) ]
def _get_token_by_user_device(self, username, platform, device_id):
try:
return super(TokenV2Manager, self).get(user=username,
platform=platform,
device_id=device_id)
except TokenV2.DoesNotExist:
return None
def get_or_create_token(self, username, platform, device_id, device_name,
client_version, platform_version, last_login_ip):
token = self._get_token_by_user_device(username, platform, device_id)
if token:
if token.client_version != client_version or token.platform_version != platform_version \
or token.device_name != device_name:
token.client_version = client_version
token.platform_version = platform_version
token.device_name = device_name
token.save()
return token
token = TokenV2(user=username,
platform=platform,
device_id=device_id,
device_name=device_name,
client_version=client_version,
platform_version=platform_version,
last_login_ip=last_login_ip)
token.save()
return token
def delete_device_token(self, username, platform, device_id):
super(TokenV2Manager, self).filter(user=username, platform=platform, device_id=device_id).delete()
class TokenV2(models.Model):
"""
Device specific token
"""
key = models.CharField(max_length=40, primary_key=True)
user = LowerCaseCharField(max_length=255)
# windows/linux/mac/ios/android
platform = LowerCaseCharField(max_length=32)
# ccnet id, android secure id, etc.
device_id = models.CharField(max_length=40)
# lin-laptop
device_name = models.CharField(max_length=40)
# platform version
platform_version = LowerCaseCharField(max_length=16)
# seafile client/app version
client_version = LowerCaseCharField(max_length=16)
# most recent activity
last_accessed = models.DateTimeField(auto_now=True)
last_login_ip = models.GenericIPAddressField(null=True, default=None)
objects = TokenV2Manager()
class Meta:
unique_together = (('user', 'platform', 'device_id'),)
def save(self, *args, **kwargs):
if not self.key:
self.key = self.generate_key()
return super(TokenV2, self).save(*args, **kwargs)
def generate_key(self):
unique = str(uuid.uuid4())
return hmac.new(unique, digestmod=sha1).hexdigest()
def __unicode__(self):
return "TokenV2{user=%(user)s,device=%(device_name)s}" % \
dict(user=self.user,device_name=self.device_name)
def is_desktop_client(self):
return str(self.platform) in ('windows', 'linux', 'mac')
def as_dict(self):
return dict(key=self.key,
user=self.user,
platform=self.platform,
device_id=self.device_id,
device_name=self.device_name,
client_version=self.client_version,
platform_version=self.platform_version,
last_accessed=self.last_accessed,
last_login_ip=self.last_login_ip)

View File

@@ -1,28 +1,103 @@
from rest_framework import serializers
from seahub.auth import authenticate
from seahub.api2.models import Token, TokenV2, DESKTOP_PLATFORMS
from seahub.api2.utils import get_client_ip
def all_none(values):
for value in values:
if value is not None:
return False
return True
def all_not_none(values):
for value in values:
if value is None:
return False
return True
class AuthTokenSerializer(serializers.Serializer):
username = serializers.CharField()
password = serializers.CharField()
# There fields are used by TokenV2
platform = serializers.CharField(required=False)
device_id = serializers.CharField(required=False)
device_name = serializers.CharField(required=False)
# These fields may be needed in the future
client_version = serializers.CharField(required=False)
platform_version = serializers.CharField(required=False)
def validate(self, attrs):
username = attrs.get('username')
password = attrs.get('password')
platform = attrs.get('platform', None)
device_id = attrs.get('device_id', None)
device_name = attrs.get('device_name', None)
client_version = attrs.get('client_version', None)
platform_version = attrs.get('platform_version', None)
v2_fields = (platform, device_id, device_name, client_version, platform_version)
# Decide the version of token we need
if all_none(v2_fields):
v2 = False
elif all_not_none(v2_fields):
v2 = True
else:
raise serializers.ValidationError('invalid params')
# first check password
if username and password:
user = authenticate(username=username, password=password)
if user:
if not user.is_active:
raise serializers.ValidationError('User account is disabled.')
attrs['user'] = user
return attrs
else:
raise serializers.ValidationError('Unable to login with provided credentials.')
else:
raise serializers.ValidationError('Must include "username" and "password"')
# Now user is authenticated
if v2:
token = self.get_token_v2(username, platform, device_id, device_name,
client_version, platform_version)
else:
token = self.get_token_v1(username)
return token.key
def get_token_v1(self, username):
token, created = Token.objects.get_or_create(user=username)
return token
def get_token_v2(self, username, platform, device_id, device_name,
client_version, platform_version):
if platform in DESKTOP_PLATFORMS:
# desktop device id is the peer id, so it must be 40 chars
if len(device_id) != 40:
raise serializers.ValidationError('invalid device id')
elif platform == 'android':
# android device id is the 64bit secure id, so it must be 16 chars in hex representation
if len(device_id) != 16:
raise serializers.ValidationError('invalid device id')
elif platform == 'ios':
pass
else:
raise serializers.ValidationError('invalid platform')
request = self.context['request']
last_login_ip = get_client_ip(request)
return TokenV2.objects.get_or_create_token(username, platform, device_id, device_name,
client_version, platform_version, last_login_ip)
class AccountSerializer(serializers.Serializer):
email = serializers.EmailField()
password = serializers.CharField()

View File

@@ -449,3 +449,12 @@ def api_group_check(func):
return api_error(status.HTTP_403_FORBIDDEN, 'Forbid to access this group.')
return _decorated
def get_client_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR', '')
return ip

View File

@@ -25,14 +25,13 @@ from django.template.loader import render_to_string
from django.shortcuts import render_to_response
from django.utils import timezone
from models import Token
from authentication import TokenAuthentication
from serializers import AuthTokenSerializer, AccountSerializer
from utils import is_repo_writable, is_repo_accessible, calculate_repo_info, \
api_error, get_file_size, prepare_starred_files, \
get_groups, get_group_and_contacts, prepare_events, \
get_person_msgs, api_group_check, get_email, get_timetamp, \
get_group_message_json, get_group_msgs, get_group_msgs_json
get_group_message_json, get_group_msgs, get_group_msgs_json, get_client_ip
from seahub.base.accounts import User
from seahub.base.models import FileDiscuss, UserStarredFiles, \
DirFilesLastModifiedInfo, DeviceToken
@@ -131,13 +130,13 @@ class ObtainAuthToken(APIView):
permission_classes = ()
parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
renderer_classes = (renderers.JSONRenderer,)
model = Token
def post(self, request):
serializer = AuthTokenSerializer(data=request.DATA)
context = { 'request': request }
serializer = AuthTokenSerializer(data=request.DATA, context=context)
if serializer.is_valid():
token, created = Token.objects.get_or_create(user=serializer.object['user'].username)
return Response({'token': token.key})
key = serializer.object
return Response({'token': key})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

View File

@@ -0,0 +1,95 @@
{% extends "home_base.html" %}
{% load seahub_tags avatar_tags i18n %}
{% block sub_title %}{% trans "Devices" %} - {% endblock %}
{% block cur_devices %}tab-cur{% endblock %}
{% block right_panel %}
<h3 class="hd">{% trans "Devices" %}</h3>
{% if devices %}
<table class="client-list">
<tr>
<th width="10%">{% trans "Platform" %}</th>
<th width="25%">{% trans "Device Name" %}</th>
<th width="20%">{% trans "IP" %}</th>
<th width="15%">{% trans "Last Access" %}</th>
<th width="20%">{% trans "Libraries" %}</th>
<th width="10%">{% trans "Operation" %}</th>
</tr>
{% for device in devices %}
<tr data-platform="{{ device.platform }}" data-device-id="{{ device.device_id }}">
<td>{{ device.platform }}</td>
<td>{{ device.device_name }}</td>
<!-- <td>{{ device.client_version }}</td> -->
<td>{{ device.last_login_ip }}</td>
<td>{{ device.last_accessed | translate_seahub_time }}</td>
<td>
<ul>
{% for repo in device.synced_repos %}
<li>
<a name="{{ repo.sync_time }}" href="{% url 'repo' repo.repo_id %}">{{ repo.repo_name }}</a>
</li>
{% endfor %}
</ul>
</td>
<td>
<div><a href="#" class="unlink-device op vh">{% trans "Unlink" %}</a></div>
</td>
</tr>
{% endfor %}
</table>
<div id="unlink-device-confirm" class="hide">
<p>{% trans "Really want to unlink this device? It will immediately stop syncing." %}</p>
<button class="yes">{% trans "Yes" %}</button>
<button class="no simplemodal-close">{% trans "No" %}</button>
</div>
{% else %}
<div class="empty-tips">
<h2 class="alc">{% trans "You do not have connected devices" %}</h2>
<p>{% trans "Your clients (Desktop/Anroid/iOS) would be listed here." %}</p>
</div>
{% endif %}
{% endblock %}
{% block extra_script %}{{block.super}}
<script type="text/javascript">
function send_unlink_request(tr) {
var post_data = {
'platform': tr.data('platform'),
'device_id': tr.data('device-id')
};
console.log('send_unlink_request called, ' + post_data);
$.ajax({
url: '{% url 'unlink_device' %}',
type: 'POST',
dataType: 'json',
beforeSend: prepareCSRFToken,
data: post_data,
success: function(data) {
$.modal.close();
tr.remove();
feedback("{% trans "Successfully unlinked." %}", 'success');
},
error: function(xhr, textStatus, errorThrown) {
$.modal.close();
var error = $.parseJSON(xhr.responseText).error;
feedback("{% trans "Failed to unlink the device." %}", 'error');
}
});
}
$('.unlink-device').click(function() {
var op = $(this);
var tr = op.parents('tr');
var form = $('#unlink-device-confirm');
form.modal({appendTo: "#main", focus:false});
$($('.yes', form)).click(function() {
send_unlink_request(tr);
return false;
});
});
</script>
{% endblock %}

View File

@@ -20,7 +20,7 @@
{% endif %}
{% endfor %}
<li class="tab {% block cur_messages %}{% endblock %}"><a href="{% url 'message_list' %}" class="msgs">{% trans "Messages" %}</a></li>
<li class="tab {% block cur_clients %}{% endblock %}"><a href="{% url 'client_mgmt' %}" class="clients">{% trans "Clients" %}</a></li>
<li class="tab {% block cur_devices %}{% endblock %}"><a href="{% url 'devices' %}" class="clients">{% trans "Devices" %}</a></li>
<li class="tab {% block cur_contacts %}{% endblock %}"><a href="{% url 'contacts' %}" class="contacts">{% trans "Contacts" %}</a></li>
</ul>

View File

@@ -53,8 +53,10 @@ urlpatterns = patterns('',
url(r'^home/wiki_page_edit/(?P<page_name>[^/]+)$', personal_wiki_page_edit, name='personal_wiki_page_edit'),
url(r'^home/wiki_page_delete/(?P<page_name>[^/]+)$', personal_wiki_page_delete, name='personal_wiki_page_delete'),
url(r'^home/clients/$', client_mgmt, name='client_mgmt'),
url(r'^home/clients/unsync/$', client_unsync, name='client_unsync'),
# url(r'^home/clients/$', client_mgmt, name='client_mgmt'),
# url(r'^home/clients/unsync/$', client_unsync, name='client_unsync'),
url(r'^devices/$', devices, name='devices'),
url(r'^home/devices/unlink/$', unlink_device, name='unlink_device'),
# url(r'^home/public/reply/(?P<msg_id>[\d]+)/$', innerpub_msg_reply, name='innerpub_msg_reply'),
# url(r'^home/owner/(?P<owner_name>[^/]+)/$', ownerhome, name='ownerhome'),

68
seahub/utils/devices.py Normal file
View File

@@ -0,0 +1,68 @@
import logging
from seaserv import seafile_api
from seahub.api2.models import TokenV2, DESKTOP_PLATFORMS
logger = logging.getLogger(__name__)
__all__ = [
'get_user_devices',
'do_unlink_device',
]
def get_user_devices(username):
devices = TokenV2.objects.get_user_devices(username)
peer_repos_map = get_user_synced_repo_infos(username)
for device in devices:
if device['platform'] in DESKTOP_PLATFORMS:
peer_id = device['device_id']
device['synced_repos'] = peer_repos_map.get(peer_id, [])
return devices
def get_user_synced_repo_infos(username):
'''Return a (client_ccnet_peer_id, synced_repos_on_that_client) dict'''
tokens = []
try:
tokens = seafile_api.list_repo_tokens_by_email(username)
except:
return {}
def sort_by_sync_time_descending(a, b):
if isinstance(a, dict):
return cmp(b['sync_time'], a['sync_time'])
else:
return cmp(b.sync_time, a.sync_time)
tokens.sort(sort_by_sync_time_descending, reverse=True)
peer_repos_map = {}
for token in tokens:
peer_id = token.peer_id
repo_id = token.repo_id
if peer_id not in peer_repos_map:
peer_repos_map[peer_id] = {}
peer_repos_map[peer_id][repo_id] = {
'repo_id': token.repo_id,
'repo_name': token.repo_name,
'sync_time': token.sync_time
}
ret = {}
for peer_id, repos in peer_repos_map.iteritems():
ret[peer_id] = sorted(repos.values(), sort_by_sync_time_descending)
return ret
def do_unlink_device(username, platform, device_id):
if platform in DESKTOP_PLATFORMS:
# For desktop client, we also remove the sync tokens
if seafile_api.delete_repo_tokens_by_peer_id(username, device_id) < 0:
logger.warning('failed to delete_repo_tokens_by_peer_id')
raise Exception('failed to delete_repo_tokens_by_peer_id')
TokenV2.objects.delete_device_token(username, platform, device_id)

View File

@@ -15,6 +15,7 @@ import datetime as dt
from datetime import datetime
from math import ceil
from urllib import quote
from django.utils.datastructures import SortedDict
from django.core.cache import cache
from django.core.urlresolvers import reverse
@@ -89,6 +90,7 @@ from seahub.utils.star import get_dir_starred_files
from seahub.views.modules import MOD_PERSONAL_WIKI, \
enable_mod_for_user, disable_mod_for_user
from seahub.utils import HAS_OFFICE_CONVERTER
from seahub.utils.devices import get_user_devices, do_unlink_device
if HAS_OFFICE_CONVERTER:
from seahub.utils import prepare_converted_html, OFFICE_PREVIEW_MAX_SIZE, OFFICE_PREVIEW_MAX_PAGES
@@ -1063,6 +1065,34 @@ def starred(request):
"starred_files": starred_files,
}, context_instance=RequestContext(request))
@login_required
def devices(request):
"""List user devices"""
username = request.user.username
user_devices = get_user_devices(username)
return render_to_response('devices.html', {
"devices": user_devices,
}, context_instance=RequestContext(request))
@login_required
def unlink_device(request):
if not request.is_ajax():
raise Http404
content_type = 'application/json; charset=utf-8'
platform = request.POST.get('platform', '')
device_id = request.POST.get('device_id', '')
if not platform or not device_id:
return HttpResponseBadRequest(content_type=content_type)
do_unlink_device(request.user.username, platform, device_id)
return HttpResponse(json.dumps({'success': True}),
content_type=content_type)
@login_required
@user_mods_check
def client_mgmt(request):