moat-kv 0.70.23__py3-none-any.whl → 0.71.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. build/lib/docs/source/conf.py +201 -0
  2. build/lib/examples/pathify.py +45 -0
  3. build/lib/moat/kv/__init__.py +19 -0
  4. build/lib/moat/kv/_cfg.yaml +93 -0
  5. build/lib/moat/kv/_main.py +91 -0
  6. build/lib/moat/kv/actor/__init__.py +98 -0
  7. build/lib/moat/kv/actor/deletor.py +139 -0
  8. build/lib/moat/kv/auth/__init__.py +444 -0
  9. build/lib/moat/kv/auth/_test.py +166 -0
  10. build/lib/moat/kv/auth/password.py +234 -0
  11. build/lib/moat/kv/auth/root.py +58 -0
  12. build/lib/moat/kv/backend/__init__.py +67 -0
  13. build/lib/moat/kv/backend/mqtt.py +71 -0
  14. build/lib/moat/kv/client.py +1025 -0
  15. build/lib/moat/kv/code.py +236 -0
  16. build/lib/moat/kv/codec.py +11 -0
  17. build/lib/moat/kv/command/__init__.py +1 -0
  18. build/lib/moat/kv/command/acl.py +180 -0
  19. build/lib/moat/kv/command/auth.py +261 -0
  20. build/lib/moat/kv/command/code.py +293 -0
  21. build/lib/moat/kv/command/codec.py +186 -0
  22. build/lib/moat/kv/command/data.py +265 -0
  23. build/lib/moat/kv/command/dump/__init__.py +143 -0
  24. build/lib/moat/kv/command/error.py +149 -0
  25. build/lib/moat/kv/command/internal.py +248 -0
  26. build/lib/moat/kv/command/job.py +433 -0
  27. build/lib/moat/kv/command/log.py +53 -0
  28. build/lib/moat/kv/command/server.py +114 -0
  29. build/lib/moat/kv/command/type.py +201 -0
  30. build/lib/moat/kv/config.py +46 -0
  31. build/lib/moat/kv/data.py +216 -0
  32. build/lib/moat/kv/errors.py +561 -0
  33. build/lib/moat/kv/exceptions.py +126 -0
  34. build/lib/moat/kv/mock/__init__.py +101 -0
  35. build/lib/moat/kv/mock/mqtt.py +159 -0
  36. build/lib/moat/kv/mock/tracer.py +63 -0
  37. build/lib/moat/kv/model.py +1069 -0
  38. build/lib/moat/kv/obj/__init__.py +646 -0
  39. build/lib/moat/kv/obj/command.py +241 -0
  40. build/lib/moat/kv/runner.py +1347 -0
  41. build/lib/moat/kv/server.py +2809 -0
  42. build/lib/moat/kv/types.py +513 -0
  43. ci/rtd-requirements.txt +4 -0
  44. ci/test-requirements.txt +7 -0
  45. ci/travis.sh +96 -0
  46. debian/.gitignore +7 -0
  47. debian/changelog +1435 -0
  48. debian/control +43 -0
  49. debian/moat-kv/usr/lib/python3/dist-packages/docs/source/conf.py +201 -0
  50. debian/moat-kv/usr/lib/python3/dist-packages/examples/pathify.py +45 -0
  51. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/__init__.py +19 -0
  52. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/_cfg.yaml +93 -0
  53. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/_main.py +91 -0
  54. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/actor/__init__.py +98 -0
  55. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/actor/deletor.py +139 -0
  56. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/__init__.py +444 -0
  57. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/_test.py +166 -0
  58. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/password.py +234 -0
  59. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/root.py +58 -0
  60. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/backend/__init__.py +67 -0
  61. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/backend/mqtt.py +71 -0
  62. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/client.py +1025 -0
  63. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/code.py +236 -0
  64. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/codec.py +11 -0
  65. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/__init__.py +1 -0
  66. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/acl.py +180 -0
  67. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/auth.py +261 -0
  68. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/code.py +293 -0
  69. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/codec.py +186 -0
  70. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/data.py +265 -0
  71. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/dump/__init__.py +143 -0
  72. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/error.py +149 -0
  73. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/internal.py +248 -0
  74. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/job.py +433 -0
  75. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/log.py +53 -0
  76. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/server.py +114 -0
  77. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/type.py +201 -0
  78. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/config.py +46 -0
  79. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/data.py +216 -0
  80. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/errors.py +561 -0
  81. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/exceptions.py +126 -0
  82. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/__init__.py +101 -0
  83. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/mqtt.py +159 -0
  84. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/tracer.py +63 -0
  85. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/model.py +1069 -0
  86. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/obj/__init__.py +646 -0
  87. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/obj/command.py +241 -0
  88. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/runner.py +1347 -0
  89. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/server.py +2809 -0
  90. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/types.py +513 -0
  91. debian/moat-kv.postinst +3 -0
  92. debian/rules +20 -0
  93. debian/source/format +1 -0
  94. debian/watch +4 -0
  95. docs/Makefile +20 -0
  96. docs/make.bat +36 -0
  97. docs/source/TODO.rst +61 -0
  98. docs/source/_static/.gitkeep +0 -0
  99. docs/source/acls.rst +80 -0
  100. docs/source/auth.rst +84 -0
  101. docs/source/client_protocol.rst +456 -0
  102. docs/source/code.rst +341 -0
  103. docs/source/command_line.rst +1187 -0
  104. docs/source/common_protocol.rst +47 -0
  105. docs/source/conf.py +201 -0
  106. docs/source/debugging.rst +70 -0
  107. docs/source/extend.rst +37 -0
  108. docs/source/history.rst +36 -0
  109. docs/source/index.rst +75 -0
  110. docs/source/model.rst +54 -0
  111. docs/source/overview.rst +83 -0
  112. docs/source/related.rst +89 -0
  113. docs/source/server_protocol.rst +450 -0
  114. docs/source/startup.rst +31 -0
  115. docs/source/translator.rst +244 -0
  116. docs/source/tutorial.rst +711 -0
  117. docs/source/v3.rst +168 -0
  118. examples/code/transform.scale.yml +21 -0
  119. examples/code/transform.switch.yml +82 -0
  120. examples/code/transform.timeslot.yml +63 -0
  121. examples/pathify.py +45 -0
  122. moat/kv/_cfg.yaml +2 -6
  123. moat/kv/actor/__init__.py +98 -0
  124. moat/kv/actor/deletor.py +139 -0
  125. moat/kv/auth/__init__.py +444 -0
  126. moat/kv/auth/_test.py +166 -0
  127. moat/kv/auth/password.py +234 -0
  128. moat/kv/auth/root.py +58 -0
  129. moat/kv/backend/__init__.py +67 -0
  130. moat/kv/backend/mqtt.py +71 -0
  131. moat/kv/command/__init__.py +1 -0
  132. moat/kv/command/acl.py +180 -0
  133. moat/kv/command/auth.py +261 -0
  134. moat/kv/command/code.py +293 -0
  135. moat/kv/command/codec.py +186 -0
  136. moat/kv/command/data.py +265 -0
  137. moat/kv/command/dump/__init__.py +143 -0
  138. moat/kv/command/error.py +149 -0
  139. moat/kv/command/internal.py +248 -0
  140. moat/kv/command/job.py +433 -0
  141. moat/kv/command/log.py +53 -0
  142. moat/kv/command/server.py +114 -0
  143. moat/kv/command/type.py +201 -0
  144. moat/kv/mock/__init__.py +101 -0
  145. moat/kv/mock/mqtt.py +159 -0
  146. moat/kv/mock/tracer.py +63 -0
  147. moat/kv/obj/__init__.py +646 -0
  148. moat/kv/obj/command.py +241 -0
  149. {moat_kv-0.70.23.dist-info → moat_kv-0.71.0.dist-info}/METADATA +2 -5
  150. moat_kv-0.71.0.dist-info/RECORD +188 -0
  151. moat_kv-0.71.0.dist-info/top_level.txt +9 -0
  152. scripts/current +15 -0
  153. scripts/env +8 -0
  154. scripts/init +39 -0
  155. scripts/recover +17 -0
  156. scripts/rotate +33 -0
  157. scripts/run +29 -0
  158. scripts/run-all +10 -0
  159. scripts/run-any +10 -0
  160. scripts/run-single +15 -0
  161. scripts/success +4 -0
  162. systemd/moat-kv-recover.service +21 -0
  163. systemd/moat-kv-rotate.service +20 -0
  164. systemd/moat-kv-rotate.timer +10 -0
  165. systemd/moat-kv-run-all.service +26 -0
  166. systemd/moat-kv-run-all@.service +25 -0
  167. systemd/moat-kv-run-any.service +26 -0
  168. systemd/moat-kv-run-any@.service +25 -0
  169. systemd/moat-kv-run-single.service +26 -0
  170. systemd/moat-kv-run-single@.service +25 -0
  171. systemd/moat-kv.service +27 -0
  172. systemd/postinst +7 -0
  173. systemd/sysusers +3 -0
  174. moat_kv-0.70.23.dist-info/RECORD +0 -19
  175. moat_kv-0.70.23.dist-info/top_level.txt +0 -1
  176. {moat_kv-0.70.23.dist-info → moat_kv-0.71.0.dist-info}/WHEEL +0 -0
  177. {moat_kv-0.70.23.dist-info → moat_kv-0.71.0.dist-info}/licenses/LICENSE.txt +0 -0
@@ -0,0 +1,234 @@
1
+ #
2
+ """
3
+ Password-based auth method.
4
+
5
+ Does not limit anything, allows everything.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import nacl.secret
11
+
12
+ from ..client import Client, NoData
13
+ from ..exceptions import AuthFailedError
14
+ from ..model import Entry
15
+ from ..server import StreamCommand
16
+ from . import (
17
+ BaseClientAuth,
18
+ BaseClientAuthMaker,
19
+ BaseServerAuthMaker,
20
+ RootServerUser,
21
+ null_client_login,
22
+ null_server_login,
23
+ )
24
+
25
+
26
+ def load(typ: str, *, make: bool = False, server: bool):
27
+ if typ == "client":
28
+ if server:
29
+ return null_server_login
30
+ else:
31
+ return null_client_login
32
+ if typ != "user":
33
+ raise NotImplementedError("This module only handles users")
34
+ if server:
35
+ if make:
36
+ return ServerUserMaker
37
+ else:
38
+ return ServerUser
39
+ else:
40
+ if make:
41
+ return ClientUserMaker
42
+ else:
43
+ return ClientUser
44
+
45
+
46
+ async def pack_pwd(client, password, length):
47
+ """Client side: encrypt password"""
48
+ secret = await client.dh_secret(length=length)
49
+ from hashlib import sha256
50
+
51
+ pwd = sha256(password).digest()
52
+ box = nacl.secret.SecretBox(secret)
53
+ pwd = box.encrypt(pwd)
54
+ return pwd
55
+
56
+
57
+ async def unpack_pwd(client, password):
58
+ """Server side: extract password"""
59
+ box = nacl.secret.SecretBox(client.dh_key)
60
+ pwd = box.decrypt(password)
61
+ return pwd
62
+ # TODO check with Argon2
63
+
64
+
65
+ class ServerUserMaker(BaseServerAuthMaker):
66
+ _name = None
67
+ _aux = None
68
+ password: str = None
69
+
70
+ @property
71
+ def ident(self):
72
+ return self._name
73
+
74
+ @classmethod
75
+ async def recv(cls, cmd, data):
76
+ self = cls()
77
+ self._name = data["ident"]
78
+ self._aux = data.get("aux")
79
+ pwd = data.get("password")
80
+ pwd = await unpack_pwd(cmd.client, pwd)
81
+
82
+ # TODO use Argon2 to re-hash this
83
+ self.password = pwd
84
+ return self
85
+
86
+ async def send(self, cmd):
87
+ return # nothing to do, we don't share the hash
88
+
89
+ @classmethod
90
+ def load(cls, data):
91
+ self = super().load(data)
92
+ self._name = data.path[-1]
93
+ return self
94
+
95
+ def save(self):
96
+ res = super().save()
97
+ res["password"] = self.password
98
+ return res
99
+
100
+
101
+ class ServerUser(RootServerUser):
102
+ @classmethod
103
+ def load(cls, data: Entry):
104
+ """Create a ServerUser object from existing stored data"""
105
+ self = super().load(data)
106
+ self._name = data.name
107
+ return self
108
+
109
+ async def auth(self, cmd: StreamCommand, data):
110
+ """Verify that @data authenticates this user."""
111
+ await super().auth(cmd, data)
112
+
113
+ pwd = await unpack_pwd(cmd.client, data.password)
114
+ if pwd != self.password: # pylint: disable=no-member
115
+ # pylint: disable=no-member
116
+ raise AuthFailedError("Password hashes do not match", self._name)
117
+
118
+
119
+ class ClientUserMaker(BaseClientAuthMaker):
120
+ gen_schema = dict(
121
+ type="object",
122
+ additionalProperties=True,
123
+ properties=dict(
124
+ name=dict(type="string", minLength=1, pattern="^[a-zA-Z][a-zA-Z0-9_]*$"),
125
+ password=dict(type="string", minLength=5),
126
+ ),
127
+ required=["name", "password"],
128
+ )
129
+ mod_schema = dict(
130
+ type="object",
131
+ additionalProperties=True,
132
+ properties=dict(password=dict(type="string", minLength=5)),
133
+ # required=[],
134
+ )
135
+ _name = None
136
+ _pass = None
137
+ _length = 1024
138
+
139
+ @property
140
+ def ident(self):
141
+ return self._name
142
+
143
+ # Overly-complicated methods of exchanging the user name
144
+
145
+ @classmethod
146
+ def build(cls, user, _initial=True):
147
+ self = super().build(user, _initial=_initial)
148
+ self._name = user["name"]
149
+ if "password" in user:
150
+ self._pass = user["password"].encode("utf-8")
151
+ return self
152
+
153
+ @classmethod
154
+ async def recv(cls, client: Client, ident: str, _kind: str = "user", _initial=True):
155
+ """Read a record representing a user from the server."""
156
+ m = await client._request(
157
+ action="auth_get",
158
+ typ=cls._auth_method,
159
+ kind=_kind,
160
+ ident=ident,
161
+ nchain=0 if _initial else 2,
162
+ )
163
+ # just to verify that the user exists
164
+ # There's no reason to send the password hash back
165
+ self = cls(_initial=_initial)
166
+ self._name = m.name
167
+ try:
168
+ self._chain = m.chain
169
+ except AttributeError:
170
+ pass
171
+ return self
172
+
173
+ async def send(self, client: Client, _kind="user", **msg): # pylint: disable=unused-argument,arguments-differ
174
+ """Send a record representing this user to the server."""
175
+ if self._pass is not None:
176
+ msg["password"] = await pack_pwd(client, self._pass, self._length)
177
+
178
+ await client._request(
179
+ action="auth_set",
180
+ ident=self._name,
181
+ typ=type(self)._auth_method,
182
+ kind=_kind,
183
+ chain=self._chain,
184
+ **msg,
185
+ )
186
+
187
+ def export(self):
188
+ """Return the data required to re-create the user via :meth:`build`."""
189
+ res = super().export()
190
+ res["name"] = self._name
191
+ return res
192
+
193
+
194
+ class ClientUser(BaseClientAuth):
195
+ schema = dict(
196
+ type="object",
197
+ additionalProperties=True,
198
+ properties=dict(
199
+ name=dict(type="string", minLength=1, pattern="^[a-zA-Z][a-zA-Z0-9_]*$"),
200
+ password=dict(type="string", minLength=5),
201
+ ),
202
+ required=["name", "password"],
203
+ )
204
+ _name = None
205
+ _pass = None
206
+ _length = 1024
207
+
208
+ @property
209
+ def ident(self):
210
+ return self._name
211
+
212
+ @classmethod
213
+ def build(cls, user):
214
+ self = super().build(user)
215
+ self._name = user["name"]
216
+ self._pass = user["password"].encode("utf-8")
217
+ return self
218
+
219
+ async def auth(self, client: Client, chroot=()):
220
+ """
221
+ Authorizes this user with the server.
222
+ """
223
+ try:
224
+ pw = await pack_pwd(client, self._pass, self._length)
225
+ await client._request(
226
+ action="auth",
227
+ typ=self._auth_method,
228
+ iter=False,
229
+ ident=self.ident,
230
+ password=pw,
231
+ **self.auth_data(),
232
+ )
233
+ except NoData:
234
+ pass
moat/kv/auth/root.py ADDED
@@ -0,0 +1,58 @@
1
+ #
2
+ """
3
+ Null auth method.
4
+
5
+ Does not limit anything, allows everything.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from . import (
11
+ BaseClientAuth,
12
+ BaseClientAuthMaker,
13
+ BaseServerAuthMaker,
14
+ RootServerUser,
15
+ null_client_login,
16
+ null_server_login,
17
+ )
18
+
19
+
20
+ def load(typ: str, *, make: bool = False, server: bool):
21
+ if typ == "client":
22
+ if server:
23
+ return null_server_login
24
+ else:
25
+ return null_client_login
26
+ if typ != "user":
27
+ raise NotImplementedError("This module only handles users")
28
+ if server:
29
+ if make:
30
+ return ServerUserMaker
31
+ else:
32
+ return ServerUser
33
+ else:
34
+ if make:
35
+ return ClientUserMaker
36
+ else:
37
+ return ClientUser
38
+
39
+
40
+ class ServerUserMaker(BaseServerAuthMaker):
41
+ schema = {"type": "object", "additionalProperties": False}
42
+
43
+
44
+ class ServerUser(RootServerUser):
45
+ schema = {"type": "object", "additionalProperties": False}
46
+
47
+
48
+ class ClientUserMaker(BaseClientAuthMaker):
49
+ gen_schema = {"type": "object", "additionalProperties": False}
50
+ mod_schema = {"type": "object", "additionalProperties": False}
51
+
52
+ @property
53
+ def ident(self):
54
+ return "*"
55
+
56
+
57
+ class ClientUser(BaseClientAuth):
58
+ schema = {"type": "object", "additionalProperties": False}
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+ from abc import ABCMeta, abstractmethod
3
+ from contextlib import asynccontextmanager
4
+
5
+ import anyio
6
+
7
+ __all__ = ["get_backend", "Backend"]
8
+
9
+
10
+ class Backend(metaclass=ABCMeta):
11
+ def __init__(self, tg):
12
+ self._tg = tg
13
+ self._njobs = 0
14
+ self._ended = None
15
+
16
+ @abstractmethod
17
+ @asynccontextmanager
18
+ async def connect(self, *a, **k):
19
+ """
20
+ This async context manager returns a connection.
21
+ """
22
+
23
+ async def aclose(self):
24
+ """
25
+ Force-close the connection.
26
+ """
27
+ self._tg.cancel_scope.cancel()
28
+ if self._njobs > 0:
29
+ with anyio.move_on_after(2):
30
+ await self._ended.wait()
31
+
32
+ async def spawn(self, p, *a, **kw):
33
+ async def _run(p, a, kw, *, task_status):
34
+ if self._ended is None:
35
+ self._ended = anyio.Event()
36
+ self._njobs += 1
37
+ task_status.started()
38
+ try:
39
+ return await p(*a, **kw)
40
+ finally:
41
+ self._njobs -= 1
42
+ if not self._njobs:
43
+ self._ended.set()
44
+ self._ended = None
45
+
46
+ return await self._tg.start(_run, p, a, kw)
47
+
48
+ @abstractmethod
49
+ @asynccontextmanager
50
+ async def monitor(self, *topic):
51
+ """
52
+ Return an async iterator that listens to this topic.
53
+ """
54
+
55
+ @abstractmethod
56
+ async def send(self, *topic, payload):
57
+ """
58
+ Send this payload to this topic.
59
+ """
60
+
61
+
62
+ def get_backend(name):
63
+ from importlib import import_module
64
+
65
+ if "." not in name:
66
+ name = "moat.kv.backend." + name
67
+ return import_module(name).connect
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+ import logging
3
+ from contextlib import asynccontextmanager
4
+
5
+ import anyio
6
+ from moat.mqtt.client import MQTTClient
7
+ from moat.mqtt.codecs import NoopCodec
8
+ from moat.util import NotGiven
9
+
10
+ from . import Backend
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class MqttMessage:
16
+ def __init__(self, topic, payload):
17
+ self.topic = topic
18
+ self.payload = payload
19
+
20
+
21
+ class MqttBackend(Backend):
22
+ client = None
23
+
24
+ @asynccontextmanager
25
+ async def connect(self, *a, **kw):
26
+ codec = kw.pop("codec", NotGiven)
27
+ C = MQTTClient(self._tg, codec=codec)
28
+ try:
29
+ await C.connect(*a, **kw)
30
+ self.client = C
31
+ yield self
32
+ finally:
33
+ self.client = None
34
+ with anyio.CancelScope(shield=True):
35
+ await self.aclose()
36
+ await C.disconnect()
37
+
38
+ @asynccontextmanager
39
+ async def monitor(self, *topic):
40
+ topic = "/".join(str(x) for x in topic)
41
+ logger.info("Monitor %s start", topic)
42
+ try:
43
+ async with self.client.subscription(topic) as sub:
44
+
45
+ async def sub_get(sub):
46
+ async for msg in sub:
47
+ yield MqttMessage(msg.topic.split("/"), msg.data)
48
+
49
+ yield sub_get(sub)
50
+ except anyio.get_cancelled_exc_class():
51
+ raise
52
+ except BaseException as exc:
53
+ logger.exception("Monitor %s end: %r", topic, exc)
54
+ raise
55
+ else:
56
+ logger.info("Monitor %s end", topic)
57
+
58
+ def send(self, *topic, payload): # pylint: disable=invalid-overridden-method
59
+ """
60
+ Send this payload to this topic.
61
+ """
62
+ # client.publish is also async, pass-thru
63
+ return self.client.publish("/".join(str(x) for x in topic), message=payload)
64
+
65
+
66
+ @asynccontextmanager
67
+ async def connect(**kw):
68
+ async with anyio.create_task_group() as tg:
69
+ c = MqttBackend(tg)
70
+ async with c.connect(**kw):
71
+ yield c
@@ -0,0 +1 @@
1
+ # empty
moat/kv/command/acl.py ADDED
@@ -0,0 +1,180 @@
1
+ # command line interface
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+
6
+ import asyncclick as click
7
+ from moat.util import P, Path, yprint
8
+
9
+ from moat.kv.data import data_get
10
+
11
+ ACL = set("rwdcxena")
12
+ # read, write, delete, create, access, enumerate
13
+
14
+
15
+ @click.group() # pylint: disable=undefined-variable
16
+ async def cli():
17
+ """Manage ACLs. Usage: … acl …"""
18
+ pass
19
+
20
+
21
+ @cli.command("list")
22
+ @click.pass_obj
23
+ async def list_(obj):
24
+ """List ACLs."""
25
+ res = await obj.client._request(
26
+ action="enum_internal",
27
+ path=("acl",),
28
+ iter=False,
29
+ nchain=obj.meta,
30
+ empty=True,
31
+ )
32
+ yprint(res if obj.meta else res.result, stream=obj.stdout)
33
+
34
+
35
+ @cli.command()
36
+ @click.option(
37
+ "-d",
38
+ "--as-dict",
39
+ default=None,
40
+ help="Structure as dictionary. The argument is the key to use "
41
+ "for values. Default: return as list",
42
+ )
43
+ @click.argument("name", nargs=1)
44
+ @click.argument("path", nargs=1)
45
+ @click.pass_obj
46
+ async def dump(obj, name, path, as_dict):
47
+ """Dump a complete (or partial) ACL."""
48
+ path = P(path)
49
+ await data_get(obj, Path("acl", name, path), internal=True, as_dict=as_dict)
50
+
51
+
52
+ @cli.command()
53
+ @click.argument("name", nargs=1)
54
+ @click.argument("path", nargs=1)
55
+ @click.pass_obj
56
+ async def get(obj, name, path):
57
+ """Read an ACL.
58
+
59
+ This command does not test a path. Use "… acl test …" for that.
60
+ """
61
+ path = P(path)
62
+ if not len(path):
63
+ raise click.UsageError("You need a non-empty path.")
64
+ res = await obj.client._request(
65
+ action="get_internal",
66
+ path=("acl", name) + path,
67
+ iter=False,
68
+ nchain=obj.meta,
69
+ )
70
+
71
+ if not obj.meta:
72
+ try:
73
+ res = res.value
74
+ except KeyError:
75
+ if obj.debug:
76
+ print("No value.", file=sys.stderr)
77
+ return
78
+ yprint(res, stream=obj.stdout)
79
+
80
+
81
+ @cli.command(name="set")
82
+ @click.option(
83
+ "-a",
84
+ "--acl",
85
+ default="+x",
86
+ help="The value to set. Start with '+' to add, '-' to remove rights.",
87
+ )
88
+ @click.argument("name", nargs=1)
89
+ @click.argument("path", nargs=1)
90
+ @click.pass_obj
91
+ async def set_(obj, acl, name, path):
92
+ """Set or change an ACL."""
93
+
94
+ path = P(path)
95
+ if not len(path):
96
+ raise click.UsageError("You need a non-empty path.")
97
+ if len(acl) > 1 and acl[0] in "+-":
98
+ mode = acl[0]
99
+ acl = acl[1:]
100
+ else:
101
+ mode = "x"
102
+ acl = set(acl)
103
+
104
+ if acl - ACL:
105
+ raise click.UsageError(f"You're trying to set an unknown ACL flag: {acl - ACL!r}")
106
+
107
+ res = await obj.client._request(
108
+ action="get_internal",
109
+ path=("acl", name) + path,
110
+ iter=False,
111
+ nchain=3 if obj.meta else 1,
112
+ )
113
+ ov = set(res.get("value", ""))
114
+ if ov - ACL:
115
+ print(f"Warning: original ACL contains unknown: {ov - acl!r}", file=sys.stderr)
116
+
117
+ if mode == "-" and not acl:
118
+ res = await obj.client._request(
119
+ action="delete_internal",
120
+ path=("acl", name) + path,
121
+ iter=False,
122
+ chain=res.chain,
123
+ )
124
+ v = "-"
125
+
126
+ else:
127
+ if mode == "+":
128
+ v = ov + acl
129
+ elif mode == "-":
130
+ v = ov - acl
131
+ else:
132
+ v = acl
133
+ res = await obj.client._request(
134
+ action="set_internal",
135
+ path=("acl", name) + path,
136
+ value="".join(v),
137
+ iter=False,
138
+ chain=res.get("chain", None),
139
+ )
140
+
141
+ if obj.meta:
142
+ res = {
143
+ "old": "".join(ov),
144
+ "new": "".join(v),
145
+ "chain": res.chain,
146
+ "tock": res.tock,
147
+ }
148
+ yprint(res, stream=obj.stdout)
149
+ else:
150
+ res = {"old": "".join(ov), "new": "".join(v)}
151
+ yprint(res, stream=obj.stdout)
152
+
153
+
154
+ @cli.command()
155
+ @click.option("-m", "--mode", default=None, help="Mode letter to test.")
156
+ @click.option("-a", "--acl", default=None, help="ACL to test. Default: current")
157
+ @click.argument("path", nargs=1)
158
+ @click.pass_obj
159
+ async def test(obj, path, acl, mode):
160
+ """Test which ACL entry matches a path"""
161
+ path = P(path)
162
+ if not len(path):
163
+ raise click.UsageError("You need a non-empty path.")
164
+
165
+ if mode is not None and len(mode) != 1:
166
+ raise click.UsageError("Mode must be one letter.")
167
+ res = await obj.client._request(
168
+ action="test_acl",
169
+ path=path,
170
+ iter=False,
171
+ nchain=obj.meta,
172
+ **({} if mode is None else {"mode": mode}),
173
+ **({} if acl is None else {"acl": acl}),
174
+ )
175
+ if obj.meta:
176
+ yprint(res, stream=obj.stdout)
177
+ elif isinstance(res.access, bool):
178
+ print("+" if res.access else "-", file=obj.stdout)
179
+ else:
180
+ print(res.access, file=obj.stdout)