• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SwissDataScienceCenter / renku-data-services / 18123243513

30 Sep 2025 08:10AM UTC coverage: 86.702% (-0.01%) from 86.714%
18123243513

Pull #1019

github

web-flow
Merge e726c4543 into 0690bab65
Pull Request #1019: feat: Attempt to support dockerhub private images

70 of 101 new or added lines in 9 files covered. (69.31%)

106 existing lines in 6 files now uncovered.

22357 of 25786 relevant lines covered (86.7%)

1.52 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.68
/components/renku_data_services/connected_services/blueprints.py
1
"""Connected services blueprint."""
2

3
from dataclasses import dataclass
2✔
4
from typing import Any
2✔
5
from urllib.parse import unquote, urlparse, urlunparse
2✔
6

7
from sanic import HTTPResponse, Request, empty, json, redirect
2✔
8
from sanic.response import JSONResponse
2✔
9
from sanic_ext import validate
2✔
10
from ulid import ULID
2✔
11

12
import renku_data_services.base_models as base_models
2✔
13
from renku_data_services.app_config import logging
2✔
14
from renku_data_services.base_api.auth import authenticate, only_admins, only_authenticated
2✔
15
from renku_data_services.base_api.blueprint import BlueprintFactoryResponse, CustomBlueprint
2✔
16
from renku_data_services.base_api.misc import validate_query
2✔
17
from renku_data_services.base_api.pagination import PaginationRequest, paginate
2✔
18
from renku_data_services.base_models.validation import validate_and_dump, validated_json
2✔
19
from renku_data_services.connected_services import apispec
2✔
20
from renku_data_services.connected_services.apispec_base import AuthorizeParams, CallbackParams
2✔
21
from renku_data_services.connected_services.core import validate_oauth2_client_patch, validate_unsaved_oauth2_client
2✔
22
from renku_data_services.connected_services.db import ConnectedServicesRepository
2✔
23

24
logger = logging.getLogger(__name__)
2✔
25

26

27
@dataclass(kw_only=True)
2✔
28
class OAuth2ClientsBP(CustomBlueprint):
2✔
29
    """Handlers for using OAuth2 Clients."""
30

31
    connected_services_repo: ConnectedServicesRepository
2✔
32
    authenticator: base_models.Authenticator
2✔
33

34
    def get_all(self) -> BlueprintFactoryResponse:
2✔
35
        """List all OAuth2 Clients."""
36

37
        @authenticate(self.authenticator)
2✔
38
        async def _get_all(_: Request, user: base_models.APIUser) -> JSONResponse:
2✔
39
            clients = await self.connected_services_repo.get_oauth2_clients(user=user)
2✔
40
            return validated_json(apispec.ProviderList, clients)
2✔
41

42
        return "/oauth2/providers", ["GET"], _get_all
2✔
43

44
    def get_one(self) -> BlueprintFactoryResponse:
2✔
45
        """Get a specific OAuth2 Client."""
46

47
        @authenticate(self.authenticator)
2✔
48
        async def _get_one(_: Request, user: base_models.APIUser, provider_id: str) -> JSONResponse:
2✔
49
            provider_id = unquote(provider_id)
2✔
50
            client = await self.connected_services_repo.get_oauth2_client(provider_id=provider_id, user=user)
2✔
51
            return validated_json(apispec.Provider, client)
1✔
52

53
        return "/oauth2/providers/<provider_id>", ["GET"], _get_one
2✔
54

55
    def post(self) -> BlueprintFactoryResponse:
2✔
56
        """Create a new OAuth2 Client."""
57

58
        @authenticate(self.authenticator)
2✔
59
        @only_admins
2✔
60
        @validate(json=apispec.ProviderPost)
2✔
61
        async def _post(_: Request, user: base_models.APIUser, body: apispec.ProviderPost) -> JSONResponse:
2✔
62
            new_client = validate_unsaved_oauth2_client(body)
2✔
63
            client = await self.connected_services_repo.insert_oauth2_client(user=user, new_client=new_client)
2✔
64
            return validated_json(apispec.Provider, client, 201)
2✔
65

66
        return "/oauth2/providers", ["POST"], _post
2✔
67

68
    def patch(self) -> BlueprintFactoryResponse:
2✔
69
        """Partially update a specific OAuth2 Client."""
70

71
        @authenticate(self.authenticator)
2✔
72
        @only_admins
2✔
73
        @validate(json=apispec.ProviderPatch)
2✔
74
        async def _patch(
2✔
75
            _: Request, user: base_models.APIUser, provider_id: str, body: apispec.ProviderPatch
76
        ) -> JSONResponse:
77
            provider_id = unquote(provider_id)
2✔
78
            client_patch = validate_oauth2_client_patch(body)
2✔
79
            client = await self.connected_services_repo.update_oauth2_client(
2✔
80
                user=user, provider_id=provider_id, patch=client_patch
81
            )
82
            return validated_json(apispec.Provider, client)
1✔
83

84
        return "/oauth2/providers/<provider_id>", ["PATCH"], _patch
2✔
85

86
    def delete(self) -> BlueprintFactoryResponse:
2✔
87
        """Delete a specific OAuth2 Client."""
88

89
        @authenticate(self.authenticator)
2✔
90
        @only_admins
2✔
91
        async def _delete(_: Request, user: base_models.APIUser, provider_id: str) -> HTTPResponse:
2✔
92
            provider_id = unquote(provider_id)
2✔
93
            await self.connected_services_repo.delete_oauth2_client(user=user, provider_id=provider_id)
2✔
94
            return HTTPResponse(status=204)
2✔
95

96
        return "/oauth2/providers/<provider_id>", ["DELETE"], _delete
2✔
97

98
    def authorize(self) -> BlueprintFactoryResponse:
2✔
99
        """Authorize an OAuth2 Client."""
100

101
        @authenticate(self.authenticator)
2✔
102
        @only_authenticated
2✔
103
        @validate_query(query=apispec.AuthorizeParams)
2✔
104
        async def _authorize(
2✔
105
            request: Request, user: base_models.APIUser, provider_id: str, query: AuthorizeParams
106
        ) -> HTTPResponse:
107
            provider_id = unquote(provider_id)
2✔
108
            callback_url = self._get_callback_url(request)
2✔
109
            url = await self.connected_services_repo.authorize_client(
2✔
110
                provider_id=provider_id, user=user, callback_url=callback_url, next_url=query.next_url
111
            )
112
            return redirect(to=url)
1✔
113

114
        return "/oauth2/providers/<provider_id>/authorize", ["GET"], _authorize
2✔
115

116
    def authorize_callback(self) -> BlueprintFactoryResponse:
2✔
117
        """OAuth2 authorization callback."""
118

119
        async def _callback(request: Request) -> HTTPResponse:
2✔
120
            params = CallbackParams.model_validate(dict(request.query_args))
1✔
121

122
            callback_url = self._get_callback_url(request)
1✔
123

124
            next_url = await self.connected_services_repo.authorize_callback(
1✔
125
                state=params.state, raw_url=request.url, callback_url=callback_url
126
            )
127

128
            return redirect(to=next_url) if next_url else json({"status": "OK"})
1✔
129

130
        return "/oauth2/callback", ["GET"], _callback
2✔
131

132
    def custom_connect(self) -> BlueprintFactoryResponse:
2✔
133
        """Custom connection."""
134

135
        @authenticate(self.authenticator)
2✔
136
        @validate(json=apispec.SimpleConnect)
2✔
137
        async def _custom_connect(
2✔
138
            _: Request, user: base_models.APIUser, provider_id: str, body: apispec.SimpleConnect
139
        ) -> JSONResponse:
140
            provider_id = unquote(provider_id)
1✔
141
            token_set = body.model_dump()
1✔
142
            conn_id = await self.connected_services_repo.custom_connect(user, provider_id, token_set)
1✔
NEW
143
            conn = apispec.Connection(
×
144
                id=str(conn_id), provider_id=provider_id, status=apispec.ConnectionStatus.connected
145
            )
NEW
146
            return validated_json(apispec.Connection, conn)
×
147

148
        return "/oauth2/providers/<provider_id>/simple_connect", ["POST"], _custom_connect
2✔
149

150
    def _get_callback_url(self, request: Request) -> str:
2✔
151
        callback_url = request.url_for(f"{self.name}.{self.authorize_callback.__name__}")
2✔
152
        # TODO: configure the server to trust the reverse proxy so that the request scheme is always "https".
153
        # TODO: see also https://github.com/SwissDataScienceCenter/renku-data-services/pull/225
154
        https_callback_url = urlunparse(urlparse(callback_url)._replace(scheme="https"))
2✔
155
        if https_callback_url != callback_url:
2✔
156
            logger.warning("Forcing the callback URL to use https. Trusted proxies configuration may be incorrect.")
2✔
157
        return https_callback_url
2✔
158

159

160
@dataclass(kw_only=True)
2✔
161
class OAuth2ConnectionsBP(CustomBlueprint):
2✔
162
    """Handlers for using OAuth2 connections."""
163

164
    connected_services_repo: ConnectedServicesRepository
2✔
165
    authenticator: base_models.Authenticator
2✔
166

167
    def get_all(self) -> BlueprintFactoryResponse:
2✔
168
        """List all OAuth2 connections."""
169

170
        @authenticate(self.authenticator)
2✔
171
        async def _get_all(_: Request, user: base_models.APIUser) -> JSONResponse:
2✔
172
            connections = await self.connected_services_repo.get_oauth2_connections(user=user)
2✔
173
            return validated_json(apispec.ConnectionList, connections)
2✔
174

175
        return "/oauth2/connections", ["GET"], _get_all
2✔
176

177
    def get_one(self) -> BlueprintFactoryResponse:
2✔
178
        """Get a specific OAuth2 connection."""
179

180
        @authenticate(self.authenticator)
2✔
181
        async def _get_one(_: Request, user: base_models.APIUser, connection_id: ULID) -> JSONResponse:
2✔
182
            connection = await self.connected_services_repo.get_oauth2_connection(
×
183
                connection_id=connection_id, user=user
184
            )
185
            return validated_json(apispec.Connection, connection)
×
186

187
        return "/oauth2/connections/<connection_id:ulid>", ["GET"], _get_one
2✔
188

189
    def delete(self) -> BlueprintFactoryResponse:
2✔
190
        """Delete a specific OAuth2 connection."""
191

192
        @authenticate(self.authenticator)
2✔
193
        async def _delete_one(_: Request, user: base_models.APIUser, connection_id: ULID) -> HTTPResponse:
2✔
194
            result = await self.connected_services_repo.delete_oauth2_connection(user, connection_id)
×
195

196
            return empty(status=204 if result else 404)
×
197

198
        return "/oauth2/connections/<connection_id:ulid>", ["DELETE"], _delete_one
2✔
199

200
    def get_account(self) -> BlueprintFactoryResponse:
2✔
201
        """Get the account information for a specific OAuth2 connection."""
202

203
        @authenticate(self.authenticator)
2✔
204
        async def _get_account(_: Request, user: base_models.APIUser, connection_id: ULID) -> JSONResponse:
2✔
205
            account = await self.connected_services_repo.get_oauth2_connected_account(
1✔
206
                connection_id=connection_id, user=user
207
            )
208
            return validated_json(apispec.ConnectedAccount, account)
1✔
209

210
        return "/oauth2/connections/<connection_id:ulid>/account", ["GET"], _get_account
2✔
211

212
    def get_token(self) -> BlueprintFactoryResponse:
2✔
213
        """Get the access token for a specific OAuth2 connection."""
214

215
        @authenticate(self.authenticator)
2✔
216
        async def _get_token(_: Request, user: base_models.APIUser, connection_id: ULID) -> JSONResponse:
2✔
217
            token = await self.connected_services_repo.get_oauth2_connection_token(
1✔
218
                connection_id=connection_id, user=user
219
            )
220
            return json(token.dump_for_api())
1✔
221

222
        return "/oauth2/connections/<connection_id:ulid>/token", ["GET"], _get_token
2✔
223

224
    def get_installations(self) -> BlueprintFactoryResponse:
2✔
225
        """Get the installations for a specific OAuth2 connection."""
226

227
        @authenticate(self.authenticator)
2✔
228
        @validate_query(query=apispec.PaginationRequest)
2✔
229
        @paginate
2✔
230
        async def _get_installations(
2✔
231
            _: Request,
232
            user: base_models.APIUser,
233
            connection_id: ULID,
234
            pagination: PaginationRequest,
235
            query: apispec.PaginationRequest,
236
        ) -> tuple[list[dict[str, Any]], int]:
237
            installations_list = await self.connected_services_repo.get_oauth2_app_installations(
1✔
238
                connection_id=connection_id,
239
                user=user,
240
                pagination=pagination,
241
            )
242
            body = validate_and_dump(apispec.AppInstallationList, installations_list.installations)
1✔
243
            return body, installations_list.total_count
1✔
244

245
        return "/oauth2/connections/<connection_id:ulid>/installations", ["GET"], _get_installations
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc