meerschaum 2.9.5__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.
Files changed (200) hide show
  1. meerschaum/__init__.py +5 -2
  2. meerschaum/_internal/__init__.py +1 -0
  3. meerschaum/_internal/arguments/_parse_arguments.py +4 -4
  4. meerschaum/_internal/arguments/_parser.py +33 -4
  5. meerschaum/_internal/cli/__init__.py +6 -0
  6. meerschaum/_internal/cli/daemons.py +103 -0
  7. meerschaum/_internal/cli/entry.py +220 -0
  8. meerschaum/_internal/cli/workers.py +435 -0
  9. meerschaum/_internal/docs/index.py +48 -2
  10. meerschaum/_internal/entry.py +50 -14
  11. meerschaum/_internal/shell/Shell.py +121 -29
  12. meerschaum/_internal/shell/__init__.py +4 -1
  13. meerschaum/_internal/static.py +359 -0
  14. meerschaum/_internal/term/TermPageHandler.py +1 -2
  15. meerschaum/_internal/term/__init__.py +40 -6
  16. meerschaum/_internal/term/tools.py +33 -8
  17. meerschaum/actions/__init__.py +6 -4
  18. meerschaum/actions/api.py +53 -13
  19. meerschaum/actions/attach.py +1 -0
  20. meerschaum/actions/bootstrap.py +8 -8
  21. meerschaum/actions/delete.py +4 -2
  22. meerschaum/actions/edit.py +171 -25
  23. meerschaum/actions/login.py +8 -8
  24. meerschaum/actions/register.py +143 -6
  25. meerschaum/actions/reload.py +22 -5
  26. meerschaum/actions/restart.py +14 -0
  27. meerschaum/actions/show.py +184 -31
  28. meerschaum/actions/start.py +166 -17
  29. meerschaum/actions/stop.py +38 -2
  30. meerschaum/actions/sync.py +7 -2
  31. meerschaum/actions/tag.py +9 -8
  32. meerschaum/actions/verify.py +5 -8
  33. meerschaum/api/__init__.py +45 -15
  34. meerschaum/api/_events.py +46 -4
  35. meerschaum/api/_oauth2.py +162 -9
  36. meerschaum/api/_tokens.py +102 -0
  37. meerschaum/api/dash/__init__.py +0 -3
  38. meerschaum/api/dash/callbacks/__init__.py +1 -0
  39. meerschaum/api/dash/callbacks/custom.py +4 -3
  40. meerschaum/api/dash/callbacks/dashboard.py +198 -118
  41. meerschaum/api/dash/callbacks/jobs.py +14 -7
  42. meerschaum/api/dash/callbacks/login.py +10 -1
  43. meerschaum/api/dash/callbacks/pipes.py +194 -14
  44. meerschaum/api/dash/callbacks/plugins.py +0 -1
  45. meerschaum/api/dash/callbacks/register.py +10 -3
  46. meerschaum/api/dash/callbacks/settings/password_reset.py +2 -2
  47. meerschaum/api/dash/callbacks/tokens.py +389 -0
  48. meerschaum/api/dash/components.py +36 -15
  49. meerschaum/api/dash/jobs.py +1 -1
  50. meerschaum/api/dash/keys.py +35 -93
  51. meerschaum/api/dash/pages/__init__.py +2 -1
  52. meerschaum/api/dash/pages/dashboard.py +1 -20
  53. meerschaum/api/dash/pages/{job.py → jobs.py} +10 -7
  54. meerschaum/api/dash/pages/login.py +2 -2
  55. meerschaum/api/dash/pages/pipes.py +16 -5
  56. meerschaum/api/dash/pages/settings/password_reset.py +1 -1
  57. meerschaum/api/dash/pages/tokens.py +53 -0
  58. meerschaum/api/dash/pipes.py +382 -95
  59. meerschaum/api/dash/sessions.py +12 -0
  60. meerschaum/api/dash/tokens.py +603 -0
  61. meerschaum/api/dash/websockets.py +1 -1
  62. meerschaum/api/dash/webterm.py +18 -6
  63. meerschaum/api/models/__init__.py +23 -3
  64. meerschaum/api/models/_actions.py +22 -0
  65. meerschaum/api/models/_pipes.py +91 -7
  66. meerschaum/api/models/_tokens.py +81 -0
  67. meerschaum/api/resources/static/js/terminado.js +3 -0
  68. meerschaum/api/resources/static/js/xterm-addon-unicode11.js +2 -0
  69. meerschaum/api/resources/templates/termpage.html +13 -0
  70. meerschaum/api/routes/__init__.py +1 -0
  71. meerschaum/api/routes/_actions.py +3 -4
  72. meerschaum/api/routes/_connectors.py +3 -7
  73. meerschaum/api/routes/_jobs.py +26 -35
  74. meerschaum/api/routes/_login.py +120 -15
  75. meerschaum/api/routes/_misc.py +5 -10
  76. meerschaum/api/routes/_pipes.py +178 -143
  77. meerschaum/api/routes/_plugins.py +38 -28
  78. meerschaum/api/routes/_tokens.py +236 -0
  79. meerschaum/api/routes/_users.py +47 -35
  80. meerschaum/api/routes/_version.py +3 -3
  81. meerschaum/api/routes/_webterm.py +3 -3
  82. meerschaum/config/__init__.py +100 -30
  83. meerschaum/config/_default.py +132 -64
  84. meerschaum/config/_edit.py +38 -32
  85. meerschaum/config/_formatting.py +2 -0
  86. meerschaum/config/_patch.py +10 -8
  87. meerschaum/config/_paths.py +133 -13
  88. meerschaum/config/_read_config.py +87 -36
  89. meerschaum/config/_sync.py +6 -3
  90. meerschaum/config/_version.py +1 -1
  91. meerschaum/config/environment.py +262 -0
  92. meerschaum/config/stack/__init__.py +37 -15
  93. meerschaum/config/static.py +18 -0
  94. meerschaum/connectors/_Connector.py +11 -6
  95. meerschaum/connectors/__init__.py +41 -22
  96. meerschaum/connectors/api/_APIConnector.py +34 -6
  97. meerschaum/connectors/api/_actions.py +2 -2
  98. meerschaum/connectors/api/_jobs.py +12 -1
  99. meerschaum/connectors/api/_login.py +33 -7
  100. meerschaum/connectors/api/_misc.py +2 -2
  101. meerschaum/connectors/api/_pipes.py +23 -32
  102. meerschaum/connectors/api/_plugins.py +2 -2
  103. meerschaum/connectors/api/_request.py +1 -1
  104. meerschaum/connectors/api/_tokens.py +146 -0
  105. meerschaum/connectors/api/_users.py +70 -58
  106. meerschaum/connectors/instance/_InstanceConnector.py +83 -0
  107. meerschaum/connectors/instance/__init__.py +10 -0
  108. meerschaum/connectors/instance/_pipes.py +442 -0
  109. meerschaum/connectors/instance/_plugins.py +159 -0
  110. meerschaum/connectors/instance/_tokens.py +317 -0
  111. meerschaum/connectors/instance/_users.py +188 -0
  112. meerschaum/connectors/parse.py +5 -2
  113. meerschaum/connectors/sql/_SQLConnector.py +22 -5
  114. meerschaum/connectors/sql/_cli.py +12 -11
  115. meerschaum/connectors/sql/_create_engine.py +12 -168
  116. meerschaum/connectors/sql/_fetch.py +2 -18
  117. meerschaum/connectors/sql/_pipes.py +295 -278
  118. meerschaum/connectors/sql/_plugins.py +29 -0
  119. meerschaum/connectors/sql/_sql.py +46 -21
  120. meerschaum/connectors/sql/_users.py +36 -2
  121. meerschaum/connectors/sql/tables/__init__.py +254 -122
  122. meerschaum/connectors/valkey/_ValkeyConnector.py +5 -7
  123. meerschaum/connectors/valkey/_pipes.py +60 -31
  124. meerschaum/connectors/valkey/_plugins.py +2 -26
  125. meerschaum/core/Pipe/__init__.py +115 -85
  126. meerschaum/core/Pipe/_attributes.py +425 -124
  127. meerschaum/core/Pipe/_bootstrap.py +54 -24
  128. meerschaum/core/Pipe/_cache.py +555 -0
  129. meerschaum/core/Pipe/_clear.py +0 -11
  130. meerschaum/core/Pipe/_data.py +96 -68
  131. meerschaum/core/Pipe/_deduplicate.py +0 -13
  132. meerschaum/core/Pipe/_delete.py +12 -21
  133. meerschaum/core/Pipe/_drop.py +11 -23
  134. meerschaum/core/Pipe/_dtypes.py +49 -19
  135. meerschaum/core/Pipe/_edit.py +14 -4
  136. meerschaum/core/Pipe/_fetch.py +1 -1
  137. meerschaum/core/Pipe/_index.py +8 -14
  138. meerschaum/core/Pipe/_show.py +5 -5
  139. meerschaum/core/Pipe/_sync.py +123 -204
  140. meerschaum/core/Pipe/_verify.py +4 -4
  141. meerschaum/{plugins → core/Plugin}/_Plugin.py +16 -12
  142. meerschaum/core/Plugin/__init__.py +1 -1
  143. meerschaum/core/Token/_Token.py +220 -0
  144. meerschaum/core/Token/__init__.py +12 -0
  145. meerschaum/core/User/_User.py +35 -10
  146. meerschaum/core/User/__init__.py +9 -1
  147. meerschaum/core/__init__.py +1 -0
  148. meerschaum/jobs/_Executor.py +88 -4
  149. meerschaum/jobs/_Job.py +149 -38
  150. meerschaum/jobs/__init__.py +3 -2
  151. meerschaum/jobs/systemd.py +8 -3
  152. meerschaum/models/__init__.py +35 -0
  153. meerschaum/models/pipes.py +247 -0
  154. meerschaum/models/tokens.py +38 -0
  155. meerschaum/models/users.py +26 -0
  156. meerschaum/plugins/__init__.py +301 -88
  157. meerschaum/plugins/bootstrap.py +510 -4
  158. meerschaum/utils/_get_pipes.py +97 -30
  159. meerschaum/utils/daemon/Daemon.py +199 -43
  160. meerschaum/utils/daemon/FileDescriptorInterceptor.py +0 -1
  161. meerschaum/utils/daemon/RotatingFile.py +63 -36
  162. meerschaum/utils/daemon/StdinFile.py +53 -13
  163. meerschaum/utils/daemon/__init__.py +47 -6
  164. meerschaum/utils/daemon/_names.py +6 -3
  165. meerschaum/utils/dataframe.py +479 -81
  166. meerschaum/utils/debug.py +49 -19
  167. meerschaum/utils/dtypes/__init__.py +476 -34
  168. meerschaum/utils/dtypes/sql.py +369 -29
  169. meerschaum/utils/formatting/__init__.py +5 -2
  170. meerschaum/utils/formatting/_jobs.py +1 -1
  171. meerschaum/utils/formatting/_pipes.py +52 -50
  172. meerschaum/utils/formatting/_pprint.py +1 -0
  173. meerschaum/utils/formatting/_shell.py +44 -18
  174. meerschaum/utils/misc.py +268 -186
  175. meerschaum/utils/packages/__init__.py +25 -40
  176. meerschaum/utils/packages/_packages.py +42 -34
  177. meerschaum/utils/pipes.py +213 -0
  178. meerschaum/utils/process.py +2 -2
  179. meerschaum/utils/prompt.py +175 -144
  180. meerschaum/utils/schedule.py +2 -1
  181. meerschaum/utils/sql.py +134 -47
  182. meerschaum/utils/threading.py +42 -0
  183. meerschaum/utils/typing.py +1 -4
  184. meerschaum/utils/venv/_Venv.py +2 -2
  185. meerschaum/utils/venv/__init__.py +7 -7
  186. meerschaum/utils/warnings.py +19 -13
  187. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/METADATA +94 -96
  188. meerschaum-3.0.0.dist-info/RECORD +289 -0
  189. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/WHEEL +1 -1
  190. meerschaum-3.0.0.dist-info/licenses/NOTICE +2 -0
  191. meerschaum/api/models/_interfaces.py +0 -15
  192. meerschaum/api/models/_locations.py +0 -15
  193. meerschaum/api/models/_metrics.py +0 -15
  194. meerschaum/config/_environment.py +0 -145
  195. meerschaum/config/static/__init__.py +0 -186
  196. meerschaum-2.9.5.dist-info/RECORD +0 -263
  197. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/entry_points.txt +0 -0
  198. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/licenses/LICENSE +0 -0
  199. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/top_level.txt +0 -0
  200. {meerschaum-2.9.5.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(plugins_endpoint + '/{name}', tags=['Plugins'])
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: 'meerschaum.core.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
- success, msg = get_api_connector(
112
- PLUGINS_INSTANCE_KEYS
113
- ).register_plugin(plugin, make_archive=False, debug=debug)
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
- @app.get(plugins_endpoint + '/{name}', tags=['Plugins'])
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(plugins_endpoint + '/{name}', tags=['Plugins'])
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
- fastapi.Depends(manager) if private else None
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 = get_api_connector(PLUGINS_INSTANCE_KEYS).get_user_id(curr_user)
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
+ )
@@ -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, app, endpoints, get_api_connector, manager,
18
- debug, check_allow_chaining, DISALLOW_CHAINING_MESSAGE,
19
- no_auth, private, default_instance_keys,
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(manager) if not no_auth else None
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(manager) if private else None
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
- return False, f"Cannot edit user '{user}': Permission denied"
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
- fastapi.Depends(manager) if not no_auth else None
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
- return get_api_connector(USERS_INSTANCE_KEYS).get_user_id(User(username), debug=debug)
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(manager) if private else None
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).get_user_attributes(User(username), debug=debug)
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(manager) if not no_auth else None
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(manager) if not no_auth else None
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(manager) if not no_auth else None
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, manager
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(manager) if private else None
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(manager) if private else None
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:8765/webterm/{username or session_id}"
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:8765/websocket/{username or session_id}"
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: