Skip to content

Commit 9ced5e1

Browse files
authored
Allow session-based HTTP authentication (#11911)
1 parent ec0ddc4 commit 9ced5e1

3 files changed

Lines changed: 83 additions & 11 deletions

File tree

mindsdb/api/http/initialize.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import secrets
23
import mimetypes
34
import threading
45
import webbrowser
@@ -24,7 +25,7 @@
2425
from mindsdb.api.http.namespaces.jobs import ns_conf as jobs_ns
2526
from mindsdb.api.http.namespaces.config import ns_conf as conf_ns
2627
from mindsdb.api.http.namespaces.databases import ns_conf as databases_ns
27-
from mindsdb.api.http.namespaces.default import ns_conf as default_ns
28+
from mindsdb.api.http.namespaces.default import ns_conf as default_ns, check_session_auth
2829
from mindsdb.api.http.namespaces.file import ns_conf as file_ns
2930
from mindsdb.api.http.namespaces.handlers import ns_conf as handlers_ns
3031
from mindsdb.api.http.namespaces.knowledge_bases import ns_conf as knowledge_bases_ns
@@ -44,7 +45,7 @@
4445
from mindsdb.interfaces.storage import db
4546
from mindsdb.metrics.server import init_metrics
4647
from mindsdb.utilities import log
47-
from mindsdb.utilities.config import config
48+
from mindsdb.utilities.config import config, HTTP_AUTH_TYPE
4849
from mindsdb.utilities.context import context as ctx
4950
from mindsdb.utilities.json_encoder import ORJSONProvider
5051
from mindsdb.utilities.ps import is_pid_listen_port, wait_func_is_true
@@ -324,10 +325,19 @@ def before_request():
324325
bearer = h.split(" ", 1)[1].strip() or None
325326

326327
# region routes where auth is required
328+
http_auth_type = config["auth"]["http_auth_type"]
327329
if (
328330
config["auth"]["http_auth_enabled"] is True
329331
and any(request.path.startswith(f"/api{ns.path}") for ns in protected_namespaces)
330-
and verify_pat(bearer) is False
332+
and (
333+
(http_auth_type == HTTP_AUTH_TYPE.SESSION and check_session_auth() is False)
334+
or (http_auth_type == HTTP_AUTH_TYPE.TOKEN and verify_pat(bearer) is False)
335+
or (
336+
http_auth_type == HTTP_AUTH_TYPE.SESSION_OR_TOKEN
337+
and check_session_auth() is False
338+
and verify_pat(bearer) is False
339+
)
340+
)
331341
):
332342
logger.debug(f"Auth failed for path {request.path}")
333343
return http_error(
@@ -392,13 +402,26 @@ def initialize_flask():
392402
app.config["SWAGGER_HOST"] = "http://localhost:8000/mindsdb"
393403
app.json = ORJSONProvider(app)
394404

395-
authorizations = {"apikey": {"type": "apiKey", "in": "header", "name": "Authorization"}}
405+
http_auth_type = config["auth"]["http_auth_type"]
406+
authorizations = {}
407+
security = []
408+
409+
if http_auth_type in (HTTP_AUTH_TYPE.SESSION, HTTP_AUTH_TYPE.SESSION_OR_TOKEN):
410+
app.config["SECRET_KEY"] = os.environ.get("FLASK_SECRET_KEY", secrets.token_hex(32))
411+
app.config["SESSION_COOKIE_NAME"] = "session"
412+
app.config["PERMANENT_SESSION_LIFETIME"] = config["auth"]["http_permanent_session_lifetime"]
413+
authorizations["session"] = {"type": "apiKey", "in": "cookie", "name": "session"}
414+
security.append(["session"])
415+
416+
if http_auth_type in (HTTP_AUTH_TYPE.TOKEN, HTTP_AUTH_TYPE.SESSION_OR_TOKEN):
417+
authorizations["bearer"] = {"type": "apiKey", "in": "header", "name": "Authorization"}
418+
security.append(["bearer"])
396419

397420
logger.debug("Creating swagger API..")
398421
api = Swagger_Api(
399422
app,
400423
authorizations=authorizations,
401-
security=["apikey"],
424+
security=security,
402425
url_prefix=":8000",
403426
prefix="/api",
404427
doc="/doc/",

mindsdb/api/http/namespaces/default.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,33 @@
1-
from flask import request
1+
from flask import request, session
22
from flask_restx import Resource
33
from flask_restx import fields
44

55
from mindsdb.__about__ import __version__ as mindsdb_version
66
from mindsdb.api.http.namespaces.configs.default import ns_conf
77
from mindsdb.api.http.utils import http_error
88
from mindsdb.metrics.metrics import api_endpoint_metrics
9-
from mindsdb.utilities.config import Config
9+
from mindsdb.utilities.config import config, HTTP_AUTH_TYPE
1010
from mindsdb.utilities import log
1111
from mindsdb.api.common.middleware import generate_pat, revoke_pat, verify_pat
1212

1313

1414
logger = log.getLogger(__name__)
1515

1616

17+
def check_session_auth() -> bool:
18+
"""checking whether current user is authenticated
19+
20+
Returns:
21+
bool: True if user authentication is approved
22+
"""
23+
try:
24+
if config["auth"]["http_auth_enabled"] is False:
25+
return True
26+
return session.get("username") == config["auth"]["username"]
27+
except Exception:
28+
return False
29+
30+
1731
@ns_conf.route("/login", methods=["POST"])
1832
class LoginRoute(Resource):
1933
@ns_conf.doc(
@@ -36,7 +50,6 @@ def post(self):
3650
):
3751
return http_error(400, "Error in username or password", "Username and password should be string")
3852

39-
config = Config()
4053
inline_username = config["auth"]["username"]
4154
inline_password = config["auth"]["password"]
4255

@@ -45,14 +58,24 @@ def post(self):
4558

4659
logger.info(f"User '{username}' logged in successfully")
4760

48-
return {"token": generate_pat()}, 200
61+
response = {}
62+
if config["auth"]["http_auth_type"] in (HTTP_AUTH_TYPE.SESSION, HTTP_AUTH_TYPE.SESSION_OR_TOKEN):
63+
session.clear()
64+
session["username"] = username
65+
session.permanent = True
66+
67+
if config["auth"]["http_auth_type"] in (HTTP_AUTH_TYPE.TOKEN, HTTP_AUTH_TYPE.SESSION_OR_TOKEN):
68+
response["token"] = generate_pat()
69+
70+
return response, 200
4971

5072

5173
@ns_conf.route("/logout", methods=["POST"])
5274
class LogoutRoute(Resource):
5375
@ns_conf.doc(responses={200: "Success"})
5476
@api_endpoint_metrics("POST", "/default/logout")
5577
def post(self):
78+
session.clear()
5679
# We can't forcibly log out a user with the
5780
h = request.headers.get("Authorization")
5881
if not h or not h.startswith("Bearer "):
@@ -89,7 +112,6 @@ class StatusRoute(Resource):
89112
def get(self):
90113
"""returns auth and environment data"""
91114
environment = "local"
92-
config = Config()
93115

94116
environment = config.get("environment")
95117
if environment is None:
@@ -107,11 +129,20 @@ def get(self):
107129
else:
108130
auth_provider = "local"
109131

132+
auth_confirmed = False
133+
auth_type = config["auth"]["http_auth_type"]
134+
if auth_type in (HTTP_AUTH_TYPE.SESSION, HTTP_AUTH_TYPE.SESSION_OR_TOKEN):
135+
auth_confirmed = auth_confirmed or check_session_auth()
136+
if auth_type in (HTTP_AUTH_TYPE.TOKEN, HTTP_AUTH_TYPE.SESSION_OR_TOKEN):
137+
auth_confirmed = auth_confirmed or verify_pat(
138+
request.headers.get("Authorization", "").replace("Bearer ", "")
139+
)
140+
110141
resp = {
111142
"mindsdb_version": mindsdb_version,
112143
"environment": environment,
113144
"auth": {
114-
"confirmed": verify_pat(request.headers.get("Authorization", "").replace("Bearer ", "")),
145+
"confirmed": auth_confirmed,
115146
"http_auth_enabled": config["auth"]["http_auth_enabled"],
116147
"provider": auth_provider,
117148
},

mindsdb/utilities/config.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
import argparse
55
import datetime
6+
import dataclasses
67
from pathlib import Path
78
from copy import deepcopy
89

@@ -57,6 +58,16 @@ def create_data_dir(path: Path) -> None:
5758
raise PermissionError(f"The directory is not allowed for writing: {path}")
5859

5960

61+
@dataclasses.dataclass(frozen=True)
62+
class HTTP_AUTH_TYPE:
63+
SESSION: str = "session"
64+
TOKEN: str = "token"
65+
SESSION_OR_TOKEN: str = "session_or_token"
66+
67+
68+
HTTP_AUTH_TYPE = HTTP_AUTH_TYPE()
69+
70+
6071
class Config:
6172
"""Application config. Singletone, initialized just once. Re-initialyze if `config.auto.json` is changed.
6273
The class loads multiple configs and merge then in one. If a config option defined in multiple places (config file,
@@ -138,6 +149,7 @@ def __new__(cls, *args, **kwargs) -> "Config":
138149
"locks": self.storage_root_path / "locks",
139150
},
140151
"auth": {
152+
"http_auth_type": HTTP_AUTH_TYPE.SESSION_OR_TOKEN, # token | session | session_or_token
141153
"http_auth_enabled": False,
142154
"http_permanent_session_lifetime": datetime.timedelta(days=31),
143155
"username": "mindsdb",
@@ -283,6 +295,12 @@ def prepare_env_config(self) -> None:
283295
self._env_config["auth"]["password"] = http_password
284296
# endregion
285297

298+
http_auth_type = os.environ.get("MINDSDB_HTTP_AUTH_TYPE", "").lower()
299+
if http_auth_type in dataclasses.astuple(HTTP_AUTH_TYPE):
300+
self._env_config["auth"]["http_auth_type"] = http_auth_type
301+
elif http_auth_type != "":
302+
raise ValueError(f"Wrong value of env var MINDSDB_HTTP_AUTH_TYPE={http_auth_type}")
303+
286304
# region logging
287305
if os.environ.get("MINDSDB_LOG_LEVEL", "") != "":
288306
self._env_config["logging"]["handlers"]["console"]["level"] = os.environ["MINDSDB_LOG_LEVEL"]

0 commit comments

Comments
 (0)