|
5 | 5 | import ssl |
6 | 6 | from collections.abc import Awaitable, Callable |
7 | 7 | from datetime import datetime, timedelta |
| 8 | +from json import JSONDecodeError |
8 | 9 | from typing import Any, Concatenate, ParamSpec, Protocol, TypeVar, cast |
9 | 10 | from zoneinfo import ZoneInfo |
10 | 11 |
|
|
13 | 14 | from sqlalchemy.ext.asyncio import AsyncSession |
14 | 15 |
|
15 | 16 | from renku_data_services import errors |
| 17 | +from renku_data_services.app_config import logging |
| 18 | + |
| 19 | +logger = logging.getLogger(__name__) |
16 | 20 |
|
17 | 21 |
|
18 | 22 | @functools.lru_cache(1) |
@@ -83,91 +87,152 @@ async def transaction_wrapper(self: _WithSessionMaker, *args: _P.args, **kwargs: |
83 | 87 | return transaction_wrapper |
84 | 88 |
|
85 | 89 |
|
86 | | -def _get_url(host: str) -> str: |
87 | | - return f"https://{host}/openbis/openbis/rmi-application-server-v3.json" |
| 90 | +def _get_openbis_url(openbis_host: str) -> str: |
| 91 | + return f"https://{openbis_host}/openbis/openbis/rmi-application-server-v3.json" |
88 | 92 |
|
89 | 93 |
|
90 | | -async def _get_openbis_session_token(host: str, login: dict[str, Any], timeout: int) -> str: |
| 94 | +async def _get_openbis_session_token(openbis_host: str, login: dict[str, Any], timeout: int) -> str: |
91 | 95 | async with httpx.AsyncClient(verify=get_ssl_context(), timeout=5) as client: |
92 | | - response = await client.post(_get_url(host), json=login, timeout=timeout) |
93 | | - if response.status_code == 200: |
| 96 | + response = await client.post(_get_openbis_url(openbis_host), json=login, timeout=timeout) |
| 97 | + if response.status_code != 200: |
| 98 | + raise errors.ThirdPartyAPIError( |
| 99 | + detail="OpenBIS responded with a non-200 status code when attempting to get a session token." |
| 100 | + ) |
| 101 | + try: |
94 | 102 | json: dict[str, str] = response.json() |
95 | | - if "result" in json and json["result"] is not None: |
96 | | - return json["result"] |
97 | | - # No session token was returned. Username and password may be incorrect. |
98 | | - raise errors.GeneralBadRequest() |
99 | | - |
100 | | - # An openBIS session token related request failed. |
101 | | - raise errors.BaseError() |
| 103 | + except JSONDecodeError as err: |
| 104 | + raise errors.ThirdPartyAPIError( |
| 105 | + detail="Did not receive a json-formatted output when attempting to get a session token from OpenBIS." |
| 106 | + ) from err |
| 107 | + if json.get("result") is None: |
| 108 | + raise errors.ThirdPartyAPIError( |
| 109 | + detail="The response from OpenBIS was parsed but it does not contain the exepected field(s) " |
| 110 | + "when attempting to get a session token." |
| 111 | + ) |
| 112 | + return json["result"] |
102 | 113 |
|
103 | 114 |
|
104 | 115 | async def get_openbis_session_token_for_anonymous_user( |
105 | | - host: str, |
| 116 | + openbis_host: str, |
106 | 117 | timeout: int = 12, |
107 | 118 | ) -> str: |
108 | 119 | """Requests an openBIS session token for the anonymous user.""" |
109 | 120 | return await _get_openbis_session_token( |
110 | | - host, {"method": "loginAsAnonymousUser", "params": [], "id": "1", "jsonrpc": "2.0"}, timeout |
| 121 | + openbis_host, {"method": "loginAsAnonymousUser", "params": [], "id": "1", "jsonrpc": "2.0"}, timeout |
111 | 122 | ) |
112 | 123 |
|
113 | 124 |
|
114 | 125 | async def get_openbis_session_token( |
115 | | - host: str, |
| 126 | + openbis_host: str, |
116 | 127 | username: str, |
117 | 128 | password: str, |
118 | 129 | timeout: int = 12, |
119 | 130 | ) -> str: |
120 | 131 | """Requests an openBIS session token with the user's login credentials.""" |
121 | 132 | return await _get_openbis_session_token( |
122 | | - host, {"method": "login", "params": [username, password], "id": "2", "jsonrpc": "2.0"}, timeout |
| 133 | + openbis_host, {"method": "login", "params": [username, password], "id": "2", "jsonrpc": "2.0"}, timeout |
123 | 134 | ) |
124 | 135 |
|
125 | 136 |
|
126 | 137 | async def get_openbis_pat( |
127 | | - host: str, |
| 138 | + openbis_host: str, |
128 | 139 | session_id: str, |
129 | 140 | personal_access_token_session_name: str = "renku", |
130 | 141 | minimum_validity_in_days: int = 2, |
131 | 142 | timeout: int = 12, |
132 | 143 | ) -> tuple[str, datetime]: |
133 | 144 | """Requests an openBIS PAT with an openBIS session ID.""" |
134 | | - url = _get_url(host) |
| 145 | + url = _get_openbis_url(openbis_host) |
135 | 146 |
|
136 | 147 | async with httpx.AsyncClient(verify=get_ssl_context(), timeout=5) as client: |
137 | 148 | get_server_information = {"method": "getServerInformation", "params": [session_id], "id": "2", "jsonrpc": "2.0"} |
138 | 149 | response = await client.post(url, json=get_server_information, timeout=timeout) |
139 | | - if response.status_code == 200: |
| 150 | + if response.status_code != 200: |
| 151 | + logger.error( |
| 152 | + f"Received a non-200 response, {response.status_code} from OpenBIS " |
| 153 | + f"for performing 'getServerInformation'. Reponse content: {response.text}" |
| 154 | + ) |
| 155 | + raise errors.ThirdPartyAPIError( |
| 156 | + detail="OpenBIS responded with a non-200 status code when performing 'getServerInformation'." |
| 157 | + ) |
| 158 | + try: |
140 | 159 | json1: dict[str, dict[str, str]] = response.json() |
141 | | - if "error" not in json1: |
142 | | - personal_access_tokens_max_validity_period = int( |
143 | | - json1["result"]["personal-access-tokens-max-validity-period"] |
144 | | - ) |
145 | | - valid_from = datetime.now(ZoneInfo("Europe/Berlin")) |
146 | | - valid_to = valid_from + timedelta(seconds=personal_access_tokens_max_validity_period) |
147 | | - validity_in_days = (valid_to - valid_from).days |
148 | | - if validity_in_days >= minimum_validity_in_days: |
149 | | - create_personal_access_tokens = { |
150 | | - "method": "createPersonalAccessTokens", |
151 | | - "params": [ |
152 | | - session_id, |
153 | | - { |
154 | | - "@type": "as.dto.pat.create.PersonalAccessTokenCreation", |
155 | | - "sessionName": personal_access_token_session_name, |
156 | | - "validFromDate": int(valid_from.timestamp() * 1000), |
157 | | - "validToDate": int(valid_to.timestamp() * 1000), |
158 | | - }, |
159 | | - ], |
160 | | - "id": "2", |
161 | | - "jsonrpc": "2.0", |
162 | | - } |
163 | | - response = await client.post(url, json=create_personal_access_tokens, timeout=timeout) |
164 | | - if response.status_code == 200: |
165 | | - json2: dict[str, list[dict[str, str]]] = response.json() |
166 | | - return json2["result"][0]["permId"], valid_to |
167 | | - else: |
168 | | - # The maximum allowed validity period of a personal access token is less than |
169 | | - # "minimum_validity_in_days" days. |
170 | | - raise errors.GeneralBadRequest() |
171 | | - |
172 | | - # An openBIS personal access token related request failed. |
173 | | - raise errors.BaseError() |
| 160 | + except JSONDecodeError as err: |
| 161 | + logger.error( |
| 162 | + f"Could not parse OpenBIS response for performing 'getServerInformation' into JSON. " |
| 163 | + f"Response content: {response.text}" |
| 164 | + ) |
| 165 | + raise errors.ThirdPartyAPIError( |
| 166 | + detail="Could not parse OpenBIS response about server information into JSON." |
| 167 | + ) from err |
| 168 | + if "error" in json1: |
| 169 | + raise errors.ThirdPartyAPIError( |
| 170 | + detail=f"The response from OpenBIS for 'getServerInformation' contained errors: {json1['error']}." |
| 171 | + ) |
| 172 | + if json1.get("result", {}).get("personal-access-tokens-max-validity-period") is None: |
| 173 | + logger.error( |
| 174 | + f"The response from OpenBIS for 'getServerInformation' did not contain the expected " |
| 175 | + "token validity period fields. " |
| 176 | + f"Response content: {response.text}" |
| 177 | + ) |
| 178 | + raise errors.ThirdPartyAPIError( |
| 179 | + detail="The response from OpenBIS for 'getServerInformation' " |
| 180 | + "did not contain the expected token validity period." |
| 181 | + ) |
| 182 | + personal_access_tokens_max_validity_period = int(json1["result"]["personal-access-tokens-max-validity-period"]) |
| 183 | + valid_from = datetime.now(ZoneInfo("Europe/Berlin")) |
| 184 | + valid_to = valid_from + timedelta(seconds=personal_access_tokens_max_validity_period) |
| 185 | + validity_in_days = (valid_to - valid_from).days |
| 186 | + if validity_in_days < minimum_validity_in_days: |
| 187 | + raise errors.ThirdPartyAPIError( |
| 188 | + detail="The allowed validity of the personal access token from OpenBIS is shorter " |
| 189 | + f"than the required minimum validity of {minimum_validity_in_days} days" |
| 190 | + ) |
| 191 | + create_personal_access_tokens = { |
| 192 | + "method": "createPersonalAccessTokens", |
| 193 | + "params": [ |
| 194 | + session_id, |
| 195 | + { |
| 196 | + "@type": "as.dto.pat.create.PersonalAccessTokenCreation", |
| 197 | + "sessionName": personal_access_token_session_name, |
| 198 | + "validFromDate": int(valid_from.timestamp() * 1000), |
| 199 | + "validToDate": int(valid_to.timestamp() * 1000), |
| 200 | + }, |
| 201 | + ], |
| 202 | + "id": "2", |
| 203 | + "jsonrpc": "2.0", |
| 204 | + } |
| 205 | + response = await client.post(url, json=create_personal_access_tokens, timeout=timeout) |
| 206 | + if response.status_code != 200: |
| 207 | + logger.error( |
| 208 | + "OpenBIS responded with a non-200 status code when creating a personal access token. " |
| 209 | + f"Status code: {response.status_code}, response content: {response.text}" |
| 210 | + ) |
| 211 | + raise errors.ThirdPartyAPIError( |
| 212 | + detail="OpenBIS responded with a non-200 status code when creating a personal access token." |
| 213 | + ) |
| 214 | + try: |
| 215 | + json2: dict[str, list[dict[str, str]]] = response.json() |
| 216 | + except JSONDecodeError as err: |
| 217 | + logger.error( |
| 218 | + "Could not parse OpenBIS response for creating a personal access token into JSON." |
| 219 | + f"Response content: {response.text}" |
| 220 | + ) |
| 221 | + raise errors.ThirdPartyAPIError( |
| 222 | + detail="Could not parse OpenBIS response for creating personal access token into JSON." |
| 223 | + ) from err |
| 224 | + if ( |
| 225 | + not isinstance(json2.get("result"), list) |
| 226 | + or len(json2["result"]) == 0 |
| 227 | + or json2["result"][0].get("permId") is None |
| 228 | + ): |
| 229 | + logger.error( |
| 230 | + "The response from OpenBIS did not have the required 'result[0].permId' field in the response " |
| 231 | + "from creating a personal access token. " |
| 232 | + f"Response content: {response.text}" |
| 233 | + ) |
| 234 | + raise errors.ThirdPartyAPIError( |
| 235 | + detail="The response from OpenBIS did not have the required 'result[0].permId' field in the response " |
| 236 | + "from creating a personal access token." |
| 237 | + ) |
| 238 | + return json2["result"][0]["permId"], valid_to |
0 commit comments