plone.restapi/unify-expired-and-excluded-items-handling: Require lock token for updating a locked object

Thomas Buchberger <jenkins-z4DKO/[email protected]> Sat, 29 Jul 2017 16:32:14 -0700 (PDT)
Newsgroups gmane.comp.web.zope.plone.cvs
Message-ID <[email protected]>
Repository: plone.restapi
Branch: refs/heads/unify-expired-and-excluded-items-handling
Date: 2017-07-27T18:20:15+02:00
Author: Thomas Buchberger (buchi) <t.buchberger-/8aE7x569Db/[email protected]>
Commit: https://github.com/plone/plone.restapi/commit/894e5ad749dd7cca3faf8f007c16171ecdf0f66f

Require lock token for updating a locked object

Files changed:
M src/plone/restapi/services/content/update.py
M src/plone/restapi/services/locking/locking.py
M src/plone/restapi/tests/test_locking.py

diff --git a/src/plone/restapi/services/content/update.py b/src/plone/restapi/services/content/update.py
index d394bd4c..19d2eb31 100644
--- a/src/plone/restapi/services/content/update.py
+++ b/src/plone/restapi/services/content/update.py
@@ -2,6 +2,7 @@
 from plone.restapi.exceptions import DeserializationError
 from plone.restapi.interfaces import IDeserializeFromJson
 from plone.restapi.services import Service
+from plone.restapi.services.locking.locking import is_locked
 from zope.component import queryMultiAdapter
 
 
@@ -10,6 +11,12 @@ class ContentPatch(Service):
     """
 
     def reply(self):
+
+        if is_locked(self.context, self.request):
+            self.request.response.setStatus(403)
+            return dict(error=dict(
+                type='Forbidden', message='Resource is locked.'))
+
         deserializer = queryMultiAdapter((self.context, self.request),
                                          IDeserializeFromJson)
         if deserializer is None:
diff --git a/src/plone/restapi/services/locking/locking.py b/src/plone/restapi/services/locking/locking.py
index 1417d45f..9037879e 100644
--- a/src/plone/restapi/services/locking/locking.py
+++ b/src/plone/restapi/services/locking/locking.py
@@ -79,7 +79,7 @@ def reply(self):
 
 def lock_info(obj):
     """Returns lock information about the given object."""
-    lockable = ILockable(obj)
+    lockable = ILockable(obj, None)
     if lockable is not None:
         info = {
             'locked': lockable.locked(),
@@ -101,7 +101,7 @@ def lock_info(obj):
 
 def webdav_lock(obj):
     """Returns the WebDAV LockItem"""
-    lockable = ILockable(obj)
+    lockable = ILockable(obj, None)
     if lockable is None:
         return
 
@@ -109,3 +109,19 @@ def webdav_lock(obj):
     if len(lock_info) > 0:
         token = lock_info[0]['token']
         return obj.wl_getLock(token)
+
+
+def is_locked(obj, request):
+    """Returns true if the object is locked and the request doesn't contain
+       the lock token.
+    """
+    lockable = ILockable(obj, None)
+    if lockable is None:
+        return False
+    if lockable.locked():
+        token = request.getHeader('Lock-Token', '')
+        lock_info = lockable.lock_info()
+        if len(lock_info) > 0 and lock_info[0]['token'] == token:
+            return False
+        return True
+    return False
diff --git a/src/plone/restapi/tests/test_locking.py b/src/plone/restapi/tests/test_locking.py
index b2d9cd14..0e7b49fb 100644
--- a/src/plone/restapi/tests/test_locking.py
+++ b/src/plone/restapi/tests/test_locking.py
@@ -89,3 +89,24 @@ def test_lock_info_for_unlocked_object(self):
 
         self.assertEqual(response.status_code, 200)
         self.assertFalse(response.json()['locked'])
+
+    def test_update_locked_object_without_token_fails(self):
+        lockable = ILockable(self.doc)
+        lockable.lock()
+        transaction.commit()
+        response = self.api_session.patch('/', json={'title': 'New Title'})
+        transaction.commit()
+        self.assertEqual(response.status_code, 403)
+        self.assertEqual(self.doc.Title(), 'My Document')
+
+    def test_update_locked_object_with_token_succeeds(self):
+        lockable = ILockable(self.doc)
+        lockable.lock()
+        transaction.commit()
+        response = self.api_session.patch(
+            '/',
+            headers={'Lock-Token': lockable.lock_info()[0]['token']},
+            json={'title': 'New Title'})
+        transaction.commit()
+        self.assertEqual(response.status_code, 204)
+        self.assertEqual(self.doc.Title(), 'New Title')



------------------------------------------------------------------------------
Check out the vibrant tech community on one of the world's most
engaging tech sites, Slashdot.org! http://sdm.link/slashdot