MindsDB 25.8.3.0__py3-none-any.whl → 25.9.1.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.
Potentially problematic release.
This version of MindsDB might be problematic. Click here for more details.
- mindsdb/__about__.py +1 -1
- mindsdb/__main__.py +2 -44
- mindsdb/api/a2a/__init__.py +52 -0
- mindsdb/api/a2a/agent.py +11 -12
- mindsdb/api/a2a/common/server/server.py +17 -36
- mindsdb/api/a2a/common/server/task_manager.py +14 -28
- mindsdb/api/a2a/task_manager.py +20 -21
- mindsdb/api/a2a/utils.py +1 -1
- mindsdb/api/common/middleware.py +106 -0
- mindsdb/api/http/initialize.py +13 -15
- mindsdb/api/http/namespaces/auth.py +6 -14
- mindsdb/api/http/namespaces/config.py +0 -2
- mindsdb/api/http/namespaces/default.py +74 -106
- mindsdb/api/http/start.py +25 -44
- mindsdb/api/litellm/start.py +11 -10
- mindsdb/api/mcp/__init__.py +165 -0
- mindsdb/api/mysql/mysql_proxy/mysql_proxy.py +33 -64
- mindsdb/api/postgres/postgres_proxy/postgres_proxy.py +86 -85
- mindsdb/integrations/handlers/crate_handler/crate_handler.py +3 -7
- mindsdb/integrations/handlers/derby_handler/derby_handler.py +32 -34
- mindsdb/integrations/handlers/documentdb_handler/requirements.txt +1 -0
- mindsdb/integrations/handlers/dummy_data_handler/dummy_data_handler.py +12 -13
- mindsdb/integrations/handlers/google_books_handler/google_books_handler.py +45 -44
- mindsdb/integrations/handlers/google_calendar_handler/google_calendar_handler.py +101 -95
- mindsdb/integrations/handlers/google_content_shopping_handler/google_content_shopping_handler.py +129 -129
- mindsdb/integrations/handlers/google_fit_handler/google_fit_handler.py +59 -43
- mindsdb/integrations/handlers/google_search_handler/google_search_handler.py +38 -39
- mindsdb/integrations/handlers/informix_handler/informix_handler.py +5 -18
- mindsdb/integrations/handlers/maxdb_handler/maxdb_handler.py +22 -28
- mindsdb/integrations/handlers/monetdb_handler/monetdb_handler.py +3 -7
- mindsdb/integrations/handlers/mongodb_handler/mongodb_handler.py +53 -67
- mindsdb/integrations/handlers/mongodb_handler/requirements.txt +1 -0
- mindsdb/{api/mongo/utilities → integrations/handlers/mongodb_handler/utils}/mongodb_ast.py +43 -68
- mindsdb/{api/mongo/utilities → integrations/handlers/mongodb_handler/utils}/mongodb_parser.py +17 -25
- mindsdb/{api/mongo/utilities → integrations/handlers/mongodb_handler/utils}/mongodb_query.py +10 -16
- mindsdb/integrations/handlers/mongodb_handler/utils/mongodb_render.py +43 -69
- mindsdb/integrations/libs/base.py +1 -1
- mindsdb/interfaces/agents/constants.py +1 -0
- mindsdb/interfaces/knowledge_base/controller.py +3 -1
- mindsdb/utilities/config.py +3 -155
- mindsdb/utilities/log.py +0 -25
- mindsdb/utilities/starters.py +0 -39
- {mindsdb-25.8.3.0.dist-info → mindsdb-25.9.1.0.dist-info}/METADATA +263 -261
- {mindsdb-25.8.3.0.dist-info → mindsdb-25.9.1.0.dist-info}/RECORD +47 -91
- mindsdb/api/a2a/__main__.py +0 -144
- mindsdb/api/a2a/run_a2a.py +0 -86
- mindsdb/api/common/check_auth.py +0 -42
- mindsdb/api/http/gunicorn_wrapper.py +0 -17
- mindsdb/api/mcp/start.py +0 -205
- mindsdb/api/mongo/__init__.py +0 -0
- mindsdb/api/mongo/classes/__init__.py +0 -5
- mindsdb/api/mongo/classes/query_sql.py +0 -19
- mindsdb/api/mongo/classes/responder.py +0 -45
- mindsdb/api/mongo/classes/responder_collection.py +0 -34
- mindsdb/api/mongo/classes/scram.py +0 -86
- mindsdb/api/mongo/classes/session.py +0 -23
- mindsdb/api/mongo/functions/__init__.py +0 -19
- mindsdb/api/mongo/responders/__init__.py +0 -73
- mindsdb/api/mongo/responders/add_shard.py +0 -13
- mindsdb/api/mongo/responders/aggregate.py +0 -90
- mindsdb/api/mongo/responders/buildinfo.py +0 -17
- mindsdb/api/mongo/responders/coll_stats.py +0 -63
- mindsdb/api/mongo/responders/company_id.py +0 -25
- mindsdb/api/mongo/responders/connection_status.py +0 -22
- mindsdb/api/mongo/responders/count.py +0 -21
- mindsdb/api/mongo/responders/db_stats.py +0 -32
- mindsdb/api/mongo/responders/delete.py +0 -105
- mindsdb/api/mongo/responders/describe.py +0 -23
- mindsdb/api/mongo/responders/end_sessions.py +0 -13
- mindsdb/api/mongo/responders/find.py +0 -175
- mindsdb/api/mongo/responders/get_cmd_line_opts.py +0 -18
- mindsdb/api/mongo/responders/get_free_monitoring_status.py +0 -14
- mindsdb/api/mongo/responders/get_parameter.py +0 -23
- mindsdb/api/mongo/responders/getlog.py +0 -14
- mindsdb/api/mongo/responders/host_info.py +0 -28
- mindsdb/api/mongo/responders/insert.py +0 -270
- mindsdb/api/mongo/responders/is_master.py +0 -20
- mindsdb/api/mongo/responders/is_master_lower.py +0 -13
- mindsdb/api/mongo/responders/list_collections.py +0 -55
- mindsdb/api/mongo/responders/list_databases.py +0 -37
- mindsdb/api/mongo/responders/list_indexes.py +0 -22
- mindsdb/api/mongo/responders/ping.py +0 -13
- mindsdb/api/mongo/responders/recv_chunk_start.py +0 -13
- mindsdb/api/mongo/responders/replsetgetstatus.py +0 -13
- mindsdb/api/mongo/responders/sasl_continue.py +0 -34
- mindsdb/api/mongo/responders/sasl_start.py +0 -33
- mindsdb/api/mongo/responders/update_range_deletions.py +0 -12
- mindsdb/api/mongo/responders/whatsmyuri.py +0 -18
- mindsdb/api/mongo/server.py +0 -388
- mindsdb/api/mongo/start.py +0 -15
- mindsdb/api/mongo/utilities/__init__.py +0 -0
- {mindsdb-25.8.3.0.dist-info → mindsdb-25.9.1.0.dist-info}/WHEEL +0 -0
- {mindsdb-25.8.3.0.dist-info → mindsdb-25.9.1.0.dist-info}/licenses/LICENSE +0 -0
- {mindsdb-25.8.3.0.dist-info → mindsdb-25.9.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
2
|
+
from starlette.responses import JSONResponse
|
|
3
|
+
from starlette.requests import Request
|
|
4
|
+
from http import HTTPStatus
|
|
5
|
+
from typing import Optional
|
|
6
|
+
import secrets
|
|
7
|
+
import hmac
|
|
8
|
+
import hashlib
|
|
9
|
+
import os
|
|
10
|
+
import traceback
|
|
11
|
+
|
|
12
|
+
from mindsdb.utilities import log
|
|
13
|
+
from mindsdb.utilities.config import config
|
|
14
|
+
|
|
15
|
+
logger = log.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
SECRET_KEY = os.environ.get("AUTH_SECRET_KEY") or secrets.token_urlsafe(32)
|
|
18
|
+
# We store token (fingerprints) in memory, which means everyone is logged out if the process restarts
|
|
19
|
+
TOKENS = []
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_pat_fingerprint(token: str) -> str:
|
|
23
|
+
"""Hash the token with HMAC-SHA256 using secret_key as pepper."""
|
|
24
|
+
return hmac.new(SECRET_KEY.encode(), token.encode(), hashlib.sha256).hexdigest()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def generate_pat() -> str:
|
|
28
|
+
logger.debug("Generating new auth token")
|
|
29
|
+
token = "pat_" + secrets.token_urlsafe(32)
|
|
30
|
+
TOKENS.append(get_pat_fingerprint(token))
|
|
31
|
+
return token
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def verify_pat(raw_token: str) -> bool:
|
|
35
|
+
"""Verify if the raw_token matches a stored fingerprint.
|
|
36
|
+
Returns token_id if valid, None if not.
|
|
37
|
+
"""
|
|
38
|
+
if not raw_token:
|
|
39
|
+
return False
|
|
40
|
+
fp = get_pat_fingerprint(raw_token)
|
|
41
|
+
for stored_fp in TOKENS:
|
|
42
|
+
if hmac.compare_digest(fp, stored_fp):
|
|
43
|
+
return True
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def revoke_pat(raw_token: str) -> bool:
|
|
48
|
+
"""Revoke raw_token from active tokens"""
|
|
49
|
+
if not raw_token:
|
|
50
|
+
return False
|
|
51
|
+
fp = get_pat_fingerprint(raw_token)
|
|
52
|
+
for stored_fp in TOKENS:
|
|
53
|
+
if hmac.compare_digest(fp, stored_fp):
|
|
54
|
+
TOKENS.remove(stored_fp)
|
|
55
|
+
return True
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class PATAuthMiddleware(BaseHTTPMiddleware):
|
|
60
|
+
def _extract_bearer(self, request: Request) -> Optional[str]:
|
|
61
|
+
h = request.headers.get("Authorization")
|
|
62
|
+
if not h or not h.startswith("Bearer "):
|
|
63
|
+
return None
|
|
64
|
+
return h.split(" ", 1)[1].strip() or None
|
|
65
|
+
|
|
66
|
+
async def dispatch(self, request: Request, call_next):
|
|
67
|
+
if config.get("auth", {}).get("http_auth_enabled", False) is False:
|
|
68
|
+
return await call_next(request)
|
|
69
|
+
|
|
70
|
+
token = self._extract_bearer(request)
|
|
71
|
+
if not token or not verify_pat(token):
|
|
72
|
+
return JSONResponse({"detail": "Unauthorized"}, status_code=HTTPStatus.UNAUTHORIZED)
|
|
73
|
+
|
|
74
|
+
request.state.user = config["auth"].get("username")
|
|
75
|
+
return await call_next(request)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Used by mysql and postgres protocols
|
|
79
|
+
def check_auth(username, password, scramble_func, salt, company_id, config):
|
|
80
|
+
try:
|
|
81
|
+
hardcoded_user = config["auth"].get("username")
|
|
82
|
+
hardcoded_password = config["auth"].get("password")
|
|
83
|
+
if hardcoded_password is None:
|
|
84
|
+
hardcoded_password = ""
|
|
85
|
+
hardcoded_password_hash = scramble_func(hardcoded_password, salt)
|
|
86
|
+
hardcoded_password = hardcoded_password.encode()
|
|
87
|
+
|
|
88
|
+
if password is None:
|
|
89
|
+
password = ""
|
|
90
|
+
if isinstance(password, str):
|
|
91
|
+
password = password.encode()
|
|
92
|
+
|
|
93
|
+
if username != hardcoded_user:
|
|
94
|
+
logger.warning(f"Check auth, user={username}: user mismatch")
|
|
95
|
+
return {"success": False}
|
|
96
|
+
|
|
97
|
+
if password != hardcoded_password and password != hardcoded_password_hash:
|
|
98
|
+
logger.warning(f"check auth, user={username}: password mismatch")
|
|
99
|
+
return {"success": False}
|
|
100
|
+
|
|
101
|
+
logger.info(f"Check auth, user={username}: Ok")
|
|
102
|
+
return {"success": True, "username": username}
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.error(f"Check auth, user={username}: ERROR")
|
|
105
|
+
logger.error(e)
|
|
106
|
+
logger.error(traceback.format_exc())
|
mindsdb/api/http/initialize.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import os
|
|
2
|
-
import secrets
|
|
3
2
|
import mimetypes
|
|
4
3
|
import threading
|
|
5
4
|
import traceback
|
|
@@ -27,7 +26,7 @@ from mindsdb.api.http.namespaces.chatbots import ns_conf as chatbots_ns
|
|
|
27
26
|
from mindsdb.api.http.namespaces.jobs import ns_conf as jobs_ns
|
|
28
27
|
from mindsdb.api.http.namespaces.config import ns_conf as conf_ns
|
|
29
28
|
from mindsdb.api.http.namespaces.databases import ns_conf as databases_ns
|
|
30
|
-
from mindsdb.api.http.namespaces.default import ns_conf as default_ns
|
|
29
|
+
from mindsdb.api.http.namespaces.default import ns_conf as default_ns
|
|
31
30
|
from mindsdb.api.http.namespaces.file import ns_conf as file_ns
|
|
32
31
|
from mindsdb.api.http.namespaces.handlers import ns_conf as handlers_ns
|
|
33
32
|
from mindsdb.api.http.namespaces.knowledge_bases import ns_conf as knowledge_bases_ns
|
|
@@ -53,6 +52,7 @@ from mindsdb.utilities.json_encoder import CustomJSONProvider
|
|
|
53
52
|
from mindsdb.utilities.ps import is_pid_listen_port, wait_func_is_true
|
|
54
53
|
from mindsdb.utilities.sentry import sentry_sdk # noqa: F401
|
|
55
54
|
from mindsdb.utilities.otel import trace # noqa: F401
|
|
55
|
+
from mindsdb.api.common.middleware import verify_pat
|
|
56
56
|
|
|
57
57
|
logger = log.getLogger(__name__)
|
|
58
58
|
|
|
@@ -314,12 +314,19 @@ def initialize_app(config, no_studio):
|
|
|
314
314
|
ctx.set_default()
|
|
315
315
|
config = Config()
|
|
316
316
|
|
|
317
|
+
h = request.headers.get("Authorization")
|
|
318
|
+
if not h or not h.startswith("Bearer "):
|
|
319
|
+
bearer = None
|
|
320
|
+
else:
|
|
321
|
+
bearer = h.split(" ", 1)[1].strip() or None
|
|
322
|
+
|
|
317
323
|
# region routes where auth is required
|
|
318
324
|
if (
|
|
319
325
|
config["auth"]["http_auth_enabled"] is True
|
|
320
326
|
and any(request.path.startswith(f"/api{ns.path}") for ns in protected_namespaces)
|
|
321
|
-
and
|
|
327
|
+
and verify_pat(bearer) is False
|
|
322
328
|
):
|
|
329
|
+
logger.debug(f"Auth failed for path {request.path}")
|
|
323
330
|
return http_error(
|
|
324
331
|
HTTPStatus.UNAUTHORIZED,
|
|
325
332
|
"Unauthorized",
|
|
@@ -340,29 +347,23 @@ def initialize_app(config, no_studio):
|
|
|
340
347
|
except Exception:
|
|
341
348
|
user_id = 0
|
|
342
349
|
|
|
343
|
-
try:
|
|
344
|
-
session_id = request.cookies.get("session")
|
|
345
|
-
except Exception:
|
|
346
|
-
session_id = "unknown"
|
|
347
|
-
|
|
348
350
|
if company_id is not None:
|
|
349
351
|
try:
|
|
350
352
|
company_id = int(company_id)
|
|
351
353
|
except Exception as e:
|
|
352
|
-
logger.error(f"
|
|
354
|
+
logger.error(f"Could not parse company id: {company_id} | exception: {e}")
|
|
353
355
|
company_id = None
|
|
354
356
|
|
|
355
357
|
if user_class is not None:
|
|
356
358
|
try:
|
|
357
359
|
user_class = int(user_class)
|
|
358
360
|
except Exception as e:
|
|
359
|
-
logger.error(f"
|
|
361
|
+
logger.error(f"Could not parse user_class: {user_class} | exception: {e}")
|
|
360
362
|
user_class = 0
|
|
361
363
|
else:
|
|
362
364
|
user_class = 0
|
|
363
365
|
|
|
364
366
|
ctx.user_id = user_id
|
|
365
|
-
ctx.session_id = session_id
|
|
366
367
|
ctx.company_id = company_id
|
|
367
368
|
ctx.user_class = user_class
|
|
368
369
|
ctx.email_confirmed = email_confirmed
|
|
@@ -394,14 +395,11 @@ def initialize_flask(config, init_static_thread, no_studio):
|
|
|
394
395
|
FlaskInstrumentor().instrument_app(app)
|
|
395
396
|
RequestsInstrumentor().instrument()
|
|
396
397
|
|
|
397
|
-
app.config["SECRET_KEY"] = os.environ.get("FLASK_SECRET_KEY", secrets.token_hex(32))
|
|
398
|
-
app.config["SESSION_COOKIE_NAME"] = "session"
|
|
399
|
-
app.config["PERMANENT_SESSION_LIFETIME"] = config["auth"]["http_permanent_session_lifetime"]
|
|
400
398
|
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 60
|
|
401
399
|
app.config["SWAGGER_HOST"] = "http://localhost:8000/mindsdb"
|
|
402
400
|
app.json = CustomJSONProvider()
|
|
403
401
|
|
|
404
|
-
authorizations = {"apikey": {"type": "
|
|
402
|
+
authorizations = {"apikey": {"type": "apiKey", "in": "header", "name": "Authorization"}}
|
|
405
403
|
|
|
406
404
|
logger.debug("Creating swagger API..")
|
|
407
405
|
api = Swagger_Api(
|
|
@@ -4,7 +4,7 @@ import time
|
|
|
4
4
|
import urllib
|
|
5
5
|
|
|
6
6
|
import requests
|
|
7
|
-
from flask import redirect, request,
|
|
7
|
+
from flask import redirect, request, url_for
|
|
8
8
|
from flask_restx import Resource
|
|
9
9
|
|
|
10
10
|
from mindsdb.api.http.namespaces.configs.auth import ns_conf
|
|
@@ -21,9 +21,7 @@ def get_access_token() -> str:
|
|
|
21
21
|
Returns:
|
|
22
22
|
str: token
|
|
23
23
|
"""
|
|
24
|
-
return (
|
|
25
|
-
Config().get("auth", {}).get("oauth", {}).get("tokens", {}).get("access_token")
|
|
26
|
-
)
|
|
24
|
+
return Config().get("auth", {}).get("oauth", {}).get("tokens", {}).get("access_token")
|
|
27
25
|
|
|
28
26
|
|
|
29
27
|
def request_user_info(access_token: str = None) -> dict:
|
|
@@ -58,7 +56,7 @@ def request_user_info(access_token: str = None) -> dict:
|
|
|
58
56
|
@ns_conf.hide
|
|
59
57
|
class Auth(Resource):
|
|
60
58
|
@ns_conf.doc(params={"code": "authentification code"})
|
|
61
|
-
@api_endpoint_metrics(
|
|
59
|
+
@api_endpoint_metrics("GET", "/auth/code")
|
|
62
60
|
def get(self):
|
|
63
61
|
"""callback from auth server if authentification is successful"""
|
|
64
62
|
config = Config()
|
|
@@ -72,9 +70,7 @@ class Auth(Resource):
|
|
|
72
70
|
client_id = oauth_meta["client_id"]
|
|
73
71
|
client_secret = oauth_meta["client_secret"]
|
|
74
72
|
auth_server = oauth_meta["server_host"]
|
|
75
|
-
client_basic = base64.b64encode(
|
|
76
|
-
f"{client_id}:{client_secret}".encode()
|
|
77
|
-
).decode()
|
|
73
|
+
client_basic = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode()
|
|
78
74
|
|
|
79
75
|
redirect_uri = f"https://{public_hostname}{request.path}"
|
|
80
76
|
response = requests.post(
|
|
@@ -115,7 +111,7 @@ class Auth(Resource):
|
|
|
115
111
|
"public_hostname": public_hostname,
|
|
116
112
|
"ami_id": aws_meta_data.get("ami-id"),
|
|
117
113
|
},
|
|
118
|
-
headers={"Authorization": f
|
|
114
|
+
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
|
119
115
|
timeout=5,
|
|
120
116
|
)
|
|
121
117
|
if resp.status_code != 200:
|
|
@@ -123,10 +119,6 @@ class Auth(Resource):
|
|
|
123
119
|
except Exception as e:
|
|
124
120
|
logger.warning(f"Cant't send request to cloud server: {e}")
|
|
125
121
|
|
|
126
|
-
session["username"] = user_data["name"]
|
|
127
|
-
session["auth_provider"] = "cloud"
|
|
128
|
-
session.permanent = True
|
|
129
|
-
|
|
130
122
|
if request.path.endswith("/auth/callback/cloud_home"):
|
|
131
123
|
return redirect(f"https://{auth_server}")
|
|
132
124
|
else:
|
|
@@ -140,7 +132,7 @@ class CloudLoginRoute(Resource):
|
|
|
140
132
|
responses={302: "Redirect to auth server"},
|
|
141
133
|
params={"location": "final redirection should lead to that location"},
|
|
142
134
|
)
|
|
143
|
-
@api_endpoint_metrics(
|
|
135
|
+
@api_endpoint_metrics("GET", "/auth/cloud_login")
|
|
144
136
|
def get(self):
|
|
145
137
|
"""redirect to cloud login form"""
|
|
146
138
|
location = request.args.get("location")
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
from flask import request, session
|
|
1
|
+
from flask import request
|
|
4
2
|
from flask_restx import Resource
|
|
5
3
|
from flask_restx import fields
|
|
6
4
|
|
|
@@ -10,143 +8,113 @@ from mindsdb.api.http.utils import http_error
|
|
|
10
8
|
from mindsdb.metrics.metrics import api_endpoint_metrics
|
|
11
9
|
from mindsdb.utilities.config import Config
|
|
12
10
|
from mindsdb.utilities import log
|
|
11
|
+
from mindsdb.api.common.middleware import generate_pat, revoke_pat, verify_pat
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
logger = log.getLogger(__name__)
|
|
16
15
|
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
''' checking whether current user is authenticated
|
|
20
|
-
|
|
21
|
-
Returns:
|
|
22
|
-
bool: True if user authentication is approved
|
|
23
|
-
'''
|
|
24
|
-
config = Config()
|
|
25
|
-
if config['auth']['http_auth_enabled'] is False:
|
|
26
|
-
return True
|
|
27
|
-
|
|
28
|
-
if config['auth'].get('provider') == 'cloud':
|
|
29
|
-
if isinstance(session.get('username'), str) is False:
|
|
30
|
-
return False
|
|
31
|
-
|
|
32
|
-
if config['auth']['oauth']['tokens']['expires_at'] < time.time():
|
|
33
|
-
return False
|
|
34
|
-
|
|
35
|
-
return True
|
|
36
|
-
|
|
37
|
-
return session.get('username') == config['auth']['username']
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
@ns_conf.route('/login', methods=['POST'])
|
|
17
|
+
@ns_conf.route("/login", methods=["POST"])
|
|
41
18
|
class LoginRoute(Resource):
|
|
42
19
|
@ns_conf.doc(
|
|
43
|
-
responses={
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
body=ns_conf.model('request_login', {
|
|
49
|
-
'username': fields.String(description='Username'),
|
|
50
|
-
'password': fields.String(description='Password')
|
|
51
|
-
})
|
|
20
|
+
responses={200: "Success", 400: "Error in username or password", 401: "Invalid username or password"},
|
|
21
|
+
body=ns_conf.model(
|
|
22
|
+
"request_login",
|
|
23
|
+
{"username": fields.String(description="Username"), "password": fields.String(description="Password")},
|
|
24
|
+
),
|
|
52
25
|
)
|
|
53
|
-
@api_endpoint_metrics(
|
|
26
|
+
@api_endpoint_metrics("POST", "/default/login")
|
|
54
27
|
def post(self):
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
password = request.json.get('password')
|
|
28
|
+
"""Check user's credentials and creates a session"""
|
|
29
|
+
username = request.json.get("username")
|
|
30
|
+
password = request.json.get("password")
|
|
59
31
|
if (
|
|
60
|
-
isinstance(username, str) is False
|
|
61
|
-
or
|
|
32
|
+
isinstance(username, str) is False
|
|
33
|
+
or len(username) == 0
|
|
34
|
+
or isinstance(password, str) is False
|
|
35
|
+
or len(password) == 0
|
|
62
36
|
):
|
|
63
|
-
return http_error(
|
|
64
|
-
400, 'Error in username or password',
|
|
65
|
-
'Username and password should be string'
|
|
66
|
-
)
|
|
37
|
+
return http_error(400, "Error in username or password", "Username and password should be string")
|
|
67
38
|
|
|
68
39
|
config = Config()
|
|
69
|
-
inline_username = config[
|
|
70
|
-
inline_password = config[
|
|
40
|
+
inline_username = config["auth"]["username"]
|
|
41
|
+
inline_password = config["auth"]["password"]
|
|
71
42
|
|
|
72
|
-
if
|
|
73
|
-
username
|
|
74
|
-
or password != inline_password
|
|
75
|
-
):
|
|
76
|
-
return http_error(
|
|
77
|
-
401, 'Forbidden',
|
|
78
|
-
'Invalid username or password'
|
|
79
|
-
)
|
|
43
|
+
if username != inline_username or password != inline_password:
|
|
44
|
+
return http_error(401, "Forbidden", "Invalid username or password")
|
|
80
45
|
|
|
81
|
-
|
|
82
|
-
session['username'] = username
|
|
83
|
-
session.permanent = True
|
|
46
|
+
logger.info(f"User '{username}' logged in successfully")
|
|
84
47
|
|
|
85
|
-
return
|
|
48
|
+
return {"token": generate_pat()}, 200
|
|
86
49
|
|
|
87
50
|
|
|
88
|
-
@ns_conf.route(
|
|
51
|
+
@ns_conf.route("/logout", methods=["POST"])
|
|
89
52
|
class LogoutRoute(Resource):
|
|
90
|
-
@ns_conf.doc(
|
|
91
|
-
|
|
92
|
-
200: 'Success'
|
|
93
|
-
}
|
|
94
|
-
)
|
|
95
|
-
@api_endpoint_metrics('POST', '/default/logout')
|
|
53
|
+
@ns_conf.doc(responses={200: "Success"})
|
|
54
|
+
@api_endpoint_metrics("POST", "/default/logout")
|
|
96
55
|
def post(self):
|
|
97
|
-
|
|
98
|
-
|
|
56
|
+
# We can't forcibly log out a user with the
|
|
57
|
+
h = request.headers.get("Authorization")
|
|
58
|
+
if not h or not h.startswith("Bearer "):
|
|
59
|
+
bearer = None
|
|
60
|
+
else:
|
|
61
|
+
bearer = h.split(" ", 1)[1].strip() or None
|
|
62
|
+
revoke_pat(bearer)
|
|
63
|
+
return "", 200
|
|
99
64
|
|
|
100
65
|
|
|
101
|
-
@ns_conf.route(
|
|
66
|
+
@ns_conf.route("/status")
|
|
102
67
|
class StatusRoute(Resource):
|
|
103
68
|
@ns_conf.doc(
|
|
104
|
-
responses={
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
69
|
+
responses={200: "Success"},
|
|
70
|
+
model=ns_conf.model(
|
|
71
|
+
"response_status",
|
|
72
|
+
{
|
|
73
|
+
"environment": fields.String(description="The name of current environment: cloud, local or other"),
|
|
74
|
+
"mindsdb_version": fields.String(description="Current version of mindsdb"),
|
|
75
|
+
"auth": fields.Nested(
|
|
76
|
+
ns_conf.model(
|
|
77
|
+
"response_status_auth",
|
|
78
|
+
{
|
|
79
|
+
"confirmed": fields.Boolean(description="is current user authenticated"),
|
|
80
|
+
"required": fields.Boolean(description="is authenticated required"),
|
|
81
|
+
"provider": fields.Boolean(description="current authenticated provider: local of 3d-party"),
|
|
82
|
+
},
|
|
83
|
+
)
|
|
84
|
+
),
|
|
85
|
+
},
|
|
86
|
+
),
|
|
118
87
|
)
|
|
119
|
-
@api_endpoint_metrics(
|
|
88
|
+
@api_endpoint_metrics("GET", "/default/status")
|
|
120
89
|
def get(self):
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
environment = 'local'
|
|
90
|
+
"""returns auth and environment data"""
|
|
91
|
+
environment = "local"
|
|
124
92
|
config = Config()
|
|
125
93
|
|
|
126
|
-
environment = config.get(
|
|
94
|
+
environment = config.get("environment")
|
|
127
95
|
if environment is None:
|
|
128
|
-
if config.get(
|
|
129
|
-
environment =
|
|
130
|
-
elif config.get(
|
|
131
|
-
environment =
|
|
96
|
+
if config.get("cloud", False):
|
|
97
|
+
environment = "cloud"
|
|
98
|
+
elif config.get("aws_marketplace", False):
|
|
99
|
+
environment = "aws_marketplace"
|
|
132
100
|
else:
|
|
133
|
-
environment =
|
|
101
|
+
environment = "local"
|
|
134
102
|
|
|
135
|
-
auth_provider =
|
|
136
|
-
if config[
|
|
137
|
-
if config[
|
|
138
|
-
auth_provider = config[
|
|
103
|
+
auth_provider = "disabled"
|
|
104
|
+
if config["auth"]["http_auth_enabled"] is True:
|
|
105
|
+
if config["auth"].get("provider") is not None:
|
|
106
|
+
auth_provider = config["auth"].get("provider")
|
|
139
107
|
else:
|
|
140
|
-
auth_provider =
|
|
108
|
+
auth_provider = "local"
|
|
141
109
|
|
|
142
110
|
resp = {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
111
|
+
"mindsdb_version": mindsdb_version,
|
|
112
|
+
"environment": environment,
|
|
113
|
+
"auth": {
|
|
114
|
+
"confirmed": verify_pat(request.headers.get("Authorization", "").replace("Bearer ", "")),
|
|
115
|
+
"http_auth_enabled": config["auth"]["http_auth_enabled"],
|
|
116
|
+
"provider": auth_provider,
|
|
117
|
+
},
|
|
150
118
|
}
|
|
151
119
|
|
|
152
120
|
return resp
|
mindsdb/api/http/start.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import gc
|
|
2
|
+
|
|
2
3
|
gc.disable()
|
|
3
4
|
|
|
4
5
|
from flask import Flask
|
|
5
|
-
from waitress import serve
|
|
6
6
|
|
|
7
7
|
from mindsdb.api.http.initialize import initialize_app
|
|
8
8
|
from mindsdb.interfaces.storage import db
|
|
@@ -10,6 +10,16 @@ from mindsdb.utilities import log
|
|
|
10
10
|
from mindsdb.utilities.config import config
|
|
11
11
|
from mindsdb.utilities.functions import init_lexer_parsers
|
|
12
12
|
from mindsdb.integrations.libs.ml_exec_base import process_cache
|
|
13
|
+
from mindsdb.api.common.middleware import PATAuthMiddleware
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
from starlette.applications import Starlette
|
|
17
|
+
from starlette.routing import Mount
|
|
18
|
+
from starlette.middleware.wsgi import WSGIMiddleware
|
|
19
|
+
import uvicorn
|
|
20
|
+
|
|
21
|
+
from mindsdb.api.a2a import get_a2a_app
|
|
22
|
+
from mindsdb.api.mcp import get_mcp_app
|
|
13
23
|
|
|
14
24
|
gc.enable()
|
|
15
25
|
|
|
@@ -23,50 +33,21 @@ def start(verbose, no_studio, app: Flask = None):
|
|
|
23
33
|
if app is None:
|
|
24
34
|
app = initialize_app(config, no_studio)
|
|
25
35
|
|
|
26
|
-
port = config[
|
|
27
|
-
host = config[
|
|
28
|
-
server_type = config['api']['http']['server']['type']
|
|
29
|
-
server_config = config['api']['http']['server']['config']
|
|
30
|
-
|
|
36
|
+
port = config["api"]["http"]["port"]
|
|
37
|
+
host = config["api"]["http"]["host"]
|
|
31
38
|
process_cache.init()
|
|
32
39
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
elif server_type == "flask":
|
|
42
|
-
logger.debug("Serving HTTP app with flask...")
|
|
43
|
-
# that will 'disable access' log in console
|
|
44
|
-
|
|
45
|
-
app.run(debug=False, port=port, host=host, **server_config)
|
|
46
|
-
elif server_type == 'gunicorn':
|
|
47
|
-
try:
|
|
48
|
-
from mindsdb.api.http.gunicorn_wrapper import StandaloneApplication
|
|
49
|
-
except ImportError:
|
|
50
|
-
logger.error(
|
|
51
|
-
"Gunicorn server is not available by default. If you wish to use it, please install 'gunicorn'"
|
|
52
|
-
)
|
|
53
|
-
return
|
|
54
|
-
|
|
55
|
-
def post_fork(arbiter, worker):
|
|
56
|
-
db.engine.dispose()
|
|
40
|
+
routes = []
|
|
41
|
+
# Specific mounts FIRST
|
|
42
|
+
a2a = get_a2a_app()
|
|
43
|
+
a2a.add_middleware(PATAuthMiddleware)
|
|
44
|
+
mcp = get_mcp_app()
|
|
45
|
+
mcp.add_middleware(PATAuthMiddleware)
|
|
46
|
+
routes.append(Mount("/a2a", app=a2a))
|
|
47
|
+
routes.append(Mount("/mcp", app=mcp))
|
|
57
48
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
not work for processes created by ProcessPoolExecutor, because they execute forever. We need to explicitly
|
|
61
|
-
call 'shutdown' for such processes before exiting.
|
|
62
|
-
"""
|
|
63
|
-
from mindsdb.integrations.libs.process_cache import process_cache
|
|
64
|
-
process_cache.shutdown(wait=True)
|
|
49
|
+
# Root app LAST so it won't shadow the others
|
|
50
|
+
routes.append(Mount("/", app=WSGIMiddleware(app)))
|
|
65
51
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
'post_fork': post_fork,
|
|
69
|
-
'worker_exit': before_worker_exit,
|
|
70
|
-
**server_config
|
|
71
|
-
}
|
|
72
|
-
StandaloneApplication(app, options).run()
|
|
52
|
+
# Setting logging to None makes uvicorn use the existing logging configuration
|
|
53
|
+
uvicorn.run(Starlette(routes=routes, debug=verbose), host=host, port=int(port), log_level=None, log_config=None)
|
mindsdb/api/litellm/start.py
CHANGED
|
@@ -24,12 +24,12 @@ async def start_async(verbose=False):
|
|
|
24
24
|
project_name = config.cmd_args.project or "mindsdb"
|
|
25
25
|
|
|
26
26
|
# Get MCP server connection details
|
|
27
|
-
mcp_host =
|
|
28
|
-
mcp_port = int(config.get(
|
|
27
|
+
mcp_host = "127.0.0.1"
|
|
28
|
+
mcp_port = int(config.get("api", {}).get("http", {}).get("port", 47334))
|
|
29
29
|
|
|
30
30
|
# Get LiteLLM server settings
|
|
31
|
-
litellm_host = config.get(
|
|
32
|
-
litellm_port = int(config.get(
|
|
31
|
+
litellm_host = config.get("api", {}).get("litellm", {}).get("host", "0.0.0.0")
|
|
32
|
+
litellm_port = int(config.get("api", {}).get("litellm", {}).get("port", 8000))
|
|
33
33
|
|
|
34
34
|
logger.info(f"Starting LiteLLM server for agent '{agent_name}' in project '{project_name}'")
|
|
35
35
|
logger.info(f"Connecting to MCP server at {mcp_host}:{mcp_port}")
|
|
@@ -41,7 +41,7 @@ async def start_async(verbose=False):
|
|
|
41
41
|
mcp_host=mcp_host,
|
|
42
42
|
mcp_port=mcp_port,
|
|
43
43
|
host=litellm_host,
|
|
44
|
-
port=litellm_port
|
|
44
|
+
port=litellm_port,
|
|
45
45
|
)
|
|
46
46
|
|
|
47
47
|
|
|
@@ -52,6 +52,7 @@ def start(verbose=False):
|
|
|
52
52
|
verbose (bool): Whether to enable verbose logging
|
|
53
53
|
"""
|
|
54
54
|
from mindsdb.interfaces.storage import db
|
|
55
|
+
|
|
55
56
|
db.init()
|
|
56
57
|
|
|
57
58
|
# Run the async function in the event loop
|
|
@@ -64,10 +65,10 @@ def start(verbose=False):
|
|
|
64
65
|
config = Config()
|
|
65
66
|
agent_name = config.cmd_args.agent
|
|
66
67
|
project_name = config.cmd_args.project or "mindsdb"
|
|
67
|
-
mcp_host = config.get(
|
|
68
|
-
mcp_port = int(config.get(
|
|
69
|
-
litellm_host = config.get(
|
|
70
|
-
litellm_port = int(config.get(
|
|
68
|
+
mcp_host = config.get("api", {}).get("mcp", {}).get("host", "127.0.0.1")
|
|
69
|
+
mcp_port = int(config.get("api", {}).get("mcp", {}).get("port", 47337))
|
|
70
|
+
litellm_host = config.get("api", {}).get("litellm", {}).get("host", "0.0.0.0")
|
|
71
|
+
litellm_port = int(config.get("api", {}).get("litellm", {}).get("port", 8000))
|
|
71
72
|
|
|
72
73
|
return run_server(
|
|
73
74
|
agent_name=agent_name,
|
|
@@ -75,7 +76,7 @@ def start(verbose=False):
|
|
|
75
76
|
mcp_host=mcp_host,
|
|
76
77
|
mcp_port=mcp_port,
|
|
77
78
|
host=litellm_host,
|
|
78
|
-
port=litellm_port
|
|
79
|
+
port=litellm_port,
|
|
79
80
|
)
|
|
80
81
|
else:
|
|
81
82
|
logger.error("LiteLLM server initialization failed")
|