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