MindsDB 25.8.3.0__py3-none-any.whl → 25.9.1.1__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 +3 -45
- 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/executor/utilities/mysql_to_duckdb_functions.py +466 -18
- mindsdb/api/executor/utilities/sql.py +9 -31
- mindsdb/api/http/initialize.py +34 -43
- 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/namespaces/file.py +9 -3
- mindsdb/api/http/namespaces/handlers.py +77 -87
- mindsdb/api/http/start.py +29 -47
- 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/autogluon_handler/requirements.txt +1 -1
- mindsdb/integrations/handlers/autosklearn_handler/requirements.txt +1 -1
- 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/flaml_handler/requirements.txt +1 -1
- 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/lightfm_handler/requirements.txt +1 -1
- mindsdb/integrations/handlers/lightwood_handler/requirements.txt +4 -4
- 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/handlers/tpot_handler/requirements.txt +1 -1
- mindsdb/integrations/libs/base.py +1 -1
- mindsdb/integrations/libs/llm/config.py +15 -0
- mindsdb/integrations/libs/llm/utils.py +15 -0
- mindsdb/interfaces/agents/constants.py +1 -0
- mindsdb/interfaces/agents/langchain_agent.py +4 -0
- mindsdb/interfaces/agents/providers.py +20 -0
- mindsdb/interfaces/knowledge_base/controller.py +25 -7
- mindsdb/utilities/config.py +15 -158
- mindsdb/utilities/log.py +0 -25
- mindsdb/utilities/render/sqlalchemy_render.py +7 -1
- mindsdb/utilities/starters.py +0 -39
- {mindsdb-25.8.3.0.dist-info → mindsdb-25.9.1.1.dist-info}/METADATA +269 -267
- {mindsdb-25.8.3.0.dist-info → mindsdb-25.9.1.1.dist-info}/RECORD +62 -105
- 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.1.dist-info}/WHEEL +0 -0
- {mindsdb-25.8.3.0.dist-info → mindsdb-25.9.1.1.dist-info}/licenses/LICENSE +0 -0
- {mindsdb-25.8.3.0.dist-info → mindsdb-25.9.1.1.dist-info}/top_level.txt +0 -0
|
@@ -14,19 +14,7 @@ from mindsdb.utilities.exception import format_db_error_message
|
|
|
14
14
|
from mindsdb.utilities.functions import resolve_table_identifier, resolve_model_identifier
|
|
15
15
|
from mindsdb.utilities.json_encoder import CustomJSONEncoder
|
|
16
16
|
from mindsdb.utilities.render.sqlalchemy_render import SqlalchemyRender
|
|
17
|
-
from mindsdb.api.executor.utilities.mysql_to_duckdb_functions import
|
|
18
|
-
adapt_char_fn,
|
|
19
|
-
adapt_locate_fn,
|
|
20
|
-
adapt_unhex_fn,
|
|
21
|
-
adapt_format_fn,
|
|
22
|
-
adapt_sha2_fn,
|
|
23
|
-
adapt_length_fn,
|
|
24
|
-
adapt_regexp_substr_fn,
|
|
25
|
-
adapt_substring_index_fn,
|
|
26
|
-
adapt_curtime_fn,
|
|
27
|
-
adapt_timestampdiff_fn,
|
|
28
|
-
adapt_extract_fn,
|
|
29
|
-
)
|
|
17
|
+
from mindsdb.api.executor.utilities.mysql_to_duckdb_functions import mysql_to_duckdb_fnc
|
|
30
18
|
|
|
31
19
|
logger = log.getLogger(__name__)
|
|
32
20
|
|
|
@@ -196,25 +184,15 @@ def query_df(df, query, session=None):
|
|
|
196
184
|
node.parts = [node.parts[-1]]
|
|
197
185
|
return node
|
|
198
186
|
if isinstance(node, Function):
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
"format": adapt_format_fn,
|
|
207
|
-
"sha2": adapt_sha2_fn,
|
|
208
|
-
"length": adapt_length_fn,
|
|
209
|
-
"regexp_substr": adapt_regexp_substr_fn,
|
|
210
|
-
"substring_index": adapt_substring_index_fn,
|
|
211
|
-
"curtime": adapt_curtime_fn,
|
|
212
|
-
"timestampdiff": adapt_timestampdiff_fn,
|
|
213
|
-
"extract": adapt_extract_fn,
|
|
214
|
-
}
|
|
215
|
-
if fnc_name in mysql_to_duck_fn_map:
|
|
216
|
-
return mysql_to_duck_fn_map[fnc_name](node)
|
|
187
|
+
fnc = mysql_to_duckdb_fnc(node)
|
|
188
|
+
if fnc is not None:
|
|
189
|
+
node2 = fnc(node)
|
|
190
|
+
if node2 is not None:
|
|
191
|
+
# copy alias
|
|
192
|
+
node2.alias = node.alias
|
|
193
|
+
return node2
|
|
217
194
|
|
|
195
|
+
fnc_name = node.op.lower()
|
|
218
196
|
if fnc_name == "database" and len(node.args) == 0:
|
|
219
197
|
if session is not None:
|
|
220
198
|
cur_db = session.database
|
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
|
|
@@ -47,12 +46,13 @@ from mindsdb.interfaces.jobs.jobs_controller import JobsController
|
|
|
47
46
|
from mindsdb.interfaces.storage import db
|
|
48
47
|
from mindsdb.metrics.server import init_metrics
|
|
49
48
|
from mindsdb.utilities import log
|
|
50
|
-
from mindsdb.utilities.config import
|
|
49
|
+
from mindsdb.utilities.config import config
|
|
51
50
|
from mindsdb.utilities.context import context as ctx
|
|
52
51
|
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
|
|
|
@@ -95,7 +95,7 @@ def custom_output_json(data, code, headers=None):
|
|
|
95
95
|
return resp
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
def get_last_compatible_gui_version() -> Version:
|
|
98
|
+
def get_last_compatible_gui_version() -> Version | bool:
|
|
99
99
|
logger.debug("Getting last compatible frontend..")
|
|
100
100
|
try:
|
|
101
101
|
res = requests.get(
|
|
@@ -164,7 +164,6 @@ def get_last_compatible_gui_version() -> Version:
|
|
|
164
164
|
|
|
165
165
|
def get_current_gui_version() -> Version:
|
|
166
166
|
logger.debug("Getting current frontend version...")
|
|
167
|
-
config = Config()
|
|
168
167
|
static_path = Path(config["paths"]["static"])
|
|
169
168
|
version_txt_path = static_path.joinpath("version.txt")
|
|
170
169
|
|
|
@@ -181,7 +180,6 @@ def get_current_gui_version() -> Version:
|
|
|
181
180
|
|
|
182
181
|
def initialize_static():
|
|
183
182
|
logger.debug("Initializing static..")
|
|
184
|
-
config = Config()
|
|
185
183
|
last_gui_version_lv = get_last_compatible_gui_version()
|
|
186
184
|
current_gui_version_lv = get_current_gui_version()
|
|
187
185
|
required_gui_version = config["gui"].get("version")
|
|
@@ -190,17 +188,18 @@ def initialize_static():
|
|
|
190
188
|
required_gui_version_lv = parse_version(required_gui_version)
|
|
191
189
|
success = True
|
|
192
190
|
if current_gui_version_lv is None or required_gui_version_lv != current_gui_version_lv:
|
|
193
|
-
logger.debug("Updating gui..")
|
|
194
191
|
success = update_static(required_gui_version_lv)
|
|
195
192
|
else:
|
|
196
193
|
if last_gui_version_lv is False:
|
|
194
|
+
logger.debug(
|
|
195
|
+
"The number of the latest version has not been determined, "
|
|
196
|
+
f"so we will continue using the current version: {current_gui_version_lv}"
|
|
197
|
+
)
|
|
197
198
|
return False
|
|
198
199
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return True
|
|
203
|
-
logger.debug("Updating gui..")
|
|
200
|
+
if current_gui_version_lv == last_gui_version_lv:
|
|
201
|
+
logger.debug(f"The latest version is already in use: {current_gui_version_lv}")
|
|
202
|
+
return True
|
|
204
203
|
success = update_static(last_gui_version_lv)
|
|
205
204
|
|
|
206
205
|
if db.session:
|
|
@@ -208,21 +207,22 @@ def initialize_static():
|
|
|
208
207
|
return success
|
|
209
208
|
|
|
210
209
|
|
|
211
|
-
def initialize_app(
|
|
210
|
+
def initialize_app():
|
|
212
211
|
static_root = config["paths"]["static"]
|
|
213
212
|
logger.debug(f"Static route: {static_root}")
|
|
214
213
|
gui_exists = Path(static_root).joinpath("index.html").is_file()
|
|
215
214
|
logger.debug(f"Does GUI already exist.. {'YES' if gui_exists else 'NO'}")
|
|
216
215
|
init_static_thread = None
|
|
217
|
-
|
|
216
|
+
|
|
217
|
+
if config["gui"]["autoupdate"] is True or (config["gui"]["open_on_start"] is True and gui_exists is False):
|
|
218
218
|
init_static_thread = threading.Thread(target=initialize_static, name="initialize_static")
|
|
219
219
|
init_static_thread.start()
|
|
220
220
|
|
|
221
221
|
# Wait for static initialization.
|
|
222
|
-
if
|
|
222
|
+
if config["gui"]["open_on_start"] is True and init_static_thread is not None:
|
|
223
223
|
init_static_thread.join()
|
|
224
224
|
|
|
225
|
-
app, api = initialize_flask(config, init_static_thread
|
|
225
|
+
app, api = initialize_flask(config, init_static_thread)
|
|
226
226
|
Compress(app)
|
|
227
227
|
|
|
228
228
|
initialize_interfaces(app)
|
|
@@ -312,14 +312,20 @@ def initialize_app(config, no_studio):
|
|
|
312
312
|
def before_request():
|
|
313
313
|
logger.debug(f"HTTP {request.method}: {request.path}")
|
|
314
314
|
ctx.set_default()
|
|
315
|
-
|
|
315
|
+
|
|
316
|
+
h = request.headers.get("Authorization")
|
|
317
|
+
if not h or not h.startswith("Bearer "):
|
|
318
|
+
bearer = None
|
|
319
|
+
else:
|
|
320
|
+
bearer = h.split(" ", 1)[1].strip() or None
|
|
316
321
|
|
|
317
322
|
# region routes where auth is required
|
|
318
323
|
if (
|
|
319
324
|
config["auth"]["http_auth_enabled"] is True
|
|
320
325
|
and any(request.path.startswith(f"/api{ns.path}") for ns in protected_namespaces)
|
|
321
|
-
and
|
|
326
|
+
and verify_pat(bearer) is False
|
|
322
327
|
):
|
|
328
|
+
logger.debug(f"Auth failed for path {request.path}")
|
|
323
329
|
return http_error(
|
|
324
330
|
HTTPStatus.UNAUTHORIZED,
|
|
325
331
|
"Unauthorized",
|
|
@@ -340,29 +346,23 @@ def initialize_app(config, no_studio):
|
|
|
340
346
|
except Exception:
|
|
341
347
|
user_id = 0
|
|
342
348
|
|
|
343
|
-
try:
|
|
344
|
-
session_id = request.cookies.get("session")
|
|
345
|
-
except Exception:
|
|
346
|
-
session_id = "unknown"
|
|
347
|
-
|
|
348
349
|
if company_id is not None:
|
|
349
350
|
try:
|
|
350
351
|
company_id = int(company_id)
|
|
351
352
|
except Exception as e:
|
|
352
|
-
logger.error(f"
|
|
353
|
+
logger.error(f"Could not parse company id: {company_id} | exception: {e}")
|
|
353
354
|
company_id = None
|
|
354
355
|
|
|
355
356
|
if user_class is not None:
|
|
356
357
|
try:
|
|
357
358
|
user_class = int(user_class)
|
|
358
359
|
except Exception as e:
|
|
359
|
-
logger.error(f"
|
|
360
|
+
logger.error(f"Could not parse user_class: {user_class} | exception: {e}")
|
|
360
361
|
user_class = 0
|
|
361
362
|
else:
|
|
362
363
|
user_class = 0
|
|
363
364
|
|
|
364
365
|
ctx.user_id = user_id
|
|
365
|
-
ctx.session_id = session_id
|
|
366
366
|
ctx.company_id = company_id
|
|
367
367
|
ctx.user_class = user_class
|
|
368
368
|
ctx.email_confirmed = email_confirmed
|
|
@@ -371,21 +371,18 @@ def initialize_app(config, no_studio):
|
|
|
371
371
|
return app
|
|
372
372
|
|
|
373
373
|
|
|
374
|
-
def initialize_flask(config, init_static_thread
|
|
374
|
+
def initialize_flask(config, init_static_thread):
|
|
375
375
|
logger.debug("Initializing flask..")
|
|
376
376
|
# region required for windows https://github.com/mindsdb/mindsdb/issues/2526
|
|
377
377
|
mimetypes.add_type("text/css", ".css")
|
|
378
378
|
mimetypes.add_type("text/javascript", ".js")
|
|
379
379
|
# endregion
|
|
380
380
|
|
|
381
|
-
|
|
382
|
-
if
|
|
383
|
-
static_path = os.path.join(
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
kwargs["static_url_path"] = "/static"
|
|
387
|
-
kwargs["static_folder"] = static_path
|
|
388
|
-
logger.debug(f"Static path: {static_path}")
|
|
381
|
+
static_path = os.path.join(config["paths"]["static"], "static/")
|
|
382
|
+
if os.path.isabs(static_path) is False:
|
|
383
|
+
static_path = os.path.join(os.getcwd(), static_path)
|
|
384
|
+
kwargs = {"static_url_path": "/static", "static_folder": static_path}
|
|
385
|
+
logger.debug(f"Static path: {static_path}")
|
|
389
386
|
|
|
390
387
|
app = Flask(__name__, **kwargs)
|
|
391
388
|
init_metrics(app)
|
|
@@ -394,14 +391,11 @@ def initialize_flask(config, init_static_thread, no_studio):
|
|
|
394
391
|
FlaskInstrumentor().instrument_app(app)
|
|
395
392
|
RequestsInstrumentor().instrument()
|
|
396
393
|
|
|
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
394
|
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 60
|
|
401
395
|
app.config["SWAGGER_HOST"] = "http://localhost:8000/mindsdb"
|
|
402
396
|
app.json = CustomJSONProvider()
|
|
403
397
|
|
|
404
|
-
authorizations = {"apikey": {"type": "
|
|
398
|
+
authorizations = {"apikey": {"type": "apiKey", "in": "header", "name": "Authorization"}}
|
|
405
399
|
|
|
406
400
|
logger.debug("Creating swagger API..")
|
|
407
401
|
api = Swagger_Api(
|
|
@@ -418,8 +412,7 @@ def initialize_flask(config, init_static_thread, no_studio):
|
|
|
418
412
|
port = config["api"]["http"]["port"]
|
|
419
413
|
host = config["api"]["http"]["host"]
|
|
420
414
|
|
|
421
|
-
|
|
422
|
-
if not no_studio:
|
|
415
|
+
if config["gui"]["open_on_start"]:
|
|
423
416
|
if host in ("", "0.0.0.0"):
|
|
424
417
|
url = f"http://127.0.0.1:{port}/"
|
|
425
418
|
else:
|
|
@@ -443,8 +436,6 @@ def initialize_interfaces(app):
|
|
|
443
436
|
app.database_controller = DatabaseController()
|
|
444
437
|
app.file_controller = FileController()
|
|
445
438
|
app.jobs_controller = JobsController()
|
|
446
|
-
config = Config()
|
|
447
|
-
app.config_obj = config
|
|
448
439
|
|
|
449
440
|
|
|
450
441
|
def _open_webbrowser(url: str, pid: int, port: int, init_static_thread, static_folder):
|
|
@@ -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
|
|
@@ -3,6 +3,7 @@ import shutil
|
|
|
3
3
|
import tarfile
|
|
4
4
|
import tempfile
|
|
5
5
|
import zipfile
|
|
6
|
+
from pathlib import Path
|
|
6
7
|
from urllib.parse import urlparse
|
|
7
8
|
|
|
8
9
|
import multipart
|
|
@@ -60,7 +61,10 @@ class File(Resource):
|
|
|
60
61
|
|
|
61
62
|
def on_file(file):
|
|
62
63
|
nonlocal file_object
|
|
63
|
-
|
|
64
|
+
file_name = file.file_name.decode()
|
|
65
|
+
data["file"] = file_name
|
|
66
|
+
if Path(file_name).name != file_name:
|
|
67
|
+
raise ValueError(f"Wrong file name: {file_name}")
|
|
64
68
|
file_object = file.file_object
|
|
65
69
|
|
|
66
70
|
temp_dir_path = tempfile.mkdtemp(prefix="mindsdb_file_")
|
|
@@ -72,8 +76,9 @@ class File(Resource):
|
|
|
72
76
|
on_file=on_file,
|
|
73
77
|
config={
|
|
74
78
|
"UPLOAD_DIR": temp_dir_path.encode(), # bytes required
|
|
75
|
-
"UPLOAD_KEEP_FILENAME":
|
|
79
|
+
"UPLOAD_KEEP_FILENAME": False,
|
|
76
80
|
"UPLOAD_KEEP_EXTENSIONS": True,
|
|
81
|
+
"UPLOAD_DELETE_TMP": False,
|
|
77
82
|
"MAX_MEMORY_FILE_SIZE": 0,
|
|
78
83
|
},
|
|
79
84
|
)
|
|
@@ -93,6 +98,7 @@ class File(Resource):
|
|
|
93
98
|
except (AttributeError, ValueError, OSError):
|
|
94
99
|
logger.debug("Failed to flush file_object before closing.", exc_info=True)
|
|
95
100
|
file_object.close()
|
|
101
|
+
Path(file_object.name).rename(Path(file_object.name).parent / data["file"])
|
|
96
102
|
file_object = None
|
|
97
103
|
else:
|
|
98
104
|
data = request.json
|
|
@@ -101,7 +107,7 @@ class File(Resource):
|
|
|
101
107
|
return http_error(
|
|
102
108
|
400,
|
|
103
109
|
"File already exists",
|
|
104
|
-
f"File with name '{
|
|
110
|
+
f"File with name '{mindsdb_file_name}' already exists",
|
|
105
111
|
)
|
|
106
112
|
|
|
107
113
|
if data.get("source_type") == "url":
|