diff --git a/seahub/api2/authentication.py b/seahub/api2/authentication.py index f72aff11df..c3569f5941 100644 --- a/seahub/api2/authentication.py +++ b/seahub/api2/authentication.py @@ -7,7 +7,7 @@ from rest_framework.exceptions import APIException import seaserv from seahub.base.accounts import User from seahub.constants import GUEST_USER -from seahub.api2.models import Token, TokenV2 +from seahub.api2.models import Token, TokenV2, WipedDevice from seahub.api2.utils import get_client_ip from seahub.utils import within_time_range try: @@ -28,6 +28,8 @@ class AuthenticationFailed(APIException): def __init__(self, detail=None): self.detail = detail or self.default_detail +class DeviceRemoteWipedException(AuthenticationFailed): + pass class TokenAuthentication(BaseAuthentication): """ @@ -98,7 +100,15 @@ class TokenAuthentication(BaseAuthentication): try: token = TokenV2.objects.get(key=key) except TokenV2.DoesNotExist: - return None # Continue authentication in token v1 + try: + token = WipedDevice.objects.get(key=key) + except WipedDevice.DoesNotExist: + pass + else: + raise DeviceRemoteWipedException('Device set to be remote wiped') + + # Continue authentication in token v1 + return None try: user = User.objects.get(email=token.user) diff --git a/seahub/api2/base.py b/seahub/api2/base.py new file mode 100644 index 0000000000..b6d54952d0 --- /dev/null +++ b/seahub/api2/base.py @@ -0,0 +1,25 @@ +#coding: UTF-8 + +from rest_framework.views import APIView as RestFrameworkAPIView + +from seahub.api2.authentication import DeviceRemoteWipedException + +class APIView(RestFrameworkAPIView): + """ + Subclass restframework's APIView to implement some custom feature like + adding a `X-Seafile-Wiped` header if the current client device has been + marked to be remote wiped by the user. + """ + def __init__(self, *a, **kw): + super(APIView, self).__init__(*a, **kw) + self._seafile_exc = None + + def handle_exception(self, exc): + self._seafile_exc = exc + return super(APIView, self).handle_exception(exc) + + def dispatch(self, *a, **kw): + response = super(APIView, self).dispatch(*a, **kw) + if self._seafile_exc and isinstance(self._seafile_exc, DeviceRemoteWipedException): + response['X-Seafile-Wiped'] = 'true' + return response diff --git a/seahub/api2/models.py b/seahub/api2/models.py index 688f9d837b..7dfd9b5f68 100644 --- a/seahub/api2/models.py +++ b/seahub/api2/models.py @@ -1,7 +1,8 @@ import uuid import hmac +import datetime from hashlib import sha1 -from django.db import models +from django.db import models, transaction from seahub.base.fields import LowerCaseCharField @@ -96,11 +97,19 @@ class TokenV2Manager(models.Manager): 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() + def mark_device_to_be_remote_wiped(self, username, platform, device_id): + token = self._get_token_by_user_device(username, platform, device_id) + if not token: + return + with transaction.atomic(): + wiped_device = WipedDevice(key=token.key) + wiped_device.save() + token.delete() class TokenV2(models.Model): """ @@ -162,3 +171,8 @@ class TokenV2(models.Model): platform_version=self.platform_version, last_accessed=self.last_accessed, last_login_ip=self.last_login_ip) + +class WipedDevice(models.Model): + key = models.CharField(max_length=40, primary_key=True) + + wiped_at = models.DateTimeField(auto_now=True) diff --git a/seahub/api2/urls.py b/seahub/api2/urls.py index 2307681cf2..4dc86e6d04 100644 --- a/seahub/api2/urls.py +++ b/seahub/api2/urls.py @@ -23,6 +23,7 @@ urlpatterns = patterns('', url(r'^server-info/$', ServerInfoView.as_view()), url(r'^logout-device/$', LogoutDeviceView.as_view()), url(r'^client-login/$', ClientLoginTokenView.as_view()), + url(r'^device-wiped/$', RemoteWipeReportView.as_view()), # RESTful API url(r'^accounts/$', Accounts.as_view(), name="accounts"), diff --git a/seahub/api2/views.py b/seahub/api2/views.py index 418d13f1a2..a2e8966f9d 100644 --- a/seahub/api2/views.py +++ b/seahub/api2/views.py @@ -16,7 +16,6 @@ from rest_framework.authentication import SessionAuthentication from rest_framework.permissions import IsAuthenticated, IsAdminUser from rest_framework.reverse import reverse from rest_framework.response import Response -from rest_framework.views import APIView from django.contrib.auth.hashers import check_password from django.contrib.sites.models import RequestSite @@ -40,6 +39,7 @@ from .utils import get_diff_details, \ api_repo_user_folder_perm_check, api_repo_setting_permission_check, \ api_repo_group_folder_perm_check +from seahub.api2.base import APIView from seahub.avatar.templatetags.avatar_tags import api_avatar_url, avatar from seahub.avatar.templatetags.group_avatar_tags import api_grp_avatar_url, \ grp_avatar @@ -1914,6 +1914,7 @@ class DevicesView(APIView): platform = request.data.get('platform', '') device_id = request.data.get('device_id', '') + remote_wipe = request.data.get('wipe_device', '') if not platform: error_msg = 'platform invalid.' @@ -1924,7 +1925,10 @@ class DevicesView(APIView): return api_error(status.HTTP_400_BAD_REQUEST, error_msg) try: - do_unlink_device(request.user.username, platform, device_id) + do_unlink_device(request.user.username, + platform, + device_id, + remote_wipe=remote_wipe) except SearpcError as e: logger.error(e) error_msg = 'Internal Server Error' @@ -4534,3 +4538,20 @@ class RepoGroupFolderPerm(APIView): logger.error(e) error_msg = 'Internal Server Error' return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + +class RemoteWipeReportView(APIView): + throttle_classes = (UserRateThrottle,) + + @json_response + def post(self, request): + from seahub.api2.models import WipedDevice + token = request.POST.get('token', '') + if not token or len(token) != 40: + return api_error(status.HTTP_400_BAD_REQUEST, "device token is missing") + try: + entry = WipedDevice.objects.get(key=token) + entry.delete() + except WipedDevice.DoesNotExist: + return api_error(status.HTTP_400_BAD_REQUEST, "invalid device token") + + return {} diff --git a/seahub/api2/views_auth.py b/seahub/api2/views_auth.py index 131366784a..d7db551151 100644 --- a/seahub/api2/views_auth.py +++ b/seahub/api2/views_auth.py @@ -1,9 +1,9 @@ from rest_framework import status -from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated from seaserv import seafile_api from seahub import settings +from seahub.api2.base import APIView from seahub.api2.throttling import AnonRateThrottle, UserRateThrottle from seahub.api2.utils import json_response, api_error from seahub.api2.authentication import TokenAuthentication diff --git a/seahub/api2/views_misc.py b/seahub/api2/views_misc.py index 5b820a4db5..af67aa23a4 100644 --- a/seahub/api2/views_misc.py +++ b/seahub/api2/views_misc.py @@ -1,5 +1,4 @@ -from rest_framework.views import APIView - +from seahub.api2.base import APIView from seahub.api2.utils import json_response, is_seafile_pro from seahub import settings from seahub.utils import HAS_OFFICE_CONVERTER, HAS_FILE_SEARCH diff --git a/seahub/utils/devices.py b/seahub/utils/devices.py index 8665caba56..823a257418 100644 --- a/seahub/utils/devices.py +++ b/seahub/utils/devices.py @@ -68,7 +68,7 @@ def get_user_synced_repo_infos(username): return ret -def do_unlink_device(username, platform, device_id): +def do_unlink_device(username, platform, device_id, remote_wipe=False): if platform in DESKTOP_PLATFORMS: # For desktop client, we also remove the sync tokens msg = 'failed to delete_repo_tokens_by_peer_id' @@ -80,4 +80,7 @@ def do_unlink_device(username, platform, device_id): logger.exception(msg) raise - TokenV2.objects.delete_device_token(username, platform, device_id) + if remote_wipe: + TokenV2.objects.mark_device_to_be_remote_wiped(username, platform, device_id) + else: + TokenV2.objects.delete_device_token(username, platform, device_id) diff --git a/static/scripts/app/models/device.js b/static/scripts/app/models/device.js index fe781169b5..46c9743e05 100644 --- a/static/scripts/app/models/device.js +++ b/static/scripts/app/models/device.js @@ -11,6 +11,9 @@ define([ 'platform': this.get('platform'), 'device_id': this.get('device_id') }; + if (options.wipe_device) { + data['wipe_device'] = 'true'; + } $.ajax({ url: Common.getUrl({name: 'devices'}), diff --git a/static/scripts/app/views/device.js b/static/scripts/app/views/device.js index be1060e88b..0768144bb2 100644 --- a/static/scripts/app/views/device.js +++ b/static/scripts/app/views/device.js @@ -73,9 +73,13 @@ define([ device_name = this.model.get('device_name'); var title = gettext('Unlink device'); var content = gettext('Are you sure you want to unlink this device?'); + var extraOption = gettext('Delete files from this device the next time it comes online.'); - var yesCallback = function () { + var yesCallback = function (wipe_device) { + console.log('wipe_device = ' + wipe_device); _this.model.unlink({ + wipe_device: wipe_device, + success: function() { _this.remove(); @@ -92,7 +96,7 @@ define([ }); return false; }; - Common.showConfirm(title, content, yesCallback); + Common.showConfirmWithExtraOption(title, content, extraOption, yesCallback); } });