interloper-api 0.69.0__tar.gz → 0.70.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.0 → interloper_api-0.70.0}/PKG-INFO +1 -1
- {interloper_api-0.69.0 → interloper_api-0.70.0}/pyproject.toml +1 -1
- {interloper_api-0.69.0 → interloper_api-0.70.0}/pyproject.toml.orig +1 -1
- {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/routes/oauth.py +16 -60
- {interloper_api-0.69.0 → interloper_api-0.70.0}/README.md +0 -0
- {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/__init__.py +0 -0
- {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/app.py +0 -0
- {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/dependencies/__init__.py +0 -0
- {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/dependencies/auth.py +0 -0
- {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/dependencies/rbac.py +0 -0
- {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/dependencies/state.py +0 -0
- {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/notifications/__init__.py +0 -0
- {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/notifications/invitations.py +0 -0
- {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/routes/__init__.py +0 -0
- {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/routes/admin.py +0 -0
- {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/routes/agent.py +0 -0
- {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/routes/auth.py +0 -0
- {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/routes/backfills.py +0 -0
- {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/routes/catalog.py +0 -0
- {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/routes/components.py +0 -0
- {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/routes/health.py +0 -0
- {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/routes/organisations.py +0 -0
- {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/routes/runs.py +0 -0
- {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/routes/tokens.py +0 -0
- {interloper_api-0.69.0 → interloper_api-0.70.0}/src/interloper_api/routes/websocket.py +0 -0
|
@@ -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.0 → interloper_api-0.70.0}/src/interloper_api/notifications/__init__.py
RENAMED
|
File without changes
|
{interloper_api-0.69.0 → interloper_api-0.70.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
|
|
File without changes
|