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.
Files changed (201) 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 +228 -117
  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 +438 -88
  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/css/dash.css +16 -0
  68. meerschaum/api/resources/static/js/terminado.js +3 -0
  69. meerschaum/api/resources/static/js/xterm-addon-unicode11.js +2 -0
  70. meerschaum/api/resources/templates/termpage.html +13 -0
  71. meerschaum/api/routes/__init__.py +1 -0
  72. meerschaum/api/routes/_actions.py +3 -4
  73. meerschaum/api/routes/_connectors.py +3 -7
  74. meerschaum/api/routes/_jobs.py +26 -35
  75. meerschaum/api/routes/_login.py +120 -15
  76. meerschaum/api/routes/_misc.py +5 -10
  77. meerschaum/api/routes/_pipes.py +178 -143
  78. meerschaum/api/routes/_plugins.py +38 -28
  79. meerschaum/api/routes/_tokens.py +236 -0
  80. meerschaum/api/routes/_users.py +47 -35
  81. meerschaum/api/routes/_version.py +3 -3
  82. meerschaum/api/routes/_webterm.py +3 -3
  83. meerschaum/config/__init__.py +100 -30
  84. meerschaum/config/_default.py +132 -64
  85. meerschaum/config/_edit.py +38 -32
  86. meerschaum/config/_formatting.py +2 -0
  87. meerschaum/config/_patch.py +10 -8
  88. meerschaum/config/_paths.py +133 -13
  89. meerschaum/config/_read_config.py +87 -36
  90. meerschaum/config/_sync.py +6 -3
  91. meerschaum/config/_version.py +1 -1
  92. meerschaum/config/environment.py +262 -0
  93. meerschaum/config/stack/__init__.py +37 -15
  94. meerschaum/config/static.py +18 -0
  95. meerschaum/connectors/_Connector.py +11 -6
  96. meerschaum/connectors/__init__.py +41 -22
  97. meerschaum/connectors/api/_APIConnector.py +34 -6
  98. meerschaum/connectors/api/_actions.py +2 -2
  99. meerschaum/connectors/api/_jobs.py +12 -1
  100. meerschaum/connectors/api/_login.py +33 -7
  101. meerschaum/connectors/api/_misc.py +2 -2
  102. meerschaum/connectors/api/_pipes.py +23 -32
  103. meerschaum/connectors/api/_plugins.py +2 -2
  104. meerschaum/connectors/api/_request.py +1 -1
  105. meerschaum/connectors/api/_tokens.py +146 -0
  106. meerschaum/connectors/api/_users.py +70 -58
  107. meerschaum/connectors/instance/_InstanceConnector.py +83 -0
  108. meerschaum/connectors/instance/__init__.py +10 -0
  109. meerschaum/connectors/instance/_pipes.py +442 -0
  110. meerschaum/connectors/instance/_plugins.py +159 -0
  111. meerschaum/connectors/instance/_tokens.py +317 -0
  112. meerschaum/connectors/instance/_users.py +188 -0
  113. meerschaum/connectors/parse.py +5 -2
  114. meerschaum/connectors/sql/_SQLConnector.py +22 -5
  115. meerschaum/connectors/sql/_cli.py +12 -11
  116. meerschaum/connectors/sql/_create_engine.py +12 -168
  117. meerschaum/connectors/sql/_fetch.py +2 -18
  118. meerschaum/connectors/sql/_pipes.py +295 -278
  119. meerschaum/connectors/sql/_plugins.py +29 -0
  120. meerschaum/connectors/sql/_sql.py +47 -22
  121. meerschaum/connectors/sql/_users.py +36 -2
  122. meerschaum/connectors/sql/tables/__init__.py +254 -122
  123. meerschaum/connectors/valkey/_ValkeyConnector.py +5 -7
  124. meerschaum/connectors/valkey/_pipes.py +60 -31
  125. meerschaum/connectors/valkey/_plugins.py +2 -26
  126. meerschaum/core/Pipe/__init__.py +115 -85
  127. meerschaum/core/Pipe/_attributes.py +425 -124
  128. meerschaum/core/Pipe/_bootstrap.py +54 -24
  129. meerschaum/core/Pipe/_cache.py +555 -0
  130. meerschaum/core/Pipe/_clear.py +0 -11
  131. meerschaum/core/Pipe/_data.py +96 -68
  132. meerschaum/core/Pipe/_deduplicate.py +0 -13
  133. meerschaum/core/Pipe/_delete.py +12 -21
  134. meerschaum/core/Pipe/_drop.py +11 -23
  135. meerschaum/core/Pipe/_dtypes.py +49 -19
  136. meerschaum/core/Pipe/_edit.py +14 -4
  137. meerschaum/core/Pipe/_fetch.py +1 -1
  138. meerschaum/core/Pipe/_index.py +8 -14
  139. meerschaum/core/Pipe/_show.py +5 -5
  140. meerschaum/core/Pipe/_sync.py +123 -204
  141. meerschaum/core/Pipe/_verify.py +4 -4
  142. meerschaum/{plugins → core/Plugin}/_Plugin.py +16 -12
  143. meerschaum/core/Plugin/__init__.py +1 -1
  144. meerschaum/core/Token/_Token.py +220 -0
  145. meerschaum/core/Token/__init__.py +12 -0
  146. meerschaum/core/User/_User.py +35 -10
  147. meerschaum/core/User/__init__.py +9 -1
  148. meerschaum/core/__init__.py +1 -0
  149. meerschaum/jobs/_Executor.py +88 -4
  150. meerschaum/jobs/_Job.py +149 -38
  151. meerschaum/jobs/__init__.py +3 -2
  152. meerschaum/jobs/systemd.py +8 -3
  153. meerschaum/models/__init__.py +35 -0
  154. meerschaum/models/pipes.py +247 -0
  155. meerschaum/models/tokens.py +38 -0
  156. meerschaum/models/users.py +26 -0
  157. meerschaum/plugins/__init__.py +301 -88
  158. meerschaum/plugins/bootstrap.py +510 -4
  159. meerschaum/utils/_get_pipes.py +97 -30
  160. meerschaum/utils/daemon/Daemon.py +199 -43
  161. meerschaum/utils/daemon/FileDescriptorInterceptor.py +0 -1
  162. meerschaum/utils/daemon/RotatingFile.py +63 -36
  163. meerschaum/utils/daemon/StdinFile.py +53 -13
  164. meerschaum/utils/daemon/__init__.py +47 -6
  165. meerschaum/utils/daemon/_names.py +6 -3
  166. meerschaum/utils/dataframe.py +480 -82
  167. meerschaum/utils/debug.py +49 -19
  168. meerschaum/utils/dtypes/__init__.py +478 -37
  169. meerschaum/utils/dtypes/sql.py +369 -29
  170. meerschaum/utils/formatting/__init__.py +5 -2
  171. meerschaum/utils/formatting/_jobs.py +1 -1
  172. meerschaum/utils/formatting/_pipes.py +52 -50
  173. meerschaum/utils/formatting/_pprint.py +1 -0
  174. meerschaum/utils/formatting/_shell.py +44 -18
  175. meerschaum/utils/misc.py +268 -186
  176. meerschaum/utils/packages/__init__.py +25 -40
  177. meerschaum/utils/packages/_packages.py +42 -34
  178. meerschaum/utils/pipes.py +213 -0
  179. meerschaum/utils/process.py +2 -2
  180. meerschaum/utils/prompt.py +175 -144
  181. meerschaum/utils/schedule.py +2 -1
  182. meerschaum/utils/sql.py +135 -49
  183. meerschaum/utils/threading.py +42 -0
  184. meerschaum/utils/typing.py +1 -4
  185. meerschaum/utils/venv/_Venv.py +2 -2
  186. meerschaum/utils/venv/__init__.py +7 -7
  187. meerschaum/utils/warnings.py +19 -13
  188. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/METADATA +94 -96
  189. meerschaum-3.0.0.dist-info/RECORD +289 -0
  190. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/WHEEL +1 -1
  191. meerschaum-3.0.0.dist-info/licenses/NOTICE +2 -0
  192. meerschaum/api/models/_interfaces.py +0 -15
  193. meerschaum/api/models/_locations.py +0 -15
  194. meerschaum/api/models/_metrics.py +0 -15
  195. meerschaum/config/_environment.py +0 -145
  196. meerschaum/config/static/__init__.py +0 -186
  197. meerschaum-2.9.4.dist-info/RECORD +0 -263
  198. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/entry_points.txt +0 -0
  199. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/licenses/LICENSE +0 -0
  200. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/top_level.txt +0 -0
  201. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/zip-safe +0 -0
meerschaum/api/_events.py CHANGED
@@ -8,14 +8,16 @@ Declare FastAPI events in this module (startup, shutdown, etc.).
8
8
 
9
9
  import sys
10
10
  import os
11
- import time
12
11
  from meerschaum.api import (
13
12
  app,
14
13
  get_api_connector,
15
14
  get_cache_connector,
16
15
  get_uvicorn_config,
17
16
  debug,
17
+ webterm_port,
18
18
  no_dash,
19
+ _include_dash,
20
+ _include_webterm,
19
21
  )
20
22
  from meerschaum.utils.debug import dprint
21
23
  from meerschaum.connectors.poll import retry_connect
@@ -25,7 +27,7 @@ from meerschaum.jobs import (
25
27
  start_check_jobs_thread,
26
28
  stop_check_jobs_thread,
27
29
  )
28
- from meerschaum.config.static import STATIC_CONFIG
30
+ from meerschaum._internal.static import STATIC_CONFIG
29
31
 
30
32
  TEMP_PREFIX: str = STATIC_CONFIG['api']['jobs']['temp_prefix']
31
33
 
@@ -35,10 +37,45 @@ async def startup():
35
37
  """
36
38
  Connect to the instance database and begin monitoring jobs.
37
39
  """
40
+ app.openapi_schema = app.openapi()
41
+
42
+ ### Remove the implicitly added HTTPBearer scheme if it exists.
43
+ if 'BearerAuth' in app.openapi_schema['components']['securitySchemes']:
44
+ del app.openapi_schema['components']['securitySchemes']['BearerAuth']
45
+ elif 'HTTPBearer' in app.openapi_schema['components']['securitySchemes']:
46
+ del app.openapi_schema['components']['securitySchemes']['HTTPBearer']
47
+ if 'LoginManager' in app.openapi_schema['components']['securitySchemes']:
48
+ del app.openapi_schema['components']['securitySchemes']['LoginManager']
49
+
50
+ scopes = STATIC_CONFIG['tokens']['scopes']
51
+ app.openapi_schema['components']['securitySchemes']['OAuth2PasswordBearer'] = {
52
+ 'type': 'oauth2',
53
+ 'flows': {
54
+ 'password': {
55
+ 'tokenUrl': STATIC_CONFIG['api']['endpoints']['login'],
56
+ 'scopes': scopes,
57
+ },
58
+ },
59
+ }
60
+ app.openapi_schema['components']['securitySchemes']['APIKey'] = {
61
+ 'type': 'http',
62
+ 'scheme': 'bearer',
63
+ 'bearerFormat': 'mrsm-key:{client_id}:{client_secret}',
64
+ 'description': 'Authentication using a Meerschaum API Key.',
65
+ }
66
+ app.openapi_schema['security'] = [
67
+ {
68
+ 'OAuth2PasswordBearer': [],
69
+ },
70
+ {
71
+ 'APIKey': [],
72
+ }
73
+ ]
74
+
38
75
  try:
39
- if not no_dash:
76
+ if _include_webterm:
40
77
  from meerschaum.api.dash.webterm import start_webterm
41
- start_webterm()
78
+ start_webterm(webterm_port=webterm_port)
42
79
 
43
80
  connected = retry_connect(
44
81
  get_api_connector(),
@@ -61,6 +98,11 @@ async def startup():
61
98
  await shutdown()
62
99
  os._exit(1)
63
100
 
101
+ conn = get_api_connector()
102
+ if conn.type == 'sql':
103
+ from meerschaum.connectors.sql.tables import get_tables
104
+ _ = get_tables(conn, refresh=True, create=True, debug=debug)
105
+
64
106
  start_check_jobs_thread()
65
107
 
66
108
 
meerschaum/api/_oauth2.py CHANGED
@@ -7,21 +7,35 @@ Define JWT authorization here.
7
7
  """
8
8
 
9
9
  import os
10
- from meerschaum.api import app, endpoints, CHECK_UPDATE
10
+ import base64
11
+ import functools
12
+ import inspect
13
+ from typing import List, Optional, Union
14
+
15
+ from meerschaum.api import endpoints, CHECK_UPDATE, no_auth, debug
16
+ from meerschaum.api._tokens import optional_token, get_token_from_authorization
17
+ from meerschaum._internal.static import STATIC_CONFIG
11
18
  from meerschaum.utils.packages import attempt_import
12
- fastapi = attempt_import('fastapi', lazy=False, check_update=CHECK_UPDATE)
19
+ from meerschaum.core import User, Token
20
+
21
+ fastapi, starlette = attempt_import('fastapi', 'starlette', lazy=False, check_update=CHECK_UPDATE)
13
22
  fastapi_responses = attempt_import('fastapi.responses', lazy=False, check_update=CHECK_UPDATE)
14
23
  fastapi_login = attempt_import('fastapi_login', check_update=CHECK_UPDATE)
24
+ jose_jwt, jose_exceptions = attempt_import('jose.jwt', 'jose.exceptions', lazy=False, check_update=CHECK_UPDATE)
25
+ from fastapi import Depends, HTTPException, Request
26
+ from starlette import status
27
+
15
28
 
16
29
  class CustomOAuth2PasswordRequestForm:
17
30
  def __init__(
18
31
  self,
19
32
  grant_type: str = fastapi.Form(None, regex="password|client_credentials"),
20
- username: str = fastapi.Form(...),
21
- password: str = fastapi.Form(...),
22
- scope: str = fastapi.Form(""),
23
- client_id: str = fastapi.Form(None),
24
- client_secret: str = fastapi.Form(None),
33
+ username: Optional[str] = fastapi.Form(None),
34
+ password: Optional[str] = fastapi.Form(None),
35
+ scope: str = fastapi.Form(" ".join(STATIC_CONFIG['tokens']['scopes'])),
36
+ client_id: Optional[str] = fastapi.Form(None),
37
+ client_secret: Optional[str] = fastapi.Form(None),
38
+ authorization: Optional[str] = fastapi.Header(None),
25
39
  ):
26
40
  self.grant_type = grant_type
27
41
  self.username = username
@@ -30,9 +44,144 @@ class CustomOAuth2PasswordRequestForm:
30
44
  self.client_id = client_id
31
45
  self.client_secret = client_secret
32
46
 
47
+ if (
48
+ not username
49
+ and not password
50
+ and not self.client_id
51
+ and not self.client_secret
52
+ and authorization
53
+ ):
54
+ try:
55
+ scheme, credentials = authorization.split()
56
+ if credentials.startswith('mrsm-key:'):
57
+ credentials = credentials[len('mrsm-key:'):]
58
+ if scheme.lower() in ('basic', 'bearer'):
59
+ decoded_credentials = base64.b64decode(credentials).decode('utf-8')
60
+ _client_id, _client_secret = decoded_credentials.split(':', 1)
61
+ self.client_id = _client_id
62
+ self.client_secret = _client_secret
63
+ self.grant_type = 'client_credentials'
64
+ except ValueError:
65
+ pass
66
+
67
+
68
+ async def optional_user(request: Request) -> Optional[User]:
69
+ """
70
+ FastAPI dependency that returns a User if logged in, otherwise None.
71
+ """
72
+ if no_auth:
73
+ return None
74
+ return await manager(request)
75
+ try:
76
+ return await manager(request)
77
+ except HTTPException:
78
+ return None
79
+
80
+
81
+ async def load_user_or_token(
82
+ request: Request,
83
+ users: bool = True,
84
+ tokens: bool = True,
85
+ ) -> Union[User, Token, None]:
86
+ """
87
+ Load the current user or token.
88
+ """
89
+ authorization = request.headers.get('authorization', request.headers.get('Authorization', None))
90
+ if not authorization:
91
+ raise HTTPException(
92
+ status_code=status.HTTP_401_UNAUTHORIZED,
93
+ detail="Not authenticated.",
94
+ )
95
+
96
+ authorization = authorization.replace('Basic ', '').replace('Bearer ', '')
97
+ if not authorization.startswith('mrsm-key:'):
98
+ if not users:
99
+ raise HTTPException(
100
+ status=status.HTTP_401_UNAUTHORIZED,
101
+ detail="Users not authenticated for this endpoint.",
102
+ )
103
+
104
+ return await manager(request)
105
+
106
+ if not tokens:
107
+ raise HTTPException(
108
+ status_code=status.HTTP_401_UNAUTHORIZED,
109
+ detail="Tokens not authenticated for this endpoint.",
110
+ )
111
+
112
+ return get_token_from_authorization(authorization)
113
+
114
+
115
+ def ScopedAuth(scopes: List[str]):
116
+ """
117
+ Dependency factory for authenticating with either a user session or a scoped token.
118
+ """
119
+ async def _authenticate(
120
+ request: Request,
121
+ user_or_token: Union[User, Token, None] = Depends(
122
+ load_user_or_token,
123
+ ),
124
+ ) -> Union[User, Token, None]:
125
+ if no_auth:
126
+ return None
127
+
128
+ if not user_or_token:
129
+ raise HTTPException(
130
+ status_code=status.HTTP_401_UNAUTHORIZED,
131
+ detail="Not authenticated.",
132
+ headers={"WWW-Authenticate": "Basic"},
133
+ )
134
+
135
+ authorization = request.headers.get('authorization', request.headers.get('Authorization', None))
136
+ is_long_lived = authorization and 'mrsm-key:' in authorization
137
+
138
+ current_scopes = []
139
+ ### For long-lived API tokens, always hit the database.
140
+ if is_long_lived:
141
+ current_scopes = user_or_token.get_scopes(refresh=True, debug=debug)
142
+
143
+ ### For JWTs, trust the scopes in the token.
144
+ else:
145
+ if not authorization:
146
+ # This should be caught by `load_user_or_token` but we can be safe.
147
+ raise HTTPException(
148
+ status_code=status.HTTP_401_UNAUTHORIZED,
149
+ detail="Not authenticated.",
150
+ )
151
+
152
+ scheme, _, token_str = authorization.partition(' ')
153
+ if not token_str or scheme.lower() != 'bearer':
154
+ raise HTTPException(
155
+ status_code=status.HTTP_401_UNAUTHORIZED,
156
+ detail="Unsupported authentication scheme.",
157
+ )
158
+
159
+ try:
160
+ payload = jose_jwt.decode(token_str, SECRET, algorithms=['HS256'])
161
+ current_scopes = payload.get('scopes', [])
162
+ except jose_exceptions.JWTError:
163
+ raise HTTPException(
164
+ status_code=status.HTTP_401_UNAUTHORIZED,
165
+ detail="Invalid access token.",
166
+ headers={"WWW-Authenticate": "Bearer"},
167
+ )
168
+
169
+ if '*' in current_scopes:
170
+ return user_or_token
171
+
172
+ for scope in scopes:
173
+ if scope not in current_scopes:
174
+ raise HTTPException(
175
+ status_code=status.HTTP_403_FORBIDDEN,
176
+ detail=f"Missing required scope: '{scope}'",
177
+ )
178
+
179
+ return user_or_token
180
+ return _authenticate
181
+
33
182
 
34
183
  LoginManager = fastapi_login.LoginManager
35
- def generate_secret_key() -> str:
184
+ def generate_secret_key() -> bytes:
36
185
  """
37
186
  Read or generate the secret keyfile.
38
187
  """
@@ -49,4 +198,8 @@ def generate_secret_key() -> str:
49
198
 
50
199
 
51
200
  SECRET = generate_secret_key()
52
- manager = LoginManager(SECRET, token_url=endpoints['login'])
201
+ manager = LoginManager(
202
+ SECRET,
203
+ token_url=endpoints['login'],
204
+ scopes=STATIC_CONFIG['tokens']['scopes'],
205
+ )
@@ -0,0 +1,102 @@
1
+ #! /usr/bin/env python3
2
+ # vim:fenc=utf-8
3
+
4
+ """
5
+ Define the authentication logic for Meerschaum Tokens.
6
+ """
7
+
8
+ import base64
9
+ import uuid
10
+ from typing import Optional, Union, List
11
+ from datetime import datetime, timezone
12
+
13
+ from fastapi import Depends, HTTPException, Request
14
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
15
+ from starlette import status
16
+
17
+ import meerschaum as mrsm
18
+ from meerschaum.api import (
19
+ get_api_connector,
20
+ debug,
21
+ )
22
+ from meerschaum.core import Token, User
23
+ from meerschaum.core.User import verify_password
24
+
25
+
26
+ http_bearer = HTTPBearer(auto_error=False, scheme_name="APIKey")
27
+
28
+
29
+ def get_token_from_authorization(authorization: str) -> Token:
30
+ """
31
+ Helper function to decode and verify a token from credentials.
32
+ Raises HTTPException on failure.
33
+ """
34
+ if authorization.startswith('mrsm-key:'):
35
+ authorization = authorization[len('mrsm-key:'):]
36
+ try:
37
+ credential_string = base64.b64decode(authorization).decode('utf-8')
38
+ token_id_str, secret = credential_string.split(':', 1)
39
+ token_id = uuid.UUID(token_id_str)
40
+ except Exception:
41
+ raise HTTPException(
42
+ status_code=status.HTTP_401_UNAUTHORIZED,
43
+ detail="Invalid token format. Expected Base64-encoded 'token_id:secret'.",
44
+ headers={"WWW-Authenticate": "Bearer"},
45
+ )
46
+
47
+ conn = get_api_connector()
48
+ token = conn.get_token(token_id)
49
+
50
+ if not token or not token.is_valid:
51
+ raise HTTPException(
52
+ status_code=status.HTTP_401_UNAUTHORIZED,
53
+ detail="Invalid or revoked token.",
54
+ headers={"WWW-Authenticate": "Bearer"},
55
+ )
56
+
57
+ if token.get_expiration_status(debug=debug):
58
+ raise HTTPException(
59
+ status_code=status.HTTP_401_UNAUTHORIZED,
60
+ detail="Token has expired.",
61
+ headers={"WWW-Authenticate": "Bearer"},
62
+ )
63
+
64
+ if not verify_password(secret, token.secret_hash):
65
+ raise HTTPException(
66
+ status_code=status.HTTP_401_UNAUTHORIZED,
67
+ detail="Invalid secret.",
68
+ headers={"WWW-Authenticate": "Bearer"},
69
+ )
70
+
71
+ return token
72
+
73
+
74
+ def get_current_token(
75
+ auth_creds: Optional[HTTPAuthorizationCredentials] = Depends(http_bearer),
76
+ ) -> Token:
77
+ """
78
+ FastAPI dependency to authenticate a request with a Meerschaum Token.
79
+ This dependency will fail if no token is provided.
80
+ """
81
+ if auth_creds is None:
82
+ raise HTTPException(
83
+ status_code=status.HTTP_401_UNAUTHORIZED,
84
+ detail="Not authenticated.",
85
+ headers={"WWW-Authenticate": "Bearer"},
86
+ )
87
+ return get_token_from_authorization(auth_creds.credentials)
88
+
89
+
90
+ async def optional_token(
91
+ auth_creds: Optional[HTTPAuthorizationCredentials] = Depends(http_bearer),
92
+ ) -> Optional[Token]:
93
+ """
94
+ FastAPI dependency that returns a Token if provided, otherwise None.
95
+ """
96
+ if not auth_creds:
97
+ return None
98
+
99
+ try:
100
+ return get_token_from_authorization(auth_creds.credentials)
101
+ except HTTPException as e:
102
+ return None
@@ -12,14 +12,11 @@ from meerschaum.utils.packages import (
12
12
  attempt_import,
13
13
  import_dcc,
14
14
  import_html,
15
- _monkey_patch_get_distribution,
16
15
  )
17
16
  flask_compress = attempt_import('flask_compress', lazy=False)
18
- _monkey_patch_get_distribution('flask-compress', flask_compress.__version__)
19
17
  dash = attempt_import('dash', lazy=False)
20
18
 
21
19
  from meerschaum.utils.typing import List, Optional
22
- from meerschaum.config.static import _static_config
23
20
  from meerschaum.api import (
24
21
  app as fastapi_app,
25
22
  debug,
@@ -10,6 +10,7 @@ from meerschaum.api import debug as _debug
10
10
  import meerschaum.api.dash.callbacks.dashboard
11
11
  import meerschaum.api.dash.callbacks.login
12
12
  import meerschaum.api.dash.callbacks.plugins
13
+ import meerschaum.api.dash.callbacks.tokens
13
14
  import meerschaum.api.dash.callbacks.jobs
14
15
  import meerschaum.api.dash.callbacks.register
15
16
  import meerschaum.api.dash.callbacks.pipes
@@ -7,7 +7,6 @@ Import custom callbacks created by plugins.
7
7
  """
8
8
 
9
9
  import traceback
10
- from typing import Any, Dict
11
10
 
12
11
  from meerschaum.api.dash import dash_app
13
12
  from meerschaum.plugins import _dash_plugins, _plugin_endpoints_to_pages
@@ -36,9 +35,9 @@ def add_plugin_pages(debug: bool = False):
36
35
  """
37
36
  Allow users to add pages via the `@web_page` decorator.
38
37
  """
39
- for plugin_name, pages_dicts in _plugin_endpoints_to_pages.items():
38
+ for page_group, pages_dicts in _plugin_endpoints_to_pages.items():
40
39
  if debug:
41
- dprint(f"Adding pages from plugin '{plugin_name}'...")
40
+ dprint(f"Adding pages for group '{page_group}'...")
42
41
  for _endpoint, _page_dict in pages_dicts.items():
43
42
  page_layout = _page_dict['function']()
44
43
  if not _page_dict['skip_navbar']:
@@ -47,6 +46,8 @@ def add_plugin_pages(debug: bool = False):
47
46
  else:
48
47
  page_layout = [pages_navbar, page_layout]
49
48
  _pages[_page_dict['page_key']] = _endpoint
49
+ if not _endpoint.lstrip('/').startswith('dash'):
50
+ _endpoint = '/dash/' + _endpoint.lstrip('/')
50
51
  _paths[_endpoint] = page_layout
51
52
  if _page_dict['login_required']:
52
53
  _required_login.add(_endpoint)