meerschaum 2.9.4__py3-none-any.whl → 3.0.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.
- meerschaum/__init__.py +5 -2
- meerschaum/_internal/__init__.py +1 -0
- meerschaum/_internal/arguments/_parse_arguments.py +4 -4
- meerschaum/_internal/arguments/_parser.py +33 -4
- meerschaum/_internal/cli/__init__.py +6 -0
- meerschaum/_internal/cli/daemons.py +103 -0
- meerschaum/_internal/cli/entry.py +220 -0
- meerschaum/_internal/cli/workers.py +435 -0
- meerschaum/_internal/docs/index.py +48 -2
- meerschaum/_internal/entry.py +50 -14
- meerschaum/_internal/shell/Shell.py +121 -29
- meerschaum/_internal/shell/__init__.py +4 -1
- meerschaum/_internal/static.py +359 -0
- meerschaum/_internal/term/TermPageHandler.py +1 -2
- meerschaum/_internal/term/__init__.py +40 -6
- meerschaum/_internal/term/tools.py +33 -8
- meerschaum/actions/__init__.py +6 -4
- meerschaum/actions/api.py +53 -13
- meerschaum/actions/attach.py +1 -0
- meerschaum/actions/bootstrap.py +8 -8
- meerschaum/actions/delete.py +4 -2
- meerschaum/actions/edit.py +171 -25
- meerschaum/actions/login.py +8 -8
- meerschaum/actions/register.py +143 -6
- meerschaum/actions/reload.py +22 -5
- meerschaum/actions/restart.py +14 -0
- meerschaum/actions/show.py +184 -31
- meerschaum/actions/start.py +166 -17
- meerschaum/actions/stop.py +38 -2
- meerschaum/actions/sync.py +7 -2
- meerschaum/actions/tag.py +9 -8
- meerschaum/actions/verify.py +5 -8
- meerschaum/api/__init__.py +45 -15
- meerschaum/api/_events.py +46 -4
- meerschaum/api/_oauth2.py +162 -9
- meerschaum/api/_tokens.py +102 -0
- meerschaum/api/dash/__init__.py +0 -3
- meerschaum/api/dash/callbacks/__init__.py +1 -0
- meerschaum/api/dash/callbacks/custom.py +4 -3
- meerschaum/api/dash/callbacks/dashboard.py +228 -117
- meerschaum/api/dash/callbacks/jobs.py +14 -7
- meerschaum/api/dash/callbacks/login.py +10 -1
- meerschaum/api/dash/callbacks/pipes.py +194 -14
- meerschaum/api/dash/callbacks/plugins.py +0 -1
- meerschaum/api/dash/callbacks/register.py +10 -3
- meerschaum/api/dash/callbacks/settings/password_reset.py +2 -2
- meerschaum/api/dash/callbacks/tokens.py +389 -0
- meerschaum/api/dash/components.py +36 -15
- meerschaum/api/dash/jobs.py +1 -1
- meerschaum/api/dash/keys.py +35 -93
- meerschaum/api/dash/pages/__init__.py +2 -1
- meerschaum/api/dash/pages/dashboard.py +1 -20
- meerschaum/api/dash/pages/{job.py → jobs.py} +10 -7
- meerschaum/api/dash/pages/login.py +2 -2
- meerschaum/api/dash/pages/pipes.py +16 -5
- meerschaum/api/dash/pages/settings/password_reset.py +1 -1
- meerschaum/api/dash/pages/tokens.py +53 -0
- meerschaum/api/dash/pipes.py +438 -88
- meerschaum/api/dash/sessions.py +12 -0
- meerschaum/api/dash/tokens.py +603 -0
- meerschaum/api/dash/websockets.py +1 -1
- meerschaum/api/dash/webterm.py +18 -6
- meerschaum/api/models/__init__.py +23 -3
- meerschaum/api/models/_actions.py +22 -0
- meerschaum/api/models/_pipes.py +91 -7
- meerschaum/api/models/_tokens.py +81 -0
- meerschaum/api/resources/static/css/dash.css +16 -0
- meerschaum/api/resources/static/js/terminado.js +3 -0
- meerschaum/api/resources/static/js/xterm-addon-unicode11.js +2 -0
- meerschaum/api/resources/templates/termpage.html +13 -0
- meerschaum/api/routes/__init__.py +1 -0
- meerschaum/api/routes/_actions.py +3 -4
- meerschaum/api/routes/_connectors.py +3 -7
- meerschaum/api/routes/_jobs.py +26 -35
- meerschaum/api/routes/_login.py +120 -15
- meerschaum/api/routes/_misc.py +5 -10
- meerschaum/api/routes/_pipes.py +178 -143
- meerschaum/api/routes/_plugins.py +38 -28
- meerschaum/api/routes/_tokens.py +236 -0
- meerschaum/api/routes/_users.py +47 -35
- meerschaum/api/routes/_version.py +3 -3
- meerschaum/api/routes/_webterm.py +3 -3
- meerschaum/config/__init__.py +100 -30
- meerschaum/config/_default.py +132 -64
- meerschaum/config/_edit.py +38 -32
- meerschaum/config/_formatting.py +2 -0
- meerschaum/config/_patch.py +10 -8
- meerschaum/config/_paths.py +133 -13
- meerschaum/config/_read_config.py +87 -36
- meerschaum/config/_sync.py +6 -3
- meerschaum/config/_version.py +1 -1
- meerschaum/config/environment.py +262 -0
- meerschaum/config/stack/__init__.py +37 -15
- meerschaum/config/static.py +18 -0
- meerschaum/connectors/_Connector.py +11 -6
- meerschaum/connectors/__init__.py +41 -22
- meerschaum/connectors/api/_APIConnector.py +34 -6
- meerschaum/connectors/api/_actions.py +2 -2
- meerschaum/connectors/api/_jobs.py +12 -1
- meerschaum/connectors/api/_login.py +33 -7
- meerschaum/connectors/api/_misc.py +2 -2
- meerschaum/connectors/api/_pipes.py +23 -32
- meerschaum/connectors/api/_plugins.py +2 -2
- meerschaum/connectors/api/_request.py +1 -1
- meerschaum/connectors/api/_tokens.py +146 -0
- meerschaum/connectors/api/_users.py +70 -58
- meerschaum/connectors/instance/_InstanceConnector.py +83 -0
- meerschaum/connectors/instance/__init__.py +10 -0
- meerschaum/connectors/instance/_pipes.py +442 -0
- meerschaum/connectors/instance/_plugins.py +159 -0
- meerschaum/connectors/instance/_tokens.py +317 -0
- meerschaum/connectors/instance/_users.py +188 -0
- meerschaum/connectors/parse.py +5 -2
- meerschaum/connectors/sql/_SQLConnector.py +22 -5
- meerschaum/connectors/sql/_cli.py +12 -11
- meerschaum/connectors/sql/_create_engine.py +12 -168
- meerschaum/connectors/sql/_fetch.py +2 -18
- meerschaum/connectors/sql/_pipes.py +295 -278
- meerschaum/connectors/sql/_plugins.py +29 -0
- meerschaum/connectors/sql/_sql.py +47 -22
- meerschaum/connectors/sql/_users.py +36 -2
- meerschaum/connectors/sql/tables/__init__.py +254 -122
- meerschaum/connectors/valkey/_ValkeyConnector.py +5 -7
- meerschaum/connectors/valkey/_pipes.py +60 -31
- meerschaum/connectors/valkey/_plugins.py +2 -26
- meerschaum/core/Pipe/__init__.py +115 -85
- meerschaum/core/Pipe/_attributes.py +425 -124
- meerschaum/core/Pipe/_bootstrap.py +54 -24
- meerschaum/core/Pipe/_cache.py +555 -0
- meerschaum/core/Pipe/_clear.py +0 -11
- meerschaum/core/Pipe/_data.py +96 -68
- meerschaum/core/Pipe/_deduplicate.py +0 -13
- meerschaum/core/Pipe/_delete.py +12 -21
- meerschaum/core/Pipe/_drop.py +11 -23
- meerschaum/core/Pipe/_dtypes.py +49 -19
- meerschaum/core/Pipe/_edit.py +14 -4
- meerschaum/core/Pipe/_fetch.py +1 -1
- meerschaum/core/Pipe/_index.py +8 -14
- meerschaum/core/Pipe/_show.py +5 -5
- meerschaum/core/Pipe/_sync.py +123 -204
- meerschaum/core/Pipe/_verify.py +4 -4
- meerschaum/{plugins → core/Plugin}/_Plugin.py +16 -12
- meerschaum/core/Plugin/__init__.py +1 -1
- meerschaum/core/Token/_Token.py +220 -0
- meerschaum/core/Token/__init__.py +12 -0
- meerschaum/core/User/_User.py +35 -10
- meerschaum/core/User/__init__.py +9 -1
- meerschaum/core/__init__.py +1 -0
- meerschaum/jobs/_Executor.py +88 -4
- meerschaum/jobs/_Job.py +149 -38
- meerschaum/jobs/__init__.py +3 -2
- meerschaum/jobs/systemd.py +8 -3
- meerschaum/models/__init__.py +35 -0
- meerschaum/models/pipes.py +247 -0
- meerschaum/models/tokens.py +38 -0
- meerschaum/models/users.py +26 -0
- meerschaum/plugins/__init__.py +301 -88
- meerschaum/plugins/bootstrap.py +510 -4
- meerschaum/utils/_get_pipes.py +97 -30
- meerschaum/utils/daemon/Daemon.py +199 -43
- meerschaum/utils/daemon/FileDescriptorInterceptor.py +0 -1
- meerschaum/utils/daemon/RotatingFile.py +63 -36
- meerschaum/utils/daemon/StdinFile.py +53 -13
- meerschaum/utils/daemon/__init__.py +47 -6
- meerschaum/utils/daemon/_names.py +6 -3
- meerschaum/utils/dataframe.py +480 -82
- meerschaum/utils/debug.py +49 -19
- meerschaum/utils/dtypes/__init__.py +478 -37
- meerschaum/utils/dtypes/sql.py +369 -29
- meerschaum/utils/formatting/__init__.py +5 -2
- meerschaum/utils/formatting/_jobs.py +1 -1
- meerschaum/utils/formatting/_pipes.py +52 -50
- meerschaum/utils/formatting/_pprint.py +1 -0
- meerschaum/utils/formatting/_shell.py +44 -18
- meerschaum/utils/misc.py +268 -186
- meerschaum/utils/packages/__init__.py +25 -40
- meerschaum/utils/packages/_packages.py +42 -34
- meerschaum/utils/pipes.py +213 -0
- meerschaum/utils/process.py +2 -2
- meerschaum/utils/prompt.py +175 -144
- meerschaum/utils/schedule.py +2 -1
- meerschaum/utils/sql.py +135 -49
- meerschaum/utils/threading.py +42 -0
- meerschaum/utils/typing.py +1 -4
- meerschaum/utils/venv/_Venv.py +2 -2
- meerschaum/utils/venv/__init__.py +7 -7
- meerschaum/utils/warnings.py +19 -13
- {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/METADATA +94 -96
- meerschaum-3.0.0.dist-info/RECORD +289 -0
- {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/WHEEL +1 -1
- meerschaum-3.0.0.dist-info/licenses/NOTICE +2 -0
- meerschaum/api/models/_interfaces.py +0 -15
- meerschaum/api/models/_locations.py +0 -15
- meerschaum/api/models/_metrics.py +0 -15
- meerschaum/config/_environment.py +0 -145
- meerschaum/config/static/__init__.py +0 -186
- meerschaum-2.9.4.dist-info/RECORD +0 -263
- {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/top_level.txt +0 -0
- {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/zip-safe +0 -0
@@ -12,6 +12,8 @@ import shutil
|
|
12
12
|
import pathlib
|
13
13
|
import os
|
14
14
|
|
15
|
+
import meerschaum as mrsm
|
16
|
+
from meerschaum.core import User
|
15
17
|
from meerschaum.utils.typing import Optional, List, SuccessTuple, Any, Dict
|
16
18
|
|
17
19
|
from meerschaum.api import (
|
@@ -24,11 +26,14 @@ from meerschaum.api import (
|
|
24
26
|
private,
|
25
27
|
no_auth,
|
26
28
|
default_instance_keys,
|
29
|
+
ScopedAuth,
|
27
30
|
)
|
31
|
+
from meerschaum.api.models import SuccessTupleResponseModel
|
28
32
|
from meerschaum.api.tables import get_tables
|
29
33
|
from fastapi import File, UploadFile
|
30
34
|
from meerschaum.utils.packages import attempt_import
|
31
35
|
from meerschaum.core import Plugin
|
36
|
+
from meerschaum.utils.misc import filter_arguments
|
32
37
|
starlette_responses = attempt_import('starlette.responses', warn=False, lazy=False)
|
33
38
|
FileResponse = starlette_responses.FileResponse
|
34
39
|
|
@@ -38,22 +43,24 @@ plugins_endpoint = endpoints['plugins']
|
|
38
43
|
PLUGINS_INSTANCE_KEYS = default_instance_keys
|
39
44
|
|
40
45
|
|
41
|
-
@app.post(
|
46
|
+
@app.post(
|
47
|
+
plugins_endpoint + '/{name}',
|
48
|
+
tags=['Plugins'],
|
49
|
+
response_model=SuccessTupleResponseModel,
|
50
|
+
)
|
42
51
|
def register_plugin(
|
43
52
|
name: str,
|
44
53
|
version: str = None,
|
45
54
|
attributes: str = None,
|
46
55
|
archive: UploadFile = File(...),
|
47
|
-
curr_user = (
|
48
|
-
fastapi.Depends(manager) if not no_auth else None
|
49
|
-
),
|
56
|
+
curr_user = fastapi.Depends(manager),
|
50
57
|
) -> SuccessTuple:
|
51
58
|
"""
|
52
59
|
Register a plugin and save its archive file.
|
53
60
|
|
54
61
|
Parameters
|
55
62
|
----------
|
56
|
-
name: str
|
63
|
+
name: str
|
57
64
|
The name of the plugin.
|
58
65
|
|
59
66
|
version: str, default None
|
@@ -65,7 +72,7 @@ def register_plugin(
|
|
65
72
|
archive: UploadFile :
|
66
73
|
The archive file of the plugin.
|
67
74
|
|
68
|
-
curr_user:
|
75
|
+
curr_user: User
|
69
76
|
The logged-in user.
|
70
77
|
|
71
78
|
Returns
|
@@ -102,16 +109,15 @@ def register_plugin(
|
|
102
109
|
)
|
103
110
|
|
104
111
|
if curr_user is not None:
|
105
|
-
plugin_user_id = get_api_connector(PLUGINS_INSTANCE_KEYS).get_plugin_user_id(plugin)
|
106
|
-
curr_user_id = get_api_connector(PLUGINS_INSTANCE_KEYS).get_user_id(curr_user) if curr_user is not None else -1
|
112
|
+
plugin_user_id = get_api_connector(PLUGINS_INSTANCE_KEYS).get_plugin_user_id(plugin, debug=debug)
|
113
|
+
curr_user_id = get_api_connector(PLUGINS_INSTANCE_KEYS).get_user_id(curr_user, debug=debug) if curr_user is not None else -1
|
107
114
|
if plugin_user_id is not None and plugin_user_id != curr_user_id:
|
108
115
|
return False, f"User '{curr_user.username}' cannot edit plugin '{plugin}'."
|
109
116
|
plugin.user_id = curr_user_id
|
110
117
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
118
|
+
register_plugin = get_api_connector(PLUGINS_INSTANCE_KEYS).register_plugin
|
119
|
+
args, kwargs = filter_arguments(register_plugin, plugin, make_archive=False, debug=debug)
|
120
|
+
success, msg = register_plugin(*args, **kwargs)
|
115
121
|
if success:
|
116
122
|
archive_path = plugin.archive_path
|
117
123
|
temp_archive_path = pathlib.Path(str(archive_path) + '.tmp')
|
@@ -122,12 +128,14 @@ def register_plugin(
|
|
122
128
|
|
123
129
|
return success, msg
|
124
130
|
|
125
|
-
|
131
|
+
|
132
|
+
@app.get(
|
133
|
+
plugins_endpoint + '/{name}',
|
134
|
+
tags=['Plugins'],
|
135
|
+
)
|
126
136
|
def get_plugin(
|
127
137
|
name: str,
|
128
|
-
curr_user = (
|
129
|
-
fastapi.Depends(manager) if private else None
|
130
|
-
),
|
138
|
+
curr_user = fastapi.Depends(ScopedAuth(['plugins:read'])) if private else None,
|
131
139
|
) -> Any:
|
132
140
|
"""
|
133
141
|
Download a plugin's archive file.
|
@@ -141,9 +149,7 @@ def get_plugin(
|
|
141
149
|
@app.get(plugins_endpoint + '/{name}/attributes', tags=['Plugins'])
|
142
150
|
def get_plugin_attributes(
|
143
151
|
name: str,
|
144
|
-
curr_user = (
|
145
|
-
fastapi.Depends(manager) if private else None
|
146
|
-
),
|
152
|
+
curr_user = fastapi.Depends(ScopedAuth(['plugins:read'])) if private else None,
|
147
153
|
) -> Dict[str, Any]:
|
148
154
|
"""
|
149
155
|
Get a plugin's attributes.
|
@@ -155,9 +161,7 @@ def get_plugin_attributes(
|
|
155
161
|
def get_plugins(
|
156
162
|
user_id: Optional[int] = None,
|
157
163
|
search_term: Optional[str] = None,
|
158
|
-
curr_user = (
|
159
|
-
fastapi.Depends(manager) if private else None
|
160
|
-
),
|
164
|
+
curr_user = fastapi.Depends(ScopedAuth(['plugins:read'])) if private else None,
|
161
165
|
) -> List[str]:
|
162
166
|
"""
|
163
167
|
Get a list of plugins.
|
@@ -179,13 +183,15 @@ def get_plugins(
|
|
179
183
|
).get_plugins(user_id=user_id, search_term=search_term)
|
180
184
|
|
181
185
|
|
182
|
-
@app.delete(
|
186
|
+
@app.delete(
|
187
|
+
plugins_endpoint + '/{name}',
|
188
|
+
tags=['Plugins'],
|
189
|
+
response_model=SuccessTupleResponseModel,
|
190
|
+
)
|
183
191
|
def delete_plugin(
|
184
192
|
name: str,
|
185
|
-
curr_user = (
|
186
|
-
|
187
|
-
),
|
188
|
-
) -> SuccessTuple:
|
193
|
+
curr_user = fastapi.Depends(manager),
|
194
|
+
) -> SuccessTupleResponseModel:
|
189
195
|
"""
|
190
196
|
Delete a plugin and its archive file from the repository.
|
191
197
|
"""
|
@@ -196,7 +202,11 @@ def delete_plugin(
|
|
196
202
|
return False, f"Plugin '{plugin}' is not registered."
|
197
203
|
|
198
204
|
if curr_user is not None:
|
199
|
-
curr_user_id =
|
205
|
+
curr_user_id = (
|
206
|
+
get_api_connector(PLUGINS_INSTANCE_KEYS).get_user_id(curr_user)
|
207
|
+
if isinstance(curr_user, User)
|
208
|
+
else get_api_connector(PLUGINS_INSTANCE_KEYS).get_token_user_id(curr_user)
|
209
|
+
)
|
200
210
|
if plugin_user_id != curr_user_id:
|
201
211
|
return False, f"User '{curr_user.username}' cannot delete plugin '{plugin}'."
|
202
212
|
else:
|
@@ -0,0 +1,236 @@
|
|
1
|
+
#! /usr/bin/env python3
|
2
|
+
# vim:fenc=utf-8
|
3
|
+
|
4
|
+
"""
|
5
|
+
Define the API token routes.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import json
|
9
|
+
import uuid
|
10
|
+
|
11
|
+
import meerschaum as mrsm
|
12
|
+
from meerschaum.core import Token
|
13
|
+
from meerschaum.api import (
|
14
|
+
app,
|
15
|
+
fastapi,
|
16
|
+
debug,
|
17
|
+
no_auth,
|
18
|
+
manager,
|
19
|
+
get_api_connector,
|
20
|
+
endpoints,
|
21
|
+
)
|
22
|
+
from meerschaum.api.models import (
|
23
|
+
RegisterTokenResponseModel,
|
24
|
+
RegisterTokenRequestModel,
|
25
|
+
SuccessTupleResponseModel,
|
26
|
+
GetTokenResponseModel,
|
27
|
+
GetTokensResponseModel,
|
28
|
+
)
|
29
|
+
from meerschaum.api._tokens import get_current_token
|
30
|
+
from meerschaum.utils.dtypes import json_serialize_value, value_is_null
|
31
|
+
from meerschaum.utils.misc import is_uuid
|
32
|
+
|
33
|
+
tokens_endpoint = endpoints['tokens']
|
34
|
+
|
35
|
+
|
36
|
+
@app.get(
|
37
|
+
tokens_endpoint,
|
38
|
+
tags=['Tokens'],
|
39
|
+
response_model=GetTokensResponseModel,
|
40
|
+
)
|
41
|
+
def get_tokens(
|
42
|
+
labels: str = '',
|
43
|
+
curr_user=(fastapi.Depends(manager) if not no_auth else None),
|
44
|
+
):
|
45
|
+
"""
|
46
|
+
Return the tokens registered to the current user.
|
47
|
+
"""
|
48
|
+
_labels = None if not labels else labels.split(',')
|
49
|
+
tokens = get_api_connector().get_tokens(user=curr_user, labels=_labels, debug=debug)
|
50
|
+
return [
|
51
|
+
{
|
52
|
+
key: (None if value_is_null(val) else val)
|
53
|
+
for key, val in dict(token.to_model()).items()
|
54
|
+
if key != 'secret_hash'
|
55
|
+
}
|
56
|
+
for token in tokens
|
57
|
+
]
|
58
|
+
|
59
|
+
|
60
|
+
@app.post(
|
61
|
+
tokens_endpoint + '/register',
|
62
|
+
tags=['Tokens'],
|
63
|
+
response_model=RegisterTokenResponseModel,
|
64
|
+
)
|
65
|
+
def register_token(
|
66
|
+
request_model: RegisterTokenRequestModel,
|
67
|
+
curr_user=(fastapi.Depends(manager) if not no_auth else None),
|
68
|
+
) -> RegisterTokenResponseModel:
|
69
|
+
"""
|
70
|
+
Register a new Token, returning its secret (unable to be retrieved later).
|
71
|
+
"""
|
72
|
+
token = Token(
|
73
|
+
user=curr_user,
|
74
|
+
label=request_model.label,
|
75
|
+
expiration=request_model.expiration,
|
76
|
+
scopes=request_model.scopes,
|
77
|
+
instance=get_api_connector(),
|
78
|
+
)
|
79
|
+
token_id, token_secret = token.generate_credentials()
|
80
|
+
register_success, register_msg = token.register(debug=debug)
|
81
|
+
if not register_success:
|
82
|
+
raise fastapi.HTTPException(
|
83
|
+
status_code=409,
|
84
|
+
detail=f"Could not register new token:\n{register_msg}",
|
85
|
+
)
|
86
|
+
api_key = token.get_api_key()
|
87
|
+
|
88
|
+
return RegisterTokenResponseModel(
|
89
|
+
label=token.label,
|
90
|
+
secret=token_secret,
|
91
|
+
id=token_id,
|
92
|
+
api_key=api_key,
|
93
|
+
expiration=token.expiration,
|
94
|
+
)
|
95
|
+
|
96
|
+
|
97
|
+
@app.post(
|
98
|
+
tokens_endpoint + '/validate',
|
99
|
+
tags=['Tokens'],
|
100
|
+
response_model=SuccessTupleResponseModel,
|
101
|
+
)
|
102
|
+
def validate_api_key(
|
103
|
+
curr_token=(fastapi.Depends(get_current_token) if not no_auth else None),
|
104
|
+
) -> mrsm.SuccessTuple:
|
105
|
+
"""
|
106
|
+
Return a 200 if the given Authorization token (API key) is valid.
|
107
|
+
"""
|
108
|
+
return True, "Success"
|
109
|
+
|
110
|
+
|
111
|
+
@app.get(
|
112
|
+
tokens_endpoint + '/{token_id}',
|
113
|
+
tags=['Tokens'],
|
114
|
+
response_model=GetTokenResponseModel,
|
115
|
+
)
|
116
|
+
def get_token_model(
|
117
|
+
token_id: str,
|
118
|
+
curr_user=(fastapi.Depends(manager) if not no_auth else None)
|
119
|
+
):
|
120
|
+
"""
|
121
|
+
Return the token model's fields.
|
122
|
+
"""
|
123
|
+
if not is_uuid(token_id):
|
124
|
+
raise fastapi.HTTPException(
|
125
|
+
status_code=400,
|
126
|
+
detail="Invalid token ID.",
|
127
|
+
)
|
128
|
+
real_token_id = uuid.UUID(token_id)
|
129
|
+
conn = get_api_connector()
|
130
|
+
token_model = conn.get_token_model(real_token_id)
|
131
|
+
if token_model is None:
|
132
|
+
raise fastapi.HTTPException(
|
133
|
+
status_code=404,
|
134
|
+
detail="Token does not exist.",
|
135
|
+
)
|
136
|
+
|
137
|
+
curr_user_id = get_api_connector().get_user_id(curr_user, debug=debug) if curr_user is not None else None
|
138
|
+
if token_model.user_id and token_model.user_id != curr_user_id:
|
139
|
+
curr_user_type = get_api_connector().get_user_type(curr_user, debug=debug)
|
140
|
+
if curr_user_type != 'admin':
|
141
|
+
raise fastapi.HTTPException(
|
142
|
+
status_code=403,
|
143
|
+
detail="Cannot edit another user's token.",
|
144
|
+
)
|
145
|
+
|
146
|
+
payload = {
|
147
|
+
key: (None if value_is_null(val) else val)
|
148
|
+
for key, val in dict(token_model).items()
|
149
|
+
if key != 'secret_hash'
|
150
|
+
}
|
151
|
+
return fastapi.Response(
|
152
|
+
json.dumps(payload, default=json_serialize_value),
|
153
|
+
media_type='application/json',
|
154
|
+
)
|
155
|
+
|
156
|
+
|
157
|
+
@app.post(
|
158
|
+
tokens_endpoint + '/{token_id}/edit',
|
159
|
+
tags=['Tokens'],
|
160
|
+
response_model=SuccessTupleResponseModel,
|
161
|
+
)
|
162
|
+
def edit_token(
|
163
|
+
token_id: str,
|
164
|
+
token_model: GetTokenResponseModel,
|
165
|
+
curr_user=(fastapi.Depends(manager) if not no_auth else None),
|
166
|
+
) -> mrsm.SuccessTuple:
|
167
|
+
"""
|
168
|
+
Edit the token's scope, expiration,, etc.
|
169
|
+
"""
|
170
|
+
if not is_uuid(token_id):
|
171
|
+
raise fastapi.HTTPException(
|
172
|
+
status_code=400,
|
173
|
+
detail="Token ID must be a UUID.",
|
174
|
+
)
|
175
|
+
|
176
|
+
token = Token(
|
177
|
+
id=uuid.UUID(token_id),
|
178
|
+
user=curr_user,
|
179
|
+
is_valid=token_model.is_valid,
|
180
|
+
creation=token_model.creation,
|
181
|
+
expiration=token_model.expiration,
|
182
|
+
scopes=token_model.scopes,
|
183
|
+
label=token_model.label,
|
184
|
+
instance=get_api_connector(),
|
185
|
+
)
|
186
|
+
return token.edit(debug=debug)
|
187
|
+
|
188
|
+
|
189
|
+
@app.post(
|
190
|
+
tokens_endpoint + '/{token_id}/invalidate',
|
191
|
+
tags=['Tokens'],
|
192
|
+
response_model=SuccessTupleResponseModel,
|
193
|
+
)
|
194
|
+
def invalidate_token(
|
195
|
+
token_id: str,
|
196
|
+
curr_user=(fastapi.Depends(manager) if not no_auth else None),
|
197
|
+
) -> mrsm.SuccessTuple:
|
198
|
+
"""
|
199
|
+
Invalidate the token, disabling it for future requests.
|
200
|
+
"""
|
201
|
+
if not is_uuid(token_id):
|
202
|
+
raise fastapi.HTTPException(
|
203
|
+
status_code=400,
|
204
|
+
detail="Token ID must be a UUID.",
|
205
|
+
)
|
206
|
+
|
207
|
+
_token_id = uuid.UUID(token_id)
|
208
|
+
return get_api_connector().invalidate_token(
|
209
|
+
Token(id=_token_id, instance=get_api_connector()),
|
210
|
+
debug=debug,
|
211
|
+
)
|
212
|
+
|
213
|
+
|
214
|
+
@app.delete(
|
215
|
+
tokens_endpoint + '/{token_id}',
|
216
|
+
tags=['Tokens'],
|
217
|
+
response_model=SuccessTupleResponseModel,
|
218
|
+
)
|
219
|
+
def delete_token(
|
220
|
+
token_id: str,
|
221
|
+
curr_user=(fastapi.Depends(manager) if not no_auth else None),
|
222
|
+
) -> mrsm.SuccessTuple:
|
223
|
+
"""
|
224
|
+
Delete the token from the instance.
|
225
|
+
"""
|
226
|
+
if not is_uuid(token_id):
|
227
|
+
raise fastapi.HTTPException(
|
228
|
+
status_code=400,
|
229
|
+
detail="Token ID must be a UUID.",
|
230
|
+
)
|
231
|
+
|
232
|
+
_token_id = uuid.UUID(token_id)
|
233
|
+
return get_api_connector().delete_token(
|
234
|
+
Token(id=_token_id, instance=get_api_connector()),
|
235
|
+
debug=debug,
|
236
|
+
)
|
meerschaum/api/routes/_users.py
CHANGED
@@ -3,26 +3,34 @@
|
|
3
3
|
# vim:fenc=utf-8
|
4
4
|
|
5
5
|
"""
|
6
|
-
Routes for managing users
|
6
|
+
Routes for managing users.
|
7
7
|
"""
|
8
8
|
|
9
9
|
from __future__ import annotations
|
10
10
|
|
11
|
+
from uuid import UUID
|
11
12
|
from meerschaum.utils.typing import (
|
12
13
|
Union, SuccessTuple, Any, Dict, List
|
13
14
|
)
|
14
15
|
|
15
|
-
from meerschaum.utils.packages import attempt_import
|
16
16
|
from meerschaum.api import (
|
17
|
-
fastapi,
|
18
|
-
|
19
|
-
|
17
|
+
fastapi,
|
18
|
+
app,
|
19
|
+
endpoints,
|
20
|
+
get_api_connector,
|
21
|
+
manager,
|
22
|
+
debug,
|
23
|
+
check_allow_chaining,
|
24
|
+
DISALLOW_CHAINING_MESSAGE,
|
25
|
+
no_auth,
|
26
|
+
private,
|
27
|
+
default_instance_keys,
|
28
|
+
ScopedAuth,
|
20
29
|
)
|
21
|
-
from meerschaum.utils.misc import string_to_dict
|
30
|
+
from meerschaum.utils.misc import string_to_dict, is_uuid
|
22
31
|
from meerschaum.config import get_config
|
23
32
|
from meerschaum.core import User
|
24
33
|
|
25
|
-
sqlalchemy = attempt_import('sqlalchemy', lazy=False)
|
26
34
|
users_endpoint = endpoints['users']
|
27
35
|
|
28
36
|
import fastapi
|
@@ -34,23 +42,27 @@ USERS_INSTANCE_KEYS = default_instance_keys
|
|
34
42
|
@app.get(users_endpoint + "/me", tags=['Users'])
|
35
43
|
def read_current_user(
|
36
44
|
curr_user = (
|
37
|
-
fastapi.Depends(
|
45
|
+
fastapi.Depends(ScopedAuth(['users:read'])) if not no_auth else None
|
38
46
|
),
|
39
47
|
) -> Dict[str, Union[str, int, None, Dict[str, Any]]]:
|
40
48
|
"""
|
41
49
|
Get information about the currently logged-in user.
|
42
50
|
"""
|
51
|
+
user_id = (
|
52
|
+
get_api_connector(USERS_INSTANCE_KEYS).get_user_id(curr_user)
|
53
|
+
if curr_user is not None
|
54
|
+
else None
|
55
|
+
)
|
56
|
+
if is_uuid(str(user_id)):
|
57
|
+
user_id = str(user_id)
|
58
|
+
|
43
59
|
return {
|
44
60
|
'username': (
|
45
61
|
curr_user.username
|
46
62
|
if curr_user is not None
|
47
63
|
else 'no_auth'
|
48
64
|
),
|
49
|
-
'user_id':
|
50
|
-
get_api_connector(USERS_INSTANCE_KEYS).get_user_id(curr_user)
|
51
|
-
if curr_user is not None
|
52
|
-
else -1
|
53
|
-
),
|
65
|
+
'user_id': user_id,
|
54
66
|
'user_type': (
|
55
67
|
get_api_connector(USERS_INSTANCE_KEYS).get_user_type(curr_user)
|
56
68
|
if curr_user is not None
|
@@ -66,9 +78,7 @@ def read_current_user(
|
|
66
78
|
|
67
79
|
@app.get(users_endpoint, tags=['Users'])
|
68
80
|
def get_users(
|
69
|
-
curr_user = (
|
70
|
-
fastapi.Depends(manager) if private else None
|
71
|
-
),
|
81
|
+
curr_user = (fastapi.Depends(ScopedAuth(['users:read'])) if private else None),
|
72
82
|
) -> List[str]:
|
73
83
|
"""
|
74
84
|
Get a list of the registered users.
|
@@ -84,7 +94,7 @@ def register_user(
|
|
84
94
|
type: str = Form(None),
|
85
95
|
email: str = Form(None),
|
86
96
|
curr_user = (
|
87
|
-
fastapi.Depends(
|
97
|
+
fastapi.Depends(ScopedAuth(['users:register', 'users:write'])) if private else None
|
88
98
|
),
|
89
99
|
) -> SuccessTuple:
|
90
100
|
"""
|
@@ -114,7 +124,7 @@ def register_user(
|
|
114
124
|
"Register a normal user first, then edit the user from an authorized account, "
|
115
125
|
"or use a SQL connector instead."
|
116
126
|
)
|
117
|
-
user = User(username, password, type=type, email=email, attributes=attributes)
|
127
|
+
user = User(username, password, type=type, email=email, attributes=attributes, instance=get_api_connector(USERS_INSTANCE_KEYS))
|
118
128
|
return get_api_connector(USERS_INSTANCE_KEYS).register_user(user, debug=debug)
|
119
129
|
|
120
130
|
|
@@ -125,9 +135,7 @@ def edit_user(
|
|
125
135
|
type: str = Form(None),
|
126
136
|
email: str = Form(None),
|
127
137
|
attributes: str = Form(None),
|
128
|
-
curr_user = (
|
129
|
-
fastapi.Depends(manager) if not no_auth else None
|
130
|
-
),
|
138
|
+
curr_user = fastapi.Depends(ScopedAuth(['users:write'])),
|
131
139
|
) -> SuccessTuple:
|
132
140
|
"""
|
133
141
|
Edit an existing user.
|
@@ -138,47 +146,51 @@ def edit_user(
|
|
138
146
|
except Exception:
|
139
147
|
return False, "Invalid dictionary string received for attributes."
|
140
148
|
|
141
|
-
user = User(username, password, email=email, attributes=attributes)
|
149
|
+
user = User(username, password, email=email, attributes=attributes, instance=get_api_connector(USERS_INSTANCE_KEYS))
|
142
150
|
user_type = get_api_connector(USERS_INSTANCE_KEYS).get_user_type(curr_user) if curr_user is not None else 'admin'
|
143
151
|
if user_type == 'admin' and type is not None:
|
144
152
|
user.type = type
|
145
153
|
if user_type == 'admin' or curr_user.username == user.username:
|
146
154
|
return get_api_connector(USERS_INSTANCE_KEYS).edit_user(user, debug=debug)
|
147
155
|
|
148
|
-
|
156
|
+
raise fastapi.HTTPException(
|
157
|
+
status=403,
|
158
|
+
detail="Permission denied.",
|
159
|
+
)
|
149
160
|
|
150
161
|
|
151
162
|
@app.get(users_endpoint + "/{username}/id", tags=['Users'])
|
152
163
|
def get_user_id(
|
153
164
|
username: str,
|
154
|
-
curr_user = (
|
155
|
-
|
156
|
-
),
|
157
|
-
) -> Union[int, None]:
|
165
|
+
curr_user = fastapi.Depends(ScopedAuth(['users:read'])),
|
166
|
+
) -> Union[int, str, None]:
|
158
167
|
"""
|
159
168
|
Get a user's ID.
|
160
169
|
"""
|
161
|
-
|
170
|
+
user_id = get_api_connector(USERS_INSTANCE_KEYS).get_user_id(User(username, instance=get_api_connector(USERS_INSTANCE_KEYS)), debug=debug)
|
171
|
+
if is_uuid(user_id):
|
172
|
+
return str(user_id)
|
173
|
+
return user_id
|
162
174
|
|
163
175
|
|
164
176
|
@app.get(users_endpoint + "/{username}/attributes", tags=['Users'])
|
165
177
|
def get_user_attributes(
|
166
178
|
username: str,
|
167
179
|
curr_user = (
|
168
|
-
fastapi.Depends(
|
180
|
+
fastapi.Depends(ScopedAuth(['users:read'])) if private else None
|
169
181
|
),
|
170
182
|
) -> Union[Dict[str, Any], None]:
|
171
183
|
"""
|
172
184
|
Get a user's attributes.
|
173
185
|
"""
|
174
|
-
return get_api_connector(USERS_INSTANCE_KEYS).
|
186
|
+
return User(username, instance=get_api_connector(USERS_INSTANCE_KEYS)).get_attributes(refresh=True, debug=debug)
|
175
187
|
|
176
188
|
|
177
189
|
@app.delete(users_endpoint + "/{username}", tags=['Users'])
|
178
190
|
def delete_user(
|
179
191
|
username: str,
|
180
192
|
curr_user = (
|
181
|
-
fastapi.Depends(
|
193
|
+
fastapi.Depends(ScopedAuth(['users:delete'])) if not no_auth else None
|
182
194
|
),
|
183
195
|
) -> SuccessTuple:
|
184
196
|
"""
|
@@ -203,7 +215,7 @@ def delete_user(
|
|
203
215
|
def get_user_password_hash(
|
204
216
|
username: str,
|
205
217
|
curr_user = (
|
206
|
-
fastapi.Depends(
|
218
|
+
fastapi.Depends(ScopedAuth(['users:read', 'instance:chain']))
|
207
219
|
),
|
208
220
|
) -> str:
|
209
221
|
"""
|
@@ -211,14 +223,14 @@ def get_user_password_hash(
|
|
211
223
|
"""
|
212
224
|
if not check_allow_chaining():
|
213
225
|
raise HTTPException(status_code=403, detail=DISALLOW_CHAINING_MESSAGE)
|
214
|
-
return get_api_connector(USERS_INSTANCE_KEYS).get_user_password_hash(User(username), debug=debug)
|
226
|
+
return get_api_connector(USERS_INSTANCE_KEYS).get_user_password_hash(User(username, instance=get_api_connector(USERS_INSTANCE_KEYS)), debug=debug)
|
215
227
|
|
216
228
|
|
217
229
|
@app.get(users_endpoint + '/{username}/type', tags=['Users'])
|
218
230
|
def get_user_type(
|
219
231
|
username: str,
|
220
232
|
curr_user = (
|
221
|
-
fastapi.Depends(
|
233
|
+
fastapi.Depends(ScopedAuth(['users:read', 'instance:chain']))
|
222
234
|
),
|
223
235
|
) -> str:
|
224
236
|
"""
|
@@ -226,4 +238,4 @@ def get_user_type(
|
|
226
238
|
"""
|
227
239
|
if not check_allow_chaining():
|
228
240
|
raise HTTPException(status_code=403, detail=DISALLOW_CHAINING_MESSAGE)
|
229
|
-
return get_api_connector(USERS_INSTANCE_KEYS).get_user_type(User(username))
|
241
|
+
return get_api_connector(USERS_INSTANCE_KEYS).get_user_type(User(username, instance=get_api_connector(USERS_INSTANCE_KEYS)))
|
@@ -7,12 +7,12 @@ Return version information
|
|
7
7
|
"""
|
8
8
|
|
9
9
|
import fastapi
|
10
|
-
from meerschaum.api import app, endpoints, private,
|
10
|
+
from meerschaum.api import app, endpoints, private, ScopedAuth
|
11
11
|
|
12
12
|
@app.get(endpoints['version'], tags=['Version'])
|
13
13
|
def get_api_version(
|
14
14
|
curr_user = (
|
15
|
-
fastapi.Depends(
|
15
|
+
fastapi.Depends(ScopedAuth(['instance:read'])) if private else None
|
16
16
|
),
|
17
17
|
):
|
18
18
|
"""
|
@@ -24,7 +24,7 @@ def get_api_version(
|
|
24
24
|
@app.get(endpoints['version'] + "/mrsm", tags=['Version'])
|
25
25
|
def get_meerschaum_version(
|
26
26
|
curr_user = (
|
27
|
-
fastapi.Depends(
|
27
|
+
fastapi.Depends(ScopedAuth(['instance:read'])) if private else None
|
28
28
|
),
|
29
29
|
):
|
30
30
|
"""
|
@@ -8,7 +8,7 @@ Routes to the Webterm proxy.
|
|
8
8
|
|
9
9
|
import asyncio
|
10
10
|
from meerschaum.utils.typing import Optional
|
11
|
-
from meerschaum.api import app, endpoints
|
11
|
+
from meerschaum.api import app, endpoints, webterm_port
|
12
12
|
from meerschaum.utils.packages import attempt_import
|
13
13
|
from meerschaum.api.dash.sessions import is_session_authenticated, get_username_from_session
|
14
14
|
fastapi, fastapi_responses = attempt_import('fastapi', 'fastapi.responses')
|
@@ -71,7 +71,7 @@ async def get_webterm(
|
|
71
71
|
|
72
72
|
username = get_username_from_session(session_id)
|
73
73
|
async with httpx.AsyncClient() as client:
|
74
|
-
webterm_url = f"http://localhost:
|
74
|
+
webterm_url = f"http://localhost:{webterm_port}/webterm/{username or session_id}"
|
75
75
|
response = await client.get(webterm_url)
|
76
76
|
text = response.text
|
77
77
|
if request.url.scheme == 'https':
|
@@ -100,7 +100,7 @@ async def webterm_websocket(websocket: WebSocket, session_id: str):
|
|
100
100
|
|
101
101
|
username = get_username_from_session(session_id)
|
102
102
|
|
103
|
-
ws_url = f"ws://localhost:
|
103
|
+
ws_url = f"ws://localhost:{webterm_port}/websocket/{username or session_id}"
|
104
104
|
async with websockets.connect(ws_url) as ws:
|
105
105
|
async def forward_messages():
|
106
106
|
try:
|