Skip to content

Commit f92d897

Browse files
committed
Spilt connection arguments for bearer and oauth authentication
1 parent 9a9f070 commit f92d897

4 files changed

Lines changed: 228 additions & 34 deletions

File tree

mindsdb/integrations/handlers/rest_api_handler/connection_args.py

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,21 @@
1+
"""Aggregator for the rest_api handler's connection arguments.
2+
3+
REST/passthrough fields and authentication fields are defined in separate
4+
modules (rest_connection_args, oauth_connection_args). This module merges
5+
them into the single `connection_args` mapping that MindsDB expects each
6+
handler package to export.
7+
"""
8+
19
from collections import OrderedDict
210

3-
from mindsdb.integrations.libs.const import HANDLER_CONNECTION_ARG_TYPE as ARG_TYPE
11+
from .rest_connection_args import rest_connection_args
12+
from .oauth_connection_args import oauth_connection_args
13+
14+
15+
connection_args = OrderedDict()
16+
connection_args.update(rest_connection_args)
17+
connection_args.update(oauth_connection_args)
418

5-
connection_args = OrderedDict(
6-
base_url={
7-
"type": ARG_TYPE.STR,
8-
"description": "Base URL of the REST API (e.g. https://api.example.com)",
9-
"required": True,
10-
"label": "Base URL",
11-
},
12-
bearer_token={
13-
"type": ARG_TYPE.PWD,
14-
"description": "Bearer token injected as Authorization: Bearer <token>",
15-
"required": True,
16-
"label": "Bearer Token",
17-
"secret": True,
18-
},
19-
default_headers={
20-
"type": ARG_TYPE.DICT,
21-
"description": 'Static headers added to every request (e.g. {"Accept": "application/json"})',
22-
"required": False,
23-
"label": "Default Headers",
24-
},
25-
allowed_hosts={
26-
"type": ARG_TYPE.LIST,
27-
"description": 'Allowed hostnames for passthrough requests. Defaults to the base_url host. Use ["*"] to disable containment.',
28-
"required": False,
29-
"label": "Allowed Hosts",
30-
},
31-
test_path={
32-
"type": ARG_TYPE.STR,
33-
"description": "Path used by the /passthrough/test endpoint. Defaults to /",
34-
"required": False,
35-
"label": "Test Path",
36-
},
37-
)
3819

3920
connection_args_example = OrderedDict(
4021
base_url="https://api.example.com",
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""Authentication connection arguments for the rest_api handler.
2+
3+
These fields describe *how* the handler should authenticate to the upstream
4+
API. The current strategies are static bearer tokens and OAuth2 client
5+
credentials; both share this argument schema. The handler — not the runtime
6+
caller — is responsible for resolving these into an Authorization header.
7+
8+
Keep this module focused on schema only: do not import the OAuth2 token
9+
provider here, do not perform any HTTP, and do not change passthrough
10+
forwarding behavior. This step only defines the args.
11+
"""
12+
13+
from collections import OrderedDict
14+
15+
from mindsdb.integrations.libs.const import HANDLER_CONNECTION_ARG_TYPE as ARG_TYPE
16+
17+
18+
oauth_connection_args = OrderedDict(
19+
auth_type={
20+
"type": ARG_TYPE.STR,
21+
"description": (
22+
"Authentication strategy. 'bearer' uses a static bearer_token; "
23+
"'oauth_client_credentials' fetches a token via the OAuth2 client "
24+
"credentials grant. Defaults to 'bearer' for backward compatibility."
25+
),
26+
"required": False,
27+
"label": "Auth Type",
28+
},
29+
bearer_token={
30+
"type": ARG_TYPE.PWD,
31+
"description": "Bearer token injected as Authorization: Bearer <token>. Used when auth_type is 'bearer'.",
32+
"required": False,
33+
"label": "Bearer Token",
34+
"secret": True,
35+
},
36+
token_url={
37+
"type": ARG_TYPE.STR,
38+
"description": "OAuth2 token endpoint URL. Used when auth_type is 'oauth_client_credentials'.",
39+
"required": False,
40+
"label": "OAuth Token URL",
41+
},
42+
client_id={
43+
"type": ARG_TYPE.STR,
44+
"description": "OAuth2 client identifier. Used when auth_type is 'oauth_client_credentials'.",
45+
"required": False,
46+
"label": "OAuth Client ID",
47+
},
48+
client_secret={
49+
"type": ARG_TYPE.PWD,
50+
"description": "OAuth2 client secret. Used when auth_type is 'oauth_client_credentials'.",
51+
"required": False,
52+
"label": "OAuth Client Secret",
53+
"secret": True,
54+
},
55+
scope={
56+
"type": ARG_TYPE.STR,
57+
"description": "Optional OAuth2 scope string (space-separated) or list of scopes.",
58+
"required": False,
59+
"label": "OAuth Scope",
60+
},
61+
audience={
62+
"type": ARG_TYPE.STR,
63+
"description": "Optional OAuth2 audience parameter (Auth0/Cognito-style extension; not part of RFC 6749).",
64+
"required": False,
65+
"label": "OAuth Audience",
66+
},
67+
token_auth_method={
68+
"type": ARG_TYPE.STR,
69+
"description": (
70+
"How client credentials are sent to the token endpoint: "
71+
"'client_secret_post' (default) or 'client_secret_basic'."
72+
),
73+
"required": False,
74+
"label": "Token Auth Method",
75+
},
76+
)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""REST/passthrough connection arguments for the rest_api handler.
2+
3+
These fields configure how the handler talks HTTP to the upstream API
4+
(base URL, allowed hosts, default headers, test path). Authentication
5+
fields live in oauth_connection_args.py — keep them separate so the
6+
passthrough plumbing stays independent of the auth strategy.
7+
"""
8+
9+
from collections import OrderedDict
10+
11+
from mindsdb.integrations.libs.const import HANDLER_CONNECTION_ARG_TYPE as ARG_TYPE
12+
13+
14+
rest_connection_args = OrderedDict(
15+
base_url={
16+
"type": ARG_TYPE.STR,
17+
"description": "Base URL of the REST API (e.g. https://api.example.com)",
18+
"required": True,
19+
"label": "Base URL",
20+
},
21+
default_headers={
22+
"type": ARG_TYPE.DICT,
23+
"description": 'Static headers added to every request (e.g. {"Accept": "application/json"})',
24+
"required": False,
25+
"label": "Default Headers",
26+
},
27+
allowed_hosts={
28+
"type": ARG_TYPE.LIST,
29+
"description": 'Allowed hostnames for passthrough requests. Defaults to the base_url host. Use ["*"] to disable containment.',
30+
"required": False,
31+
"label": "Allowed Hosts",
32+
},
33+
test_path={
34+
"type": ARG_TYPE.STR,
35+
"description": "Path used by the /passthrough/test endpoint. Defaults to /",
36+
"required": False,
37+
"label": "Test Path",
38+
},
39+
)

tests/unit/handlers/test_rest_api.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,18 @@
22

33
from unittest.mock import patch, MagicMock
44

5+
from mindsdb.integrations.handlers.rest_api_handler import (
6+
connection_args as exported_connection_args,
7+
)
8+
from mindsdb.integrations.handlers.rest_api_handler.connection_args import connection_args
9+
from mindsdb.integrations.handlers.rest_api_handler.oauth_connection_args import (
10+
oauth_connection_args,
11+
)
12+
from mindsdb.integrations.handlers.rest_api_handler.rest_connection_args import (
13+
rest_connection_args,
14+
)
515
from mindsdb.integrations.handlers.rest_api_handler.rest_api_handler import RestApiHandler
16+
from mindsdb.integrations.libs.const import HANDLER_CONNECTION_ARG_TYPE as ARG_TYPE
617
from mindsdb.integrations.libs.passthrough import PassthroughProtocol
718
from mindsdb.integrations.libs.passthrough_types import PassthroughRequest, PassthroughResponse
819
from mindsdb.integrations.libs.response import (
@@ -163,3 +174,90 @@ def test_test_passthrough_with_no_network(self):
163174
assert isinstance(result, dict)
164175
assert result["ok"] is False
165176
assert result["error_code"] in ("network", "unknown")
177+
178+
179+
class TestConnectionArgsSchema:
180+
"""The exported connection_args is the union of REST + auth modules."""
181+
182+
def test_rest_module_only_holds_passthrough_fields(self):
183+
assert set(rest_connection_args.keys()) == {
184+
"base_url",
185+
"default_headers",
186+
"allowed_hosts",
187+
"test_path",
188+
}
189+
190+
def test_auth_module_only_holds_auth_fields(self):
191+
assert set(oauth_connection_args.keys()) == {
192+
"auth_type",
193+
"bearer_token",
194+
"token_url",
195+
"client_id",
196+
"client_secret",
197+
"scope",
198+
"audience",
199+
"token_auth_method",
200+
}
201+
202+
def test_bearer_token_lives_in_auth_module(self):
203+
# bearer_token is an auth strategy, not a REST/passthrough setting.
204+
assert "bearer_token" not in rest_connection_args
205+
assert "bearer_token" in oauth_connection_args
206+
207+
def test_aggregated_connection_args_includes_rest_fields(self):
208+
for key in ("base_url", "default_headers", "allowed_hosts", "test_path"):
209+
assert key in connection_args
210+
211+
def test_aggregated_connection_args_includes_bearer_token(self):
212+
assert "bearer_token" in connection_args
213+
214+
def test_aggregated_connection_args_includes_oauth_fields(self):
215+
for key in (
216+
"auth_type",
217+
"token_url",
218+
"client_id",
219+
"client_secret",
220+
"scope",
221+
"audience",
222+
"token_auth_method",
223+
):
224+
assert key in connection_args
225+
226+
def test_client_secret_marked_secret_and_pwd(self):
227+
spec = connection_args["client_secret"]
228+
assert spec["type"] == ARG_TYPE.PWD
229+
assert spec.get("secret") is True
230+
231+
def test_bearer_token_marked_secret_and_pwd(self):
232+
spec = connection_args["bearer_token"]
233+
assert spec["type"] == ARG_TYPE.PWD
234+
assert spec.get("secret") is True
235+
236+
def test_package_exports_aggregated_args(self):
237+
# The handler package re-exports connection_args; make sure the
238+
# aggregator and the package-level export are the same object.
239+
assert exported_connection_args is connection_args
240+
241+
242+
class TestBackwardCompatibleBearerInit:
243+
"""Existing bearer-only configs must still initialize and validate."""
244+
245+
def test_legacy_config_initializes(self):
246+
handler = _make_handler(
247+
{
248+
"base_url": "https://api.example.com",
249+
"bearer_token": "legacy-token",
250+
}
251+
)
252+
assert handler.connection_data["base_url"] == "https://api.example.com"
253+
assert handler.connection_data["bearer_token"] == "legacy-token"
254+
255+
def test_legacy_config_check_connection_succeeds(self):
256+
handler = _make_handler(
257+
{
258+
"base_url": "https://api.example.com",
259+
"bearer_token": "legacy-token",
260+
}
261+
)
262+
response = handler.check_connection()
263+
assert response.success is True

0 commit comments

Comments
 (0)