Skip to content

Commit ece7f6c

Browse files
author
Jean-Louis Fuchs
authored
Merge pull request #195 from ganwell/t/post_ca88
feat: add post-write-hook/callback
2 parents 1cae47f + 80cbf38 commit ece7f6c

File tree

6 files changed

+104
-56
lines changed

6 files changed

+104
-56
lines changed

README.md

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,13 @@ an unauthorized third party.
4141
refresh the lock. Consequently, we leverage this mechanism to also refresh the
4242
token.
4343

44-
**pre_write_hook**: We provide an optional pre-write-hook feature that enhances
45-
the API's capabilities by adding a versioning system. The hook is invoked with
46-
the token as an argument **prior** to the document being saved using WebDAV-PUT.
47-
The API has the ability to verify this token and subsequently create a new
48-
version of the document. Once the hook operation is successfully completed,
49-
Manabi takes over and saves the document.
50-
51-
A post-write-hook is not implemented, if you need one, please open an issue.
44+
**cb_hook_config**: We provide an optional write-hooks/callbacks feature that
45+
enhances the API's capabilities by adding a versioning system. The pre-write-
46+
hook/callback is invoked with the token as an argument **prior** to the document
47+
being saved using WebDAV-PUT. The API has the ability to verify this token and
48+
subsequently create a new version of the document. Once the hook operation is
49+
successfully completed, Manabi takes over and saves the document. Manabi then
50+
continues and invokes the post-write-hook/callback.
5251

5352
## Install
5453

@@ -102,12 +101,20 @@ a computer, tokens should be expired by the time an adversary gets them.
102101
```python
103102
from manabi import ManabiDAVApp
104103

104+
# All hooks and callbacks are optional
105+
cb_hook_config = CallbackHookConfig(
106+
pre_write_hook=_pre_write_hook,
107+
pre_write_callback=_pre_write_callback,
108+
post_write_hook=_post_write_hook,
109+
post_write_callback=_post_write_callback,
110+
)
111+
105112
postgres_dsn = "dbname=manabi user=manabi host=localhost password=manabi"
106113
config = {
107114
"mount_path": "/dav",
108115
"lock_storage": ManabiDbLockStorage(refresh, postgres_dsn),
109116
"provider_mapping": {
110-
"/": ManabiProvider(settings.MEDIA_ROOT, pre_write_hook="http://127.0.0.1/hook"),
117+
"/": ManabiProvider(settings.MEDIA_ROOT, cb_hook_config=cb_hook_config),
111118
},
112119
"middleware_stack": [
113120
WsgiDavDebugFilter,

manabi/conftest.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,21 +66,25 @@ def lock_storage(request, postgres_dsn):
6666

6767

6868
@pytest.fixture()
69-
def pre_write_hook():
69+
def write_hooks():
7070
try:
7171
mock._pre_write_hook = "http://127.0.0.1/pre_write_hook"
72+
mock._post_write_hook = "http://127.0.0.1/post_write_hook"
7273
yield
7374
finally:
7475
mock._pre_write_hook = None
76+
mock._post_write_hook = None
7577

7678

7779
@pytest.fixture()
78-
def pre_write_callback():
80+
def write_callback():
7981
try:
8082
mock._pre_write_callback = mock.check_token
83+
mock._post_write_callback = mock.check_token
8184
yield mock.check_token
8285
finally:
8386
mock._pre_write_callback = None
87+
mock._post_write_callback = None
8488

8589

8690
@pytest.fixture()

manabi/filesystem.py

Lines changed: 57 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from pathlib import Path
22
from typing import Any, Dict, Optional
33

4+
from attr import dataclass
45
from wsgidav.dav_error import HTTP_FORBIDDEN, DAVError
56
from wsgidav.fs_dav_provider import FileResource, FilesystemProvider, FolderResource
67

78
from .token import Token
8-
from .type_alias import PreWriteType
9-
from .util import requests_session
9+
from .type_alias import WriteType
10+
from .util import cattrib, requests_session
1011

1112

1213
class ManabiFolderResource(FolderResource):
@@ -51,18 +52,26 @@ def set_last_modified(self, dest_path, time_stamp, dry_run):
5152
raise DAVError(HTTP_FORBIDDEN)
5253

5354

55+
@dataclass
56+
class CallbackHookConfig:
57+
pre_write_hook: Optional[str] = cattrib(Optional[str], default=None)
58+
pre_write_callback: Optional[WriteType] = cattrib(Optional[WriteType], default=None)
59+
post_write_hook: Optional[str] = cattrib(Optional[str], default=None)
60+
post_write_callback: Optional[WriteType] = cattrib(
61+
Optional[WriteType], default=None
62+
)
63+
64+
5465
class ManabiFileResource(FileResource):
5566
def __init__(
5667
self,
5768
path,
5869
environ,
5970
file_path,
6071
*,
61-
pre_write_hook: Optional[str] = None,
62-
pre_write_callback: Optional[PreWriteType] = None,
72+
cb_hook_config: Optional[CallbackHookConfig] = None,
6373
):
64-
self._pre_write_hook = pre_write_hook
65-
self._pre_write_callback = pre_write_callback
74+
self._cb_config = cb_hook_config
6675
self._token = environ["manabi.token"]
6776
super().__init__(path, environ, file_path)
6877

@@ -78,22 +87,46 @@ def support_recursive_move(self, dest_path):
7887
def move_recursive(self, dest_path):
7988
raise DAVError(HTTP_FORBIDDEN)
8089

81-
def begin_write(self, *, content_type):
90+
def get_token_and_config(self):
8291
token = self._token
83-
if token:
84-
pre_hook = self._pre_write_hook
85-
pre_callback = self._pre_write_callback
86-
87-
if pre_hook:
88-
session = requests_session()
89-
res = session.post(pre_hook, data=token.encode())
90-
if res.status_code != 200:
91-
raise DAVError(HTTP_FORBIDDEN)
92-
if pre_callback:
93-
if not pre_callback(token):
94-
raise DAVError(HTTP_FORBIDDEN)
95-
# The hook returned and hopefully created a new version.
96-
# Now we can save.
92+
config = self._cb_config
93+
return token and config, token, config
94+
95+
def process_post_write_hooks(self):
96+
ok, token, config = self.get_token_and_config()
97+
if not ok:
98+
return
99+
post_hook = config.post_write_hook
100+
post_callback = config.post_write_callback
101+
102+
if post_hook:
103+
session = requests_session()
104+
session.post(post_hook, data=token.encode())
105+
if post_callback:
106+
post_callback(token)
107+
108+
def end_write(self, *, with_errors):
109+
if not with_errors:
110+
self.process_post_write_hooks()
111+
112+
def process_pre_write_hooks(self):
113+
ok, token, config = self.get_token_and_config()
114+
if not ok:
115+
return
116+
pre_hook = config.pre_write_hook
117+
pre_callback = config.pre_write_callback
118+
119+
if pre_hook:
120+
session = requests_session()
121+
res = session.post(pre_hook, data=token.encode())
122+
if res.status_code != 200:
123+
raise DAVError(HTTP_FORBIDDEN)
124+
if pre_callback:
125+
if not pre_callback(token):
126+
raise DAVError(HTTP_FORBIDDEN)
127+
128+
def begin_write(self, *, content_type):
129+
self.process_pre_write_hooks()
97130
return super().begin_write(content_type=content_type)
98131

99132

@@ -104,11 +137,9 @@ def __init__(
104137
*,
105138
readonly=False,
106139
shadow=None,
107-
pre_write_hook: Optional[str] = None,
108-
pre_write_callback: Optional[PreWriteType] = None,
140+
cb_hook_config: Optional[CallbackHookConfig] = None,
109141
):
110-
self._pre_write_hook = pre_write_hook
111-
self._pre_write_callback = pre_write_callback
142+
self._cb_hook_config = cb_hook_config
112143
super().__init__(root_folder, readonly=readonly, shadow=shadow)
113144

114145
def get_resource_inst(self, path: str, environ: Dict[str, Any]):
@@ -128,8 +159,7 @@ def get_resource_inst(self, path: str, environ: Dict[str, Any]):
128159
path,
129160
environ,
130161
fp,
131-
pre_write_hook=self._pre_write_hook,
132-
pre_write_callback=self._pre_write_callback,
162+
cb_hook_config=self._cb_hook_config,
133163
)
134164
else:
135165
return None

manabi/filesystem_test.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@ def test_get_and_put(tamper, expect_status, config: Dict[str, Any], server):
3030
],
3131
)
3232
def test_get_and_put_hooked(
33-
hook_status, expect_status, pre_write_hook, config: Dict[str, Any], server
33+
hook_status, expect_status, write_hooks, config: Dict[str, Any], server
3434
):
35-
assert config["pre_write_hook"] == "http://127.0.0.1/pre_write_hook"
36-
with mock.with_pre_write_hook(config, hook_status):
35+
cb_hook_config = config["cb_hook_config"]
36+
assert cb_hook_config.pre_write_hook == "http://127.0.0.1/pre_write_hook"
37+
assert cb_hook_config.post_write_hook == "http://127.0.0.1/post_write_hook"
38+
with mock.with_write_hooks(config, hook_status):
3739
req = mock.make_req(config)
3840
res = requests.get(req)
3941
assert res.status_code == 200
@@ -45,11 +47,12 @@ def test_get_and_put_hooked(
4547
def test_get_and_put_called(
4648
callback_return,
4749
expect_status,
48-
pre_write_callback,
50+
write_callback,
4951
config: Dict[str, Any],
5052
server,
5153
):
52-
assert config["pre_write_callback"] == pre_write_callback
54+
assert config["cb_hook_config"].pre_write_callback == write_callback
55+
assert config["cb_hook_config"].post_write_callback == write_callback
5356
req = mock.make_req(config)
5457
res = requests.get(req)
5558
assert res.status_code == 200

manabi/mock.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@
1818

1919
from . import ManabiDAVApp, lock as mlock
2020
from .auth import ManabiAuthenticator
21-
from .filesystem import ManabiProvider
21+
from .filesystem import CallbackHookConfig, ManabiProvider
2222
from .lock import ManabiDbLockStorage as ManabiDbLockStorageOrig
2323
from .log import HeaderLogger, ResponseLogger
2424
from .token import Config, Key, Token, now
25-
from .type_alias import PreWriteType
25+
from .type_alias import WriteType
2626
from .util import get_rfc1123_time
2727

2828
_servers: Dict[Tuple[str, int], wsgi.Server] = dict()
@@ -58,11 +58,12 @@ def check_token(token: Token) -> bool:
5858

5959

6060
_pre_write_hook: Optional[str] = None
61+
_post_write_hook: Optional[str] = None
6162

6263

6364
@contextmanager
64-
def with_pre_write_hook(config: Dict[str, Any], status_code=None):
65-
if not _pre_write_hook:
65+
def with_write_hooks(config: Dict[str, Any], status_code=None):
66+
if not (_pre_write_hook and _post_write_hook):
6667
return
6768

6869
cfg = Config.from_dictionary(config)
@@ -79,10 +80,12 @@ def check_token(request, context):
7980

8081
with requests_mock.Mocker(real_http=True) as m:
8182
m.post(_pre_write_hook, text=check_token)
83+
m.post(_post_write_hook, text=check_token)
8284
yield
8385

8486

85-
_pre_write_callback: Optional[PreWriteType] = None
87+
_pre_write_callback: Optional[WriteType] = None
88+
_post_write_callback: Optional[WriteType] = None
8689

8790

8891
def get_config(server_dir: Path, lock_storage: Union[Path, str]):
@@ -93,19 +96,20 @@ def get_config(server_dir: Path, lock_storage: Union[Path, str]):
9396
lock_obj = mlock.ManabiShelfLockLockStorage(refresh, lock_storage)
9497
else:
9598
lock_obj = mlock.ManabiDbLockStorage(refresh, lock_storage)
99+
cb_hook_config = CallbackHookConfig(
100+
pre_write_hook=_pre_write_hook,
101+
pre_write_callback=_pre_write_callback,
102+
post_write_hook=_post_write_hook,
103+
post_write_callback=_post_write_callback,
104+
)
96105
return {
97-
"pre_write_hook": _pre_write_hook,
98-
"pre_write_callback": _pre_write_callback,
106+
"cb_hook_config": cb_hook_config,
99107
"host": "0.0.0.0",
100108
"port": 8081,
101109
"mount_path": "/dav",
102110
"lock_storage": lock_obj,
103111
"provider_mapping": {
104-
"/": ManabiProvider(
105-
server_dir,
106-
pre_write_hook=_pre_write_hook,
107-
pre_write_callback=_pre_write_callback,
108-
),
112+
"/": ManabiProvider(server_dir, cb_hook_config=cb_hook_config),
109113
},
110114
"middleware_stack": [
111115
HeaderLogger,

manabi/type_alias.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@
2121
bool,
2222
]
2323
OptionalProp = Optional[PropType]
24-
PreWriteType = Callable[["Token"], bool]
24+
WriteType = Callable[["Token"], bool]

0 commit comments

Comments
 (0)