mirror of
https://github.com/YunoHost-Apps/seafile_ynh.git
synced 2024-09-03 20:26:01 +02:00
496 lines
19 KiB
Python
496 lines
19 KiB
Python
# (c) 2009-2011 Martin Wendt and contributors; see WsgiDAV http://wsgidav.googlecode.com/
|
|
# Original PyFileServer (c) 2005 Ho Chun Wei.
|
|
# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
|
|
"""
|
|
Implements the `LockManager` object that provides the locking functionality.
|
|
|
|
The LockManager requires a LockStorage object to implement persistence.
|
|
Two alternative lock storage classes are defined in the lock_storage module:
|
|
|
|
- wsgidav.lock_storage.LockStorageDict
|
|
- wsgidav.lock_storage.LockStorageShelve
|
|
|
|
|
|
The lock data model is a dictionary with these fields:
|
|
|
|
root:
|
|
Resource URL.
|
|
principal:
|
|
Name of the authenticated user that created the lock.
|
|
type:
|
|
Must be 'write'.
|
|
scope:
|
|
Must be 'shared' or 'exclusive'.
|
|
depth:
|
|
Must be '0' or 'infinity'.
|
|
owner:
|
|
String identifying the owner.
|
|
timeout:
|
|
Seconds remaining until lock expiration.
|
|
This value is passed to create() and refresh()
|
|
expire:
|
|
Converted timeout for persistence: expire = time() + timeout.
|
|
token:
|
|
Automatically generated unique token.
|
|
|
|
See `Developers info`_ for more information about the WsgiDAV architecture.
|
|
|
|
.. _`Developers info`: http://docs.wsgidav.googlecode.com/hg/html/develop.html
|
|
"""
|
|
from pprint import pprint
|
|
from dav_error import DAVError, HTTP_LOCKED, PRECONDITION_CODE_LockConflict
|
|
from wsgidav.dav_error import DAVErrorCondition
|
|
import sys
|
|
import util
|
|
import random
|
|
import time
|
|
from rw_lock import ReadWriteLock
|
|
|
|
__docformat__ = "reStructuredText"
|
|
|
|
_logger = util.getModuleLogger(__name__)
|
|
|
|
|
|
#===============================================================================
|
|
# Tool functions
|
|
#===============================================================================
|
|
|
|
def generateLockToken():
|
|
return "opaquelocktoken:" + str(hex(random.getrandbits(256)))
|
|
|
|
|
|
def normalizeLockRoot(path):
|
|
# Normalize root: /foo/bar
|
|
assert path
|
|
if type(path) is unicode:
|
|
path = path.encode("utf-8")
|
|
path = "/" + path.strip("/")
|
|
return path
|
|
|
|
|
|
def isLockExpired(lock):
|
|
expire = float(lock["expire"])
|
|
return expire >= 0 and expire < time.time()
|
|
|
|
|
|
def lockString(lockDict):
|
|
"""Return readable rep."""
|
|
if not lockDict:
|
|
return "Lock: None"
|
|
|
|
if lockDict["expire"] < 0:
|
|
expire = "Infinite (%s)" % (lockDict["expire"])
|
|
else:
|
|
expire = "%s (in %s seconds)" % (util.getLogTime(lockDict["expire"]),
|
|
lockDict["expire"] - time.time())
|
|
|
|
return "Lock(<%s..>, '%s', %s, %s, depth-%s, until %s" % (
|
|
lockDict.get("token","?"*30)[18:22], # first 4 significant token characters
|
|
lockDict.get("root"),
|
|
lockDict.get("principal"),
|
|
lockDict.get("scope"),
|
|
lockDict.get("depth"),
|
|
expire,
|
|
)
|
|
|
|
|
|
def validateLock(lock):
|
|
assert type(lock["root"]) is str
|
|
assert lock["root"].startswith("/")
|
|
assert lock["type"] == "write"
|
|
assert lock["scope"] in ("shared", "exclusive")
|
|
assert lock["depth"] in ("0", "infinity")
|
|
assert type(lock["owner"]) is str
|
|
# raises TypeError:
|
|
timeout = float(lock["timeout"])
|
|
assert timeout > 0 or timeout == -1, "timeout must be positive or -1"
|
|
assert type(lock["principal"]) is str
|
|
if "token" in lock:
|
|
assert type(lock["token"]) is str
|
|
|
|
|
|
#===============================================================================
|
|
# LockManager
|
|
#===============================================================================
|
|
class LockManager(object):
|
|
"""
|
|
Implements locking functionality using a custom storage layer.
|
|
|
|
"""
|
|
LOCK_TIME_OUT_DEFAULT = 604800 # 1 week, in seconds
|
|
|
|
def __init__(self, storage):
|
|
"""
|
|
storage:
|
|
LockManagerStorage object
|
|
"""
|
|
assert hasattr(storage, "getLockList")
|
|
self._lock = ReadWriteLock()
|
|
self.storage = storage
|
|
self.storage.open()
|
|
|
|
|
|
def __del__(self):
|
|
self.storage.close()
|
|
|
|
|
|
def __repr__(self):
|
|
return "%s(%r)" % (self.__class__.__name__, self.storage)
|
|
|
|
|
|
def _dump(self, msg="", out=None):
|
|
if out is None:
|
|
out = sys.stdout
|
|
|
|
urlDict = {} # { <url>: [<tokenlist>] }
|
|
ownerDict = {} # { <LOCKOWNER>: [<tokenlist>] }
|
|
userDict = {} # { <LOCKUSER>: [<tokenlist>] }
|
|
tokenDict = {} # { <token>: <LOCKURLS> }
|
|
|
|
print >>out, "%s: %s" % (self, msg)
|
|
|
|
for lock in self.storage.getLockList("/", includeRoot=True,
|
|
includeChildren=True,
|
|
tokenOnly=False):
|
|
tok = lock["token"]
|
|
tokenDict[tok] = lockString(lock)
|
|
userDict.setdefault(lock["principal"], []).append(tok)
|
|
ownerDict.setdefault(lock["owner"], []).append(tok)
|
|
urlDict.setdefault(lock["root"], []).append(tok)
|
|
|
|
# assert ("URL2TOKEN:" + v["root"]) in self._dict, "Inconsistency: missing URL2TOKEN:%s" % v["root"]
|
|
# assert v["token"] in self._dict["URL2TOKEN:" + v["root"]], "Inconsistency: missing token %s in URL2TOKEN:%s" % (v["token"], v["root"])
|
|
|
|
print >>out, "Locks:"
|
|
pprint(tokenDict, indent=0, width=255)
|
|
if tokenDict:
|
|
print >>out, "Locks by URL:"
|
|
pprint(urlDict, indent=4, width=255, stream=out)
|
|
print >>out, "Locks by principal:"
|
|
pprint(userDict, indent=4, width=255, stream=out)
|
|
print >>out, "Locks by owner:"
|
|
pprint(ownerDict, indent=4, width=255, stream=out)
|
|
|
|
|
|
def _generateLock(self, principal,
|
|
locktype, lockscope, lockdepth, lockowner, path, timeout):
|
|
"""Acquire lock and return lockDict.
|
|
|
|
principal
|
|
Name of the principal.
|
|
locktype
|
|
Must be 'write'.
|
|
lockscope
|
|
Must be 'shared' or 'exclusive'.
|
|
lockdepth
|
|
Must be '0' or 'infinity'.
|
|
lockowner
|
|
String identifying the owner.
|
|
path
|
|
Resource URL.
|
|
timeout
|
|
Seconds to live
|
|
|
|
This function does NOT check, if the new lock creates a conflict!
|
|
"""
|
|
if timeout is None:
|
|
timeout = LockManager.LOCK_TIME_OUT_DEFAULT
|
|
elif timeout < 0:
|
|
timeout = -1
|
|
|
|
lockDict = {"root": path,
|
|
"type": locktype,
|
|
"scope": lockscope,
|
|
"depth": lockdepth,
|
|
"owner": lockowner,
|
|
"timeout": timeout,
|
|
"principal": principal,
|
|
}
|
|
#
|
|
self.storage.create(path, lockDict)
|
|
return lockDict
|
|
|
|
|
|
def acquire(self, url, locktype, lockscope, lockdepth, lockowner, timeout,
|
|
principal, tokenList):
|
|
"""Check for permissions and acquire a lock.
|
|
|
|
On success return new lock dictionary.
|
|
On error raise a DAVError with an embedded DAVErrorCondition.
|
|
"""
|
|
url = normalizeLockRoot(url)
|
|
self._lock.acquireWrite()
|
|
try:
|
|
# Raises DAVError on conflict:
|
|
self._checkLockPermission(url, locktype, lockscope, lockdepth, tokenList, principal)
|
|
return self._generateLock(principal, locktype, lockscope, lockdepth, lockowner, url, timeout)
|
|
finally:
|
|
self._lock.release()
|
|
|
|
|
|
def refresh(self, token, timeout=None):
|
|
"""Set new timeout for lock, if existing and valid."""
|
|
if timeout is None:
|
|
timeout = LockManager.LOCK_TIME_OUT_DEFAULT
|
|
return self.storage.refresh(token, timeout)
|
|
|
|
|
|
def getLock(self, token, key=None):
|
|
"""Return lockDict, or None, if not found or invalid.
|
|
|
|
Side effect: if lock is expired, it will be purged and None is returned.
|
|
|
|
key:
|
|
name of lock attribute that will be returned instead of a dictionary.
|
|
"""
|
|
assert key in (None, "type", "scope", "depth", "owner", "root",
|
|
"timeout", "principal", "token")
|
|
lock = self.storage.get(token)
|
|
if key is None or lock is None:
|
|
return lock
|
|
return lock[key]
|
|
|
|
|
|
def release(self, token):
|
|
"""Delete lock."""
|
|
self.storage.delete(token)
|
|
|
|
|
|
def isTokenLockedByUser(self, token, principal):
|
|
"""Return True, if <token> exists, is valid, and bound to <principal>."""
|
|
return self.getLock(token, "principal") == principal
|
|
|
|
|
|
# def getUrlLockList(self, url, principal=None):
|
|
def getUrlLockList(self, url):
|
|
"""Return list of lockDict, if <url> is protected by at least one direct, valid lock.
|
|
|
|
Side effect: expired locks for this url are purged.
|
|
"""
|
|
url = normalizeLockRoot(url)
|
|
lockList = self.storage.getLockList(url, includeRoot=True,
|
|
includeChildren=False,
|
|
tokenOnly=False)
|
|
return lockList
|
|
|
|
|
|
def getIndirectUrlLockList(self, url, principal=None):
|
|
"""Return a list of valid lockDicts, that protect <path> directly or indirectly.
|
|
|
|
If a principal is given, only locks owned by this principal are returned.
|
|
Side effect: expired locks for this path and all parents are purged.
|
|
"""
|
|
url = normalizeLockRoot(url)
|
|
lockList = []
|
|
u = url
|
|
while u:
|
|
ll = self.storage.getLockList(u, includeRoot=True,
|
|
includeChildren=False,
|
|
tokenOnly=False)
|
|
for l in ll:
|
|
if u != url and l["depth"] != "infinity":
|
|
continue # We only consider parents with Depth: infinity
|
|
# TODO: handle shared locks in some way?
|
|
# if l["scope"] == "shared" and lockscope == "shared" and principal != l["principal"]:
|
|
# continue # Only compatible with shared locks by other users
|
|
if principal is None or principal == l["principal"]:
|
|
lockList.append(l)
|
|
u = util.getUriParent(u)
|
|
return lockList
|
|
|
|
|
|
def isUrlLocked(self, url):
|
|
"""Return True, if url is directly locked."""
|
|
lockList = self.getUrlLockList(url)
|
|
return len(lockList) > 0
|
|
|
|
|
|
def isUrlLockedByToken(self, url, locktoken):
|
|
"""Check, if url (or any of it's parents) is locked by locktoken."""
|
|
lockUrl = self.getLock(locktoken, "root")
|
|
return lockUrl and util.isEqualOrChildUri(lockUrl, url)
|
|
|
|
|
|
def removeAllLocksFromUrl(self, url):
|
|
self._lock.acquireWrite()
|
|
try:
|
|
lockList = self.getUrlLockList(url)
|
|
for lock in lockList:
|
|
self.release(lock["token"])
|
|
finally:
|
|
self._lock.release()
|
|
|
|
|
|
def _checkLockPermission(self, url, locktype, lockscope, lockdepth,
|
|
tokenList, principal):
|
|
"""Check, if <principal> can lock <url>, otherwise raise an error.
|
|
|
|
If locking <url> would create a conflict, DAVError(HTTP_LOCKED) is
|
|
raised. An embedded DAVErrorCondition contains the conflicting resource.
|
|
|
|
@see http://www.webdav.org/specs/rfc4918.html#lock-model
|
|
|
|
- Parent locks WILL NOT be conflicting, if they are depth-0.
|
|
- Exclusive depth-infinity parent locks WILL be conflicting, even if
|
|
they are owned by <principal>.
|
|
- Child locks WILL NOT be conflicting, if we request a depth-0 lock.
|
|
- Exclusive child locks WILL be conflicting, even if they are owned by
|
|
<principal>. (7.7)
|
|
- It is not enough to check whether a lock is owned by <principal>, but
|
|
also the token must be passed with the request. (Because <principal>
|
|
may run two different applications on his client.)
|
|
- <principal> cannot lock-exclusive, if he holds a parent shared-lock.
|
|
(This would only make sense, if he was the only shared-lock holder.)
|
|
- TODO: litmus tries to acquire a shared lock on one resource twice
|
|
(locks: 27 'double_sharedlock') and fails, when we return HTTP_LOCKED.
|
|
So we allow multi shared locks on a resource even for the same
|
|
principal.
|
|
|
|
@param url: URL that shall be locked
|
|
@param locktype: "write"
|
|
@param lockscope: "shared"|"exclusive"
|
|
@param lockdepth: "0"|"infinity"
|
|
@param tokenList: list of lock tokens, that the user submitted in If: header
|
|
@param principal: name of the principal requesting a lock
|
|
|
|
@return: None (or raise)
|
|
"""
|
|
assert locktype == "write"
|
|
assert lockscope in ("shared", "exclusive")
|
|
assert lockdepth in ("0", "infinity")
|
|
|
|
_logger.debug("checkLockPermission(%s, %s, %s, %s)" % (url, lockscope, lockdepth, principal))
|
|
|
|
# Error precondition to collect conflicting URLs
|
|
errcond = DAVErrorCondition(PRECONDITION_CODE_LockConflict)
|
|
|
|
self._lock.acquireRead()
|
|
try:
|
|
# Check url and all parents for conflicting locks
|
|
u = url
|
|
while u:
|
|
ll = self.getUrlLockList(u)
|
|
for l in ll:
|
|
_logger.debug(" check parent %s, %s" % (u, lockString(l)))
|
|
if u != url and l["depth"] != "infinity":
|
|
# We only consider parents with Depth: infinity
|
|
continue
|
|
elif l["scope"] == "shared" and lockscope == "shared":
|
|
# Only compatible with shared locks (even by same principal)
|
|
continue
|
|
# Lock conflict
|
|
_logger.debug(" -> DENIED due to locked parent %s" % lockString(l))
|
|
errcond.add_href(l["root"])
|
|
u = util.getUriParent(u)
|
|
|
|
if lockdepth == "infinity":
|
|
# Check child URLs for conflicting locks
|
|
childLocks = self.storage.getLockList(url,
|
|
includeRoot=False,
|
|
includeChildren=True,
|
|
tokenOnly=False)
|
|
|
|
for l in childLocks:
|
|
assert util.isChildUri(url, l["root"])
|
|
# if util.isChildUri(url, l["root"]):
|
|
_logger.debug(" -> DENIED due to locked child %s" % lockString(l))
|
|
errcond.add_href(l["root"])
|
|
finally:
|
|
self._lock.release()
|
|
|
|
# If there were conflicts, raise HTTP_LOCKED for <url>, and pass
|
|
# conflicting resource with 'no-conflicting-lock' precondition
|
|
if len(errcond.hrefs) > 0:
|
|
raise DAVError(HTTP_LOCKED, errcondition=errcond)
|
|
return
|
|
|
|
|
|
def checkWritePermission(self, url, depth, tokenList, principal):
|
|
"""Check, if <principal> can modify <url>, otherwise raise HTTP_LOCKED.
|
|
|
|
If modifying <url> is prevented by a lock, DAVError(HTTP_LOCKED) is
|
|
raised. An embedded DAVErrorCondition contains the conflicting locks.
|
|
|
|
<url> may be modified by <principal>, if it is not currently locked
|
|
directly or indirectly (i.e. by a locked parent).
|
|
For depth-infinity operations, <url> also must not have locked children.
|
|
|
|
It is not enough to check whether a lock is owned by <principal>, but
|
|
also the token must be passed with the request. Because <principal> may
|
|
run two different applications.
|
|
|
|
See http://www.webdav.org/specs/rfc4918.html#lock-model
|
|
http://www.webdav.org/specs/rfc4918.html#rfc.section.7.4
|
|
|
|
TODO: verify assumptions:
|
|
- Parent locks WILL NOT be conflicting, if they are depth-0.
|
|
- Exclusive child locks WILL be conflicting, even if they are owned by <principal>.
|
|
|
|
@param url: URL that shall be modified, created, moved, or deleted
|
|
@param depth: "0"|"infinity"
|
|
@param tokenList: list of lock tokens, that the principal submitted in If: header
|
|
@param principal: name of the principal requesting a lock
|
|
|
|
@return: None or raise error
|
|
"""
|
|
assert depth in ("0", "infinity")
|
|
_logger.debug("checkWritePermission(%s, %s, %s, %s)" % (url, depth, tokenList, principal))
|
|
|
|
# Error precondition to collect conflicting URLs
|
|
errcond = DAVErrorCondition(PRECONDITION_CODE_LockConflict)
|
|
|
|
self._lock.acquireRead()
|
|
try:
|
|
# Check url and all parents for conflicting locks
|
|
u = url
|
|
while u:
|
|
ll = self.getUrlLockList(u)
|
|
_logger.debug(" checking %s" % u)
|
|
for l in ll:
|
|
_logger.debug(" l=%s" % lockString(l))
|
|
if u != url and l["depth"] != "infinity":
|
|
# We only consider parents with Depth: inifinity
|
|
continue
|
|
elif principal == l["principal"] and l["token"] in tokenList:
|
|
# User owns this lock
|
|
continue
|
|
else:
|
|
# Token is owned by principal, but not passed with lock list
|
|
_logger.debug(" -> DENIED due to locked parent %s" % lockString(l))
|
|
errcond.add_href(l["root"])
|
|
u = util.getUriParent(u)
|
|
|
|
if depth == "infinity":
|
|
# Check child URLs for conflicting locks
|
|
childLocks = self.storage.getLockList(url,
|
|
includeRoot=False,
|
|
includeChildren=True,
|
|
tokenOnly=False)
|
|
|
|
for l in childLocks:
|
|
assert util.isChildUri(url, l["root"])
|
|
# if util.isChildUri(url, l["root"]):
|
|
_logger.debug(" -> DENIED due to locked child %s" % lockString(l))
|
|
errcond.add_href(l["root"])
|
|
finally:
|
|
self._lock.release()
|
|
|
|
# If there were conflicts, raise HTTP_LOCKED for <url>, and pass
|
|
# conflicting resource with 'no-conflicting-lock' precondition
|
|
if len(errcond.hrefs) > 0:
|
|
raise DAVError(HTTP_LOCKED, errcondition=errcond)
|
|
return
|
|
|
|
|
|
#===============================================================================
|
|
# test
|
|
#===============================================================================
|
|
def test():
|
|
# l = ShelveLockManager("wsgidav-locks.shelve")
|
|
# l._lazyOpen()
|
|
# l._dump()
|
|
# l.generateLock("martin", "", lockscope, lockdepth, lockowner, lockroot, timeout)
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
test()
|