Skip to content

Commit c51cccc

Browse files
authored
feat: add support for using OAuth 2.0 integrations with data connectors (#1201)
Add support for using OAuth2.0 integrations to mount data connectors in sessions. Google Drive and Dropbox storage types can now be accessed from Renku sessions. Contents: * #1190 * #1195 * #1200
1 parent d3c9e42 commit c51cccc

26 files changed

Lines changed: 971 additions & 111 deletions

File tree

bases/renku_data_services/data_api/app.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,12 @@ def register_all_handlers(app: Sanic, dm: DependencyManager) -> Sanic:
115115
storage_repo=dm.storage_repo,
116116
authenticator=dm.gitlab_authenticator,
117117
)
118-
storage_schema = StorageSchemaBP(name="storage_schema", url_prefix=url_prefix)
118+
storage_schema = StorageSchemaBP(
119+
name="storage_schema",
120+
url_prefix=url_prefix,
121+
data_source_repo=dm.data_source_repo,
122+
authenticator=dm.authenticator,
123+
)
119124
user_preferences = UserPreferencesBP(
120125
name="user_preferences",
121126
url_prefix=url_prefix,
@@ -185,6 +190,7 @@ def register_all_handlers(app: Sanic, dm: DependencyManager) -> Sanic:
185190
connected_services_repo=dm.connected_services_repo,
186191
oauth_client_factory=dm.oauth_http_client_factory,
187192
authenticator=dm.authenticator,
193+
nb_config=dm.config.nb_config,
188194
)
189195
repositories = RepositoriesBP(
190196
name="repositories",
@@ -202,6 +208,7 @@ def register_all_handlers(app: Sanic, dm: DependencyManager) -> Sanic:
202208
data_connector_repo=dm.data_connector_repo,
203209
data_connector_secret_repo=dm.data_connector_secret_repo,
204210
git_provider_helper=dm.git_provider_helper,
211+
data_source_repo=dm.data_source_repo,
205212
image_check_repo=dm.image_check_repo,
206213
internal_gitlab_authenticator=dm.gitlab_authenticator,
207214
metrics=dm.metrics,

bases/renku_data_services/data_api/dependencies.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from renku_data_services.notebooks.api.classes.data_service import DummyGitProviderHelper, GitProviderHelper
5050
from renku_data_services.notebooks.config import GitProviderHelperProto, get_clusters
5151
from renku_data_services.notebooks.constants import AMALTHEA_SESSION_GVK, JUPYTER_SESSION_GVK
52+
from renku_data_services.notebooks.data_sources import DataSourceRepository
5253
from renku_data_services.notebooks.image_check import ImageCheckRepository
5354
from renku_data_services.notifications.db import NotificationsRepository
5455
from renku_data_services.platform.db import PlatformRepository, UrlRedirectRepository
@@ -144,6 +145,7 @@ class DependencyManager:
144145
data_connector_repo: DataConnectorRepository
145146
data_connector_secret_repo: DataConnectorSecretRepository
146147
cluster_repo: ClusterRepository
148+
data_source_repo: DataSourceRepository
147149
image_check_repo: ImageCheckRepository
148150
metrics_repo: MetricsRepository
149151
metrics: StagingMetricsService
@@ -382,6 +384,11 @@ def from_env(cls) -> DependencyManager:
382384
secret_service_public_key=config.secrets.public_key,
383385
authz=authz,
384386
)
387+
data_source_repo = DataSourceRepository(
388+
nb_config=config.nb_config,
389+
connected_services_repo=connected_services_repo,
390+
oauth_client_factory=oauth_http_client_factory,
391+
)
385392
image_check_repo = ImageCheckRepository(
386393
nb_config=config.nb_config,
387394
connected_services_repo=connected_services_repo,
@@ -429,6 +436,7 @@ def from_env(cls) -> DependencyManager:
429436
data_connector_repo=data_connector_repo,
430437
data_connector_secret_repo=data_connector_secret_repo,
431438
cluster_repo=cluster_repo,
439+
data_source_repo=data_source_repo,
432440
image_check_repo=image_check_repo,
433441
metrics_repo=metrics_repo,
434442
metrics=metrics,

components/renku_data_services/connected_services/api.spec.yaml

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,26 @@ paths:
210210
$ref: "#/components/responses/Error"
211211
tags:
212212
- oauth2
213+
/oauth2/connections/{connection_id}/token:
214+
get:
215+
summary: Get the access token for a specific OAuth2 connection
216+
parameters:
217+
- in: path
218+
name: connection_id
219+
required: true
220+
schema:
221+
type: string
222+
responses:
223+
"200":
224+
description: The access token and its metadata.
225+
content:
226+
application/json:
227+
schema:
228+
$ref: "#/components/schemas/OAuth2Token"
229+
default:
230+
$ref: "#/components/responses/Error"
231+
tags:
232+
- oauth2
213233
/oauth2/connections/{connection_id}/installations:
214234
get:
215235
summary: Get the installations for this OAuth2 connection for the currently authenticated user if their account is connected
@@ -259,6 +279,32 @@ paths:
259279
$ref: "#/components/responses/Error"
260280
tags:
261281
- oauth2
282+
/oauth2/connections/{connection_id}/token_endpoint:
283+
post:
284+
summary: OAuth 2.0 token endpoint to support applications running in sessions
285+
parameters:
286+
- in: path
287+
name: connection_id
288+
required: true
289+
schema:
290+
type: string
291+
requestBody:
292+
required: true
293+
content:
294+
application/json:
295+
schema:
296+
$ref: "#/components/schemas/PostTokenRequest"
297+
responses:
298+
"200":
299+
description: The access token and its metadata.
300+
content:
301+
application/json:
302+
schema:
303+
$ref: "#/components/schemas/PostTokenResponse"
304+
default:
305+
$ref: "#/components/responses/Error"
306+
tags:
307+
- oauth2
262308
components:
263309
schemas:
264310
ProviderList:
@@ -385,7 +431,21 @@ components:
385431
$ref: "#/components/schemas/WebUrl"
386432
required:
387433
- username
388-
- web_url
434+
OAuth2Token:
435+
type: object
436+
additionalProperties: true
437+
properties:
438+
access_token:
439+
type: string
440+
description: An access token for OAuth 2.0
441+
scope:
442+
type: string
443+
token_type:
444+
type: string
445+
id_token:
446+
type: string
447+
expires_at_iso:
448+
type: string
389449
AppInstallationList:
390450
type: array
391451
items:
@@ -413,6 +473,38 @@ components:
413473
- account_login
414474
- account_web_url
415475
- repository_selection
476+
PostTokenRequest:
477+
type: object
478+
additionalProperties: true
479+
properties:
480+
grant_type:
481+
$ref: "#/components/schemas/PostTokenGrantType"
482+
refresh_token:
483+
type: string
484+
required:
485+
- grant_type
486+
- refresh_token
487+
PostTokenResponse:
488+
type: object
489+
additionalProperties: true
490+
properties:
491+
access_token:
492+
type: string
493+
token_type:
494+
type: string
495+
expires_in:
496+
type: integer
497+
refresh_token:
498+
type: string
499+
refresh_expires_in:
500+
type: integer
501+
scope:
502+
type: string
503+
required:
504+
- access_token
505+
- token_type
506+
- expires_in
507+
- refresh_token
416508
Ulid:
417509
description: ULID identifier
418510
type: string
@@ -426,12 +518,11 @@ components:
426518
ProviderKind:
427519
type: string
428520
enum:
429-
- "gitlab"
430-
- "github"
431-
- "drive"
432-
- "onedrive"
433521
- "dropbox"
434522
- "generic_oidc"
523+
- "github"
524+
- "gitlab"
525+
- "google"
435526
example: "gitlab"
436527
ApplicationSlug:
437528
description: |
@@ -507,6 +598,11 @@ components:
507598
description: |
508599
The URL for OpenID Connect client discovery. Used for providers of kind 'generic_oidc'.
509600
example: https://renkulab.io/auth/realms/Renku
601+
PostTokenGrantType:
602+
type: string
603+
description: A grant type for OAuth 2.0 (see RFC 6749)
604+
enum:
605+
- refresh_token
510606
ErrorResponse:
511607
type: object
512608
properties:

components/renku_data_services/connected_services/apispec.py

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# generated by datamodel-codegen:
22
# filename: api.spec.yaml
3-
# timestamp: 2025-09-05T11:16:18+00:00
3+
# timestamp: 2026-02-05T09:28:04+00:00
44

55
from __future__ import annotations
66

@@ -12,6 +12,19 @@
1212
from renku_data_services.connected_services.apispec_base import BaseAPISpec
1313

1414

15+
class OAuth2Token(BaseAPISpec):
16+
model_config = ConfigDict(
17+
extra="allow",
18+
)
19+
access_token: Optional[str] = Field(
20+
None, description="An access token for OAuth 2.0"
21+
)
22+
scope: Optional[str] = None
23+
token_type: Optional[str] = None
24+
id_token: Optional[str] = None
25+
expires_at_iso: Optional[str] = None
26+
27+
1528
class RepositorySelection(Enum):
1629
all = "all"
1730
selected = "selected"
@@ -28,13 +41,24 @@ class AppInstallation(BaseAPISpec):
2841
suspended_at: Optional[datetime] = None
2942

3043

44+
class PostTokenResponse(BaseAPISpec):
45+
model_config = ConfigDict(
46+
extra="allow",
47+
)
48+
access_token: str
49+
token_type: str
50+
expires_in: int
51+
refresh_token: str
52+
refresh_expires_in: Optional[int] = None
53+
scope: Optional[str] = None
54+
55+
3156
class ProviderKind(Enum):
32-
gitlab = "gitlab"
33-
github = "github"
34-
drive = "drive"
35-
onedrive = "onedrive"
3657
dropbox = "dropbox"
3758
generic_oidc = "generic_oidc"
59+
github = "github"
60+
gitlab = "gitlab"
61+
google = "google"
3862

3963

4064
class ConnectionStatus(Enum):
@@ -52,6 +76,10 @@ class PaginationRequest(BaseAPISpec):
5276
)
5377

5478

79+
class PostTokenGrantType(Enum):
80+
refresh_token = "refresh_token"
81+
82+
5583
class Error(BaseAPISpec):
5684
code: int = Field(..., examples=[1404], gt=0)
5785
detail: Optional[str] = Field(
@@ -250,8 +278,8 @@ class ConnectedAccount(BaseAPISpec):
250278
extra="forbid",
251279
)
252280
username: str = Field(..., examples=["some-username"])
253-
web_url: str = Field(
254-
...,
281+
web_url: Optional[str] = Field(
282+
None,
255283
description="A URL which can be opened in a browser, i.e. a web page.",
256284
examples=["https://example.org"],
257285
)
@@ -261,6 +289,14 @@ class AppInstallationList(RootModel[List[AppInstallation]]):
261289
root: List[AppInstallation]
262290

263291

292+
class PostTokenRequest(BaseAPISpec):
293+
model_config = ConfigDict(
294+
extra="allow",
295+
)
296+
grant_type: PostTokenGrantType
297+
refresh_token: str
298+
299+
264300
class ProviderList(RootModel[List[Provider]]):
265301
root: List[Provider]
266302

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""Extra definitions for the API spec."""
2+
3+
from __future__ import annotations
4+
5+
import base64
6+
from typing import Self
7+
8+
from pydantic import ConfigDict
9+
10+
from renku_data_services.connected_services.apispec_base import BaseAPISpec
11+
12+
13+
class RenkuTokens(BaseAPISpec):
14+
"""Represents a set of authentication tokens used in Renku."""
15+
16+
model_config = ConfigDict(
17+
extra="forbid",
18+
)
19+
access_token: str
20+
refresh_token: str
21+
22+
def encode(self) -> str:
23+
"""Encode the Renku tokens as a single URL-safe string."""
24+
as_json = self.model_dump_json()
25+
return base64.urlsafe_b64encode(as_json.encode("utf-8")).decode("utf-8")
26+
27+
@classmethod
28+
def decode(cls, encoded: str) -> Self:
29+
"""Decode a single string into a set of Renku tokens."""
30+
json_raw = base64.urlsafe_b64decode(encoded.encode("utf-8"))
31+
return cls.model_validate_json(json_raw)

0 commit comments

Comments
 (0)