interloper-api 0.69.1__tar.gz → 0.71.0__tar.gz
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.
- {interloper_api-0.69.1 → interloper_api-0.71.0}/PKG-INFO +1 -1
- {interloper_api-0.69.1 → interloper_api-0.71.0}/pyproject.toml +1 -1
- {interloper_api-0.69.1 → interloper_api-0.71.0}/pyproject.toml.orig +1 -1
- {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/routes/components.py +12 -1
- {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/routes/oauth.py +16 -60
- {interloper_api-0.69.1 → interloper_api-0.71.0}/README.md +0 -0
- {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/__init__.py +0 -0
- {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/app.py +0 -0
- {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/dependencies/__init__.py +0 -0
- {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/dependencies/auth.py +0 -0
- {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/dependencies/rbac.py +0 -0
- {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/dependencies/state.py +0 -0
- {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/notifications/__init__.py +0 -0
- {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/notifications/invitations.py +0 -0
- {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/routes/__init__.py +0 -0
- {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/routes/admin.py +0 -0
- {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/routes/agent.py +0 -0
- {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/routes/auth.py +0 -0
- {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/routes/backfills.py +0 -0
- {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/routes/catalog.py +0 -0
- {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/routes/health.py +0 -0
- {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/routes/organisations.py +0 -0
- {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/routes/runs.py +0 -0
- {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/routes/tokens.py +0 -0
- {interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/routes/websocket.py +0 -0
|
@@ -118,7 +118,12 @@ class ComponentUpdateRequest(BaseModel):
|
|
|
118
118
|
|
|
119
119
|
|
|
120
120
|
class ComponentResponse(BaseModel):
|
|
121
|
-
"""Response body for a component of any kind.
|
|
121
|
+
"""Response body for a component of any kind.
|
|
122
|
+
|
|
123
|
+
``auto_renew`` surfaces a connection's renewal toggle even in list
|
|
124
|
+
responses, where a secret kind's ``config`` stays undisclosed — the
|
|
125
|
+
toggle is operational metadata, not a credential.
|
|
126
|
+
"""
|
|
122
127
|
|
|
123
128
|
id: UUID
|
|
124
129
|
org_id: UUID
|
|
@@ -129,6 +134,7 @@ class ComponentResponse(BaseModel):
|
|
|
129
134
|
config: dict[str, Any] | None = None
|
|
130
135
|
state: dict[str, Any] | None = None
|
|
131
136
|
encrypted: bool = False
|
|
137
|
+
auto_renew: bool | None = None
|
|
132
138
|
parent_id: UUID | None = None
|
|
133
139
|
relations: dict[str, list[RelationRef]] = {}
|
|
134
140
|
children: list[ComponentResponse] = []
|
|
@@ -167,6 +173,10 @@ class ComponentResponse(BaseModel):
|
|
|
167
173
|
if KINDS[row.kind].sensitive:
|
|
168
174
|
config = store.components.decode_config(row) if include_config else None
|
|
169
175
|
|
|
176
|
+
auto_renew: bool | None = None
|
|
177
|
+
if row.kind == "connection":
|
|
178
|
+
auto_renew = bool((config or store.components.decode_config(row)).get("auto_renew", True))
|
|
179
|
+
|
|
170
180
|
return cls(
|
|
171
181
|
id=row.id,
|
|
172
182
|
org_id=row.org_id,
|
|
@@ -177,6 +187,7 @@ class ComponentResponse(BaseModel):
|
|
|
177
187
|
config=config,
|
|
178
188
|
state=row.state,
|
|
179
189
|
encrypted=row.encrypted,
|
|
190
|
+
auto_renew=auto_renew,
|
|
180
191
|
parent_id=row.parent_id,
|
|
181
192
|
relations=_relations_of(row),
|
|
182
193
|
children=[
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
"""OAuth2 token exchange routes.
|
|
2
2
|
|
|
3
3
|
Providers come from the core registry (``interloper.oauth``): each
|
|
4
|
-
``OAuthProvider``
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
new route.
|
|
4
|
+
``OAuthProvider`` builds its own token requests (its dialect included), so
|
|
5
|
+
the exchange is performed generically — adding a provider is an
|
|
6
|
+
``interloper.oauth_providers`` entry point, not a new route.
|
|
8
7
|
|
|
9
8
|
The in-house *OAuth* credentials (``client_id`` / ``client_secret`` /
|
|
10
9
|
``redirect_uri``) are read from provider-scoped environment variables
|
|
@@ -19,14 +18,13 @@ buttons to render.
|
|
|
19
18
|
|
|
20
19
|
from __future__ import annotations
|
|
21
20
|
|
|
22
|
-
import base64
|
|
23
21
|
import logging
|
|
24
22
|
import os
|
|
25
23
|
from typing import Any
|
|
26
24
|
|
|
27
25
|
import httpx
|
|
28
26
|
from fastapi import APIRouter, Depends, HTTPException
|
|
29
|
-
from interloper.oauth import PROVIDERS, OAuthAppCredentials
|
|
27
|
+
from interloper.oauth import PROVIDERS, OAuthAppCredentials
|
|
30
28
|
from interloper_db import Profile
|
|
31
29
|
from pydantic import BaseModel
|
|
32
30
|
|
|
@@ -55,59 +53,10 @@ def log_provider_status() -> None:
|
|
|
55
53
|
logger.info("OAuth sign-in providers: %s", ", ".join(sorted(active)) if active else "none configured")
|
|
56
54
|
|
|
57
55
|
|
|
58
|
-
# -- Generic token exchange ----------------------------------------------------
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
async def _exchange(
|
|
62
|
-
client: httpx.AsyncClient,
|
|
63
|
-
spec: OAuthProvider,
|
|
64
|
-
config: OAuthAppCredentials,
|
|
65
|
-
code: str,
|
|
66
|
-
) -> dict[str, Any]:
|
|
67
|
-
"""Exchange an authorization code for tokens, driven by the provider spec.
|
|
68
|
-
|
|
69
|
-
Args:
|
|
70
|
-
client: The HTTP client to use.
|
|
71
|
-
spec: The provider's token-exchange spec.
|
|
72
|
-
config: The in-house OAuth credentials.
|
|
73
|
-
code: The authorization code.
|
|
74
|
-
|
|
75
|
-
Returns:
|
|
76
|
-
The provider's raw token response (e.g. ``refresh_token``). The OAuth
|
|
77
|
-
credentials are *not* injected: they are the in-house per-provider
|
|
78
|
-
values resolved from env at runtime, so the secret never leaves the
|
|
79
|
-
server.
|
|
80
|
-
"""
|
|
81
|
-
logical_values = {
|
|
82
|
-
"grant_type": "authorization_code",
|
|
83
|
-
"code": code,
|
|
84
|
-
"redirect_uri": config.redirect_uri,
|
|
85
|
-
"client_id": config.client_id,
|
|
86
|
-
"client_secret": config.client_secret,
|
|
87
|
-
}
|
|
88
|
-
params = {wire: logical_values[logical] for logical, wire in spec.token_params.items()}
|
|
89
|
-
|
|
90
|
-
headers: dict[str, str] = {}
|
|
91
|
-
if spec.token_basic_auth:
|
|
92
|
-
creds = base64.b64encode(f"{config.client_id}:{config.client_secret}".encode()).decode()
|
|
93
|
-
headers["Authorization"] = f"Basic {creds}"
|
|
94
|
-
|
|
95
|
-
if spec.token_method == "get":
|
|
96
|
-
response = await client.get(spec.token_url, params=params, headers=headers)
|
|
97
|
-
elif spec.token_encoding == "form":
|
|
98
|
-
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
|
99
|
-
response = await client.post(spec.token_url, data=params, headers=headers)
|
|
100
|
-
else:
|
|
101
|
-
response = await client.post(spec.token_url, json=params, headers=headers)
|
|
102
|
-
response.raise_for_status()
|
|
103
|
-
|
|
104
|
-
return response.json()
|
|
105
|
-
|
|
106
|
-
|
|
107
56
|
# -- Routes --------------------------------------------------------------------
|
|
108
57
|
|
|
109
58
|
|
|
110
|
-
class
|
|
59
|
+
class AuthorizationCodeExchangeRequest(BaseModel):
|
|
111
60
|
"""Request body for exchanging an authorization code for tokens."""
|
|
112
61
|
|
|
113
62
|
code: str
|
|
@@ -151,9 +100,9 @@ def list_providers() -> list[ProviderInfo]:
|
|
|
151
100
|
|
|
152
101
|
|
|
153
102
|
@router.post("/{provider}")
|
|
154
|
-
async def
|
|
103
|
+
async def exchange_authorization_code(
|
|
155
104
|
provider: str,
|
|
156
|
-
body:
|
|
105
|
+
body: AuthorizationCodeExchangeRequest,
|
|
157
106
|
_user: Profile = Depends(get_current_user),
|
|
158
107
|
) -> dict[str, Any]:
|
|
159
108
|
"""Exchange an authorization code for tokens. Requires authentication.
|
|
@@ -185,10 +134,17 @@ async def exchange_token(
|
|
|
185
134
|
|
|
186
135
|
try:
|
|
187
136
|
logger.info("Exchanging auth code for provider %s", provider)
|
|
137
|
+
request = spec.authorization_code_request(
|
|
138
|
+
code=body.code,
|
|
139
|
+
redirect_uri=config.redirect_uri,
|
|
140
|
+
client_id=config.client_id,
|
|
141
|
+
client_secret=config.client_secret,
|
|
142
|
+
)
|
|
188
143
|
async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client:
|
|
189
|
-
|
|
144
|
+
response = await client.send(request)
|
|
145
|
+
response.raise_for_status()
|
|
190
146
|
logger.info("Successfully exchanged auth code for provider %s", provider)
|
|
191
|
-
return
|
|
147
|
+
return response.json()
|
|
192
148
|
except httpx.HTTPStatusError as exception:
|
|
193
149
|
detail = exception.response.text
|
|
194
150
|
logger.error("Token exchange failed for %s: %s %s", provider, exception.response.status_code, detail)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/notifications/__init__.py
RENAMED
|
File without changes
|
{interloper_api-0.69.1 → interloper_api-0.71.0}/src/interloper_api/notifications/invitations.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|