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

SwissDataScienceCenter / renku-data-services / 10353957042

12 Aug 2024 02:35PM UTC coverage: 90.758% (+0.4%) from 90.398%
10353957042

Pull #338

github

web-flow
Merge 3a49eb6c2 into 8afb94949
Pull Request #338: feat!: expand environments specification

227 of 237 new or added lines in 7 files covered. (95.78%)

48 existing lines in 9 files now uncovered.

9202 of 10139 relevant lines covered (90.76%)

1.61 hits per line

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

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

3
from dataclasses import dataclass
2✔
4
from urllib.parse import urlparse, urlunparse
2✔
5

6
from sanic import HTTPResponse, Request, json, redirect
2✔
7
from sanic.log import logger
2✔
8
from sanic.response import JSONResponse
2✔
9
from sanic_ext import validate
2✔
10

11
import renku_data_services.base_models as base_models
2✔
12
from renku_data_services.base_api.auth import authenticate, only_admins, only_authenticated
2✔
13
from renku_data_services.base_api.blueprint import BlueprintFactoryResponse, CustomBlueprint
2✔
14
from renku_data_services.base_api.misc import validate_query
2✔
15
from renku_data_services.base_models.validation import validated_json
2✔
16
from renku_data_services.connected_services import apispec
2✔
17
from renku_data_services.connected_services.apispec_base import AuthorizeParams, CallbackParams
2✔
18
from renku_data_services.connected_services.db import ConnectedServicesRepository
2✔
19

20

21
@dataclass(kw_only=True)
2✔
22
class OAuth2ClientsBP(CustomBlueprint):
2✔
23
    """Handlers for using OAuth2 Clients."""
2✔
24

25
    connected_services_repo: ConnectedServicesRepository
2✔
26
    authenticator: base_models.Authenticator
2✔
27

28
    def get_all(self) -> BlueprintFactoryResponse:
2✔
29
        """List all OAuth2 Clients."""
30

31
        @authenticate(self.authenticator)
2✔
32
        async def _get_all(_: Request, user: base_models.APIUser) -> JSONResponse:
2✔
33
            clients = await self.connected_services_repo.get_oauth2_clients(user=user)
2✔
34
            return validated_json(apispec.ProviderList, clients)
2✔
35

36
        return "/oauth2/providers", ["GET"], _get_all
2✔
37

38
    def get_one(self) -> BlueprintFactoryResponse:
2✔
39
        """Get a specific OAuth2 Client."""
40

41
        @authenticate(self.authenticator)
2✔
42
        async def _get_one(_: Request, user: base_models.APIUser, provider_id: str) -> JSONResponse:
2✔
43
            client = await self.connected_services_repo.get_oauth2_client(provider_id=provider_id, user=user)
2✔
44
            return validated_json(apispec.Provider, client)
1✔
45

46
        return "/oauth2/providers/<provider_id>", ["GET"], _get_one
2✔
47

48
    def post(self) -> BlueprintFactoryResponse:
2✔
49
        """Create a new OAuth2 Client."""
50

51
        @authenticate(self.authenticator)
2✔
52
        @only_admins
2✔
53
        @validate(json=apispec.ProviderPost)
2✔
54
        async def _post(_: Request, user: base_models.APIUser, body: apispec.ProviderPost) -> JSONResponse:
2✔
55
            client = await self.connected_services_repo.insert_oauth2_client(user=user, new_client=body)
2✔
56
            return validated_json(apispec.Provider, client, 201)
2✔
57

58
        return "/oauth2/providers", ["POST"], _post
2✔
59

60
    def patch(self) -> BlueprintFactoryResponse:
2✔
61
        """Partially update a specific OAuth2 Client."""
62

63
        @authenticate(self.authenticator)
2✔
64
        @only_admins
2✔
65
        @validate(json=apispec.ProviderPatch)
2✔
66
        async def _patch(
2✔
67
            _: Request, user: base_models.APIUser, provider_id: str, body: apispec.ProviderPatch
68
        ) -> JSONResponse:
69
            body_dict = body.model_dump(exclude_none=True)
2✔
70
            client = await self.connected_services_repo.update_oauth2_client(
2✔
71
                user=user, provider_id=provider_id, **body_dict
72
            )
73
            return validated_json(apispec.Provider, client)
1✔
74

75
        return "/oauth2/providers/<provider_id>", ["PATCH"], _patch
2✔
76

77
    def delete(self) -> BlueprintFactoryResponse:
2✔
78
        """Delete a specific OAuth2 Client."""
79

80
        @authenticate(self.authenticator)
2✔
81
        @only_admins
2✔
82
        async def _delete(_: Request, user: base_models.APIUser, provider_id: str) -> HTTPResponse:
2✔
83
            await self.connected_services_repo.delete_oauth2_client(user=user, provider_id=provider_id)
2✔
84
            return HTTPResponse(status=204)
2✔
85

86
        return "/oauth2/providers/<provider_id>", ["DELETE"], _delete
2✔
87

88
    def authorize(self) -> BlueprintFactoryResponse:
2✔
89
        """Authorize an OAuth2 Client."""
90

91
        @authenticate(self.authenticator)
2✔
92
        @only_authenticated
2✔
93
        @validate_query(query=apispec.AuthorizeParams)
2✔
94
        async def _authorize(
2✔
95
            request: Request, user: base_models.APIUser, provider_id: str, query: AuthorizeParams
96
        ) -> HTTPResponse:
97
            callback_url = self._get_callback_url(request)
2✔
98
            url = await self.connected_services_repo.authorize_client(
2✔
99
                provider_id=provider_id, user=user, callback_url=callback_url, next_url=query.next_url
100
            )
101
            return redirect(to=url)
1✔
102

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

105
    def authorize_callback(self) -> BlueprintFactoryResponse:
2✔
106
        """OAuth2 authorization callback."""
107

108
        async def _callback(request: Request) -> HTTPResponse:
2✔
109
            params = CallbackParams.model_validate(dict(request.query_args))
1✔
110

111
            callback_url = self._get_callback_url(request)
1✔
112

113
            next_url = await self.connected_services_repo.authorize_callback(
1✔
114
                state=params.state, raw_url=request.url, callback_url=callback_url
115
            )
116

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

119
        return "/oauth2/callback", ["GET"], _callback
2✔
120

121
    def _get_callback_url(self, request: Request) -> str:
2✔
122
        callback_url = request.url_for(f"{self.name}.{self.authorize_callback.__name__}")
2✔
123
        # TODO: configure the server to trust the reverse proxy so that the request scheme is always "https".
124
        # TODO: see also https://github.com/SwissDataScienceCenter/renku-data-services/pull/225
125
        https_callback_url = urlunparse(urlparse(callback_url)._replace(scheme="https"))
2✔
126
        if https_callback_url != callback_url:
2✔
127
            logger.warning("Forcing the callback URL to use https. Trusted proxies configuration may be incorrect.")
2✔
128
        return https_callback_url
2✔
129

130

131
@dataclass(kw_only=True)
2✔
132
class OAuth2ConnectionsBP(CustomBlueprint):
2✔
133
    """Handlers for using OAuth2 connections."""
2✔
134

135
    connected_services_repo: ConnectedServicesRepository
2✔
136
    authenticator: base_models.Authenticator
2✔
137
    internal_gitlab_authenticator: base_models.Authenticator
2✔
138

139
    def get_all(self) -> BlueprintFactoryResponse:
2✔
140
        """List all OAuth2 connections."""
141

142
        @authenticate(self.authenticator)
2✔
143
        async def _get_all(_: Request, user: base_models.APIUser) -> JSONResponse:
2✔
144
            connections = await self.connected_services_repo.get_oauth2_connections(user=user)
2✔
145
            return validated_json(apispec.ConnectionList, connections)
2✔
146

147
        return "/oauth2/connections", ["GET"], _get_all
2✔
148

149
    def get_one(self) -> BlueprintFactoryResponse:
2✔
150
        """Get a specific OAuth2 connection."""
151

152
        @authenticate(self.authenticator)
2✔
153
        async def _get_one(_: Request, user: base_models.APIUser, connection_id: str) -> JSONResponse:
2✔
154
            connection = await self.connected_services_repo.get_oauth2_connection(
1✔
155
                connection_id=connection_id, user=user
156
            )
UNCOV
157
            return validated_json(apispec.Connection, connection)
×
158

159
        return "/oauth2/connections/<connection_id>", ["GET"], _get_one
2✔
160

161
    def get_account(self) -> BlueprintFactoryResponse:
2✔
162
        """Get the account information for a specific OAuth2 connection."""
163

164
        @authenticate(self.authenticator)
2✔
165
        async def _get_account(_: Request, user: base_models.APIUser, connection_id: str) -> JSONResponse:
2✔
166
            account = await self.connected_services_repo.get_oauth2_connected_account(
2✔
167
                connection_id=connection_id, user=user
168
            )
169
            return validated_json(apispec.ConnectedAccount, account)
1✔
170

171
        return "/oauth2/connections/<connection_id>/account", ["GET"], _get_account
2✔
172

173
    def get_token(self) -> BlueprintFactoryResponse:
2✔
174
        """Get the access token for a specific OAuth2 connection."""
175

176
        @authenticate(self.authenticator)
2✔
177
        async def _get_token(_: Request, user: base_models.APIUser, connection_id: str) -> JSONResponse:
2✔
178
            token = await self.connected_services_repo.get_oauth2_connection_token(
1✔
179
                connection_id=connection_id, user=user
180
            )
181
            return json(token.dump_for_api())
1✔
182

183
        return "/oauth2/connections/<connection_id>/token", ["GET"], _get_token
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