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.
- 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 +198 -118
- 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 +382 -95
- 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/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 +46 -21
- 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 +479 -81
- meerschaum/utils/debug.py +49 -19
- meerschaum/utils/dtypes/__init__.py +476 -34
- 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 +134 -47
- 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.5.dist-info → meerschaum-3.0.0.dist-info}/METADATA +94 -96
- meerschaum-3.0.0.dist-info/RECORD +289 -0
- {meerschaum-2.9.5.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.5.dist-info/RECORD +0 -263
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/top_level.txt +0 -0
- {meerschaum-2.9.5.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.
|
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
|
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
|
-
|
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
|
-
|
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() ->
|
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(
|
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
|
meerschaum/api/dash/__init__.py
CHANGED
@@ -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
|
38
|
+
for page_group, pages_dicts in _plugin_endpoints_to_pages.items():
|
40
39
|
if debug:
|
41
|
-
dprint(f"Adding pages
|
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)
|