-
Notifications
You must be signed in to change notification settings - Fork 101
feat(skore-hub-project): Persist URI used for authentication
#2045
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
|
||
|
|
||
| def test_post_oauth_refresh_token(respx_mock): | ||
| route = respx_mock.post(urljoin(URI, "identity/oauth/token/refresh")).mock( | ||
| Response( | ||
| 200, | ||
| json={"access_token": "A", "refresh_token": "B", "expires_at": "C"}, | ||
| ) | ||
| ) | ||
|
|
||
| access_token, refresh_token, expires_at = api.post_oauth_refresh_token("token") | ||
|
|
||
| assert route.calls.last.request.read() == b'{"refresh_token":"token"}' | ||
| assert access_token == "A" | ||
| assert refresh_token == "B" | ||
| assert expires_at == "C" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved to test_token.py.
| def post_oauth_refresh_token(refresh_token: str): | ||
| """Refresh an access token using a provided refresh token. | ||
| This endpoint allows a client to obtain a new access token | ||
| by providing a valid refresh token. | ||
| Parameters | ||
| ---------- | ||
| refresh_token : str | ||
| A valid refresh token | ||
| Returns | ||
| ------- | ||
| tuple | ||
| A tuple containing: | ||
| - access_token : str | ||
| The OAuth access token | ||
| - refresh_token : str | ||
| The OAuth refresh token | ||
| - expires_at : str | ||
| The expiration datetime as ISO 8601 str of the access token | ||
| """ | ||
| from skore_hub_project.client.client import HUBClient | ||
|
|
||
| url = "identity/oauth/token/refresh" | ||
| json = {"refresh_token": refresh_token} | ||
|
|
||
| with HUBClient(authenticated=False) as client: | ||
| response = client.post(url, json=json).json() | ||
|
|
||
| return ( | ||
| response["access_token"], | ||
| response["refresh_token"], | ||
| response["expires_at"], | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved to token.py.
| def get_oauth_device_login(success_uri: str | None = None): | ||
| """Initiate device OAuth flow. | ||
| Initiates the OAuth device flow. | ||
| Provides the user with a URL and a OTP code to authenticate the device. | ||
| Parameters | ||
| ---------- | ||
| success_uri : str, optional | ||
| The URI to redirect to after successful authentication. | ||
| If not provided, defaults to None. | ||
| Returns | ||
| ------- | ||
| tuple | ||
| A tuple containing: | ||
| - authorization_url: str | ||
| The URL to which the user needs to navigate | ||
| - device_code: str | ||
| The device code used for authentication | ||
| - user_code: str | ||
| The user code that needs to be entered on the authorization page | ||
| """ | ||
| from skore_hub_project.client.client import HUBClient | ||
|
|
||
| url = "identity/oauth/device/login" | ||
| params = {"success_uri": success_uri} if success_uri is not None else {} | ||
|
|
||
| with HUBClient(authenticated=False) as client: | ||
| response = client.get(url, params=params).json() | ||
|
|
||
| return ( | ||
| response["authorization_url"], | ||
| response["device_code"], | ||
| response["user_code"], | ||
| ) | ||
|
|
||
|
|
||
| def post_oauth_device_callback(state: str, user_code: str): | ||
| """Validate the user-provided device code. | ||
| This endpoint verifies the code entered by the user during the device auth flow. | ||
| Parameters | ||
| ---------- | ||
| state: str | ||
| The unique value identifying the device flow. | ||
| user_code: str | ||
| The code entered by the user. | ||
| """ | ||
| from skore_hub_project.client.client import HUBClient | ||
|
|
||
| url = "identity/oauth/device/callback" | ||
| data = {"state": state, "user_code": user_code} | ||
|
|
||
| with HUBClient(authenticated=False) as client: | ||
| client.post(url, data=data) | ||
|
|
||
|
|
||
| def get_oauth_device_token(device_code: str): | ||
| """Exchanges the device code for an access token. | ||
| This endpoint completes the device authorization flow | ||
| by exchanging the validated device code for an access token. | ||
| Parameters | ||
| ---------- | ||
| device_code : str | ||
| The device code to exchange for tokens | ||
| Returns | ||
| ------- | ||
| tuple | ||
| A tuple containing: | ||
| - access_token : str | ||
| The OAuth access token | ||
| - refresh_token : str | ||
| The OAuth refresh token | ||
| - expires_at : str | ||
| The expiration datetime as ISO 8601 str of the access token | ||
| """ | ||
| from skore_hub_project.client.client import HUBClient | ||
|
|
||
| url = "identity/oauth/device/token" | ||
| params = {"device_code": device_code} | ||
|
|
||
| with HUBClient(authenticated=False) as client: | ||
| response = client.get(url, params=params).json() | ||
| tokens = response["token"] | ||
|
|
||
| return ( | ||
| tokens["access_token"], | ||
| tokens["refresh_token"], | ||
| tokens["expires_at"], | ||
| ) | ||
|
|
||
|
|
||
| def get_oauth_device_code_probe(device_code: str, *, timeout=600): | ||
| """Ensure authorization code is acknowledged. | ||
| Start polling, wait for the authorization code to be acknowledged by the hub. | ||
| This is mandatory to be authorize to exchange with a token. | ||
| Parameters | ||
| ---------- | ||
| device_code : str | ||
| The device code to exchange for tokens. | ||
| """ | ||
| from skore_hub_project.client.client import HUBClient | ||
|
|
||
| url = "identity/oauth/device/code-probe" | ||
| params = {"device_code": device_code} | ||
|
|
||
| with HUBClient(authenticated=False) as client: | ||
| start = datetime.now() | ||
|
|
||
| while True: | ||
| try: | ||
| client.get(url, params=params) | ||
| except HTTPError as exc: | ||
| if exc.response.status_code != 400: | ||
| raise | ||
|
|
||
| if (datetime.now() - start).total_seconds() >= timeout: | ||
| raise TimeoutException("Authentication timeout") from exc | ||
|
|
||
| sleep(0.5) | ||
| else: | ||
| break |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved to login.py.
| @mark.parametrize("success_uri", [None, "toto"]) | ||
| def test_get_oauth_device_login(respx_mock, success_uri): | ||
| respx_mock.get(urljoin(URI, "identity/oauth/device/login")).mock( | ||
| Response( | ||
| 200, | ||
| json={ | ||
| "authorization_url": "A", | ||
| "device_code": "B", | ||
| "user_code": "C", | ||
| }, | ||
| ) | ||
| ) | ||
|
|
||
| params = {} if success_uri is None else {"success_uri": "toto"} | ||
| authorization_url, device_code, user_code = api.get_oauth_device_login( | ||
| success_uri=success_uri | ||
| ) | ||
|
|
||
| assert dict(respx_mock.calls.last.request.url.params) == params | ||
| assert authorization_url == "A" | ||
| assert device_code == "B" | ||
| assert user_code == "C" | ||
|
|
||
|
|
||
| def test_post_oauth_device_callback(respx_mock): | ||
| route = respx_mock.post(urljoin(URI, "identity/oauth/device/callback")).mock( | ||
| Response(201, json={}) | ||
| ) | ||
|
|
||
| api.post_oauth_device_callback("my_state", "my_user_code") | ||
|
|
||
| assert route.called | ||
| assert route.calls.last.request.read() == b"state=my_state&user_code=my_user_code" | ||
|
|
||
|
|
||
| def test_get_oauth_device_token(respx_mock): | ||
| respx_mock.get(urljoin(URI, "identity/oauth/device/token")).mock( | ||
| Response( | ||
| 200, | ||
| json={ | ||
| "token": { | ||
| "access_token": "A", | ||
| "refresh_token": "B", | ||
| "expires_at": "C", | ||
| } | ||
| }, | ||
| ) | ||
| ) | ||
|
|
||
| access_token, refresh_token, expires_at = api.get_oauth_device_token( | ||
| "<device_code>" | ||
| ) | ||
|
|
||
| assert access_token == "A" | ||
| assert refresh_token == "B" | ||
| assert expires_at == "C" | ||
| assert dict(respx_mock.calls.last.request.url.params) == { | ||
| "device_code": "<device_code>" | ||
| } | ||
|
|
||
|
|
||
| def test_get_oauth_device_code_probe(monkeypatch, respx_mock): | ||
| monkeypatch.setattr("skore_hub_project.client.api.sleep", lambda _: None) | ||
| respx_mock.get(urljoin(URI, "identity/oauth/device/code-probe")).mock( | ||
| side_effect=[ | ||
| Response(400), | ||
| Response(400), | ||
| Response(200), | ||
| ] | ||
| ) | ||
|
|
||
| api.get_oauth_device_code_probe("<device_code>") | ||
|
|
||
| assert len(respx_mock.calls) == 3 | ||
| assert dict(respx_mock.calls.last.request.url.params) == { | ||
| "device_code": "<device_code>", | ||
| } | ||
|
|
||
|
|
||
| def test_get_oauth_device_code_probe_exception(respx_mock): | ||
| respx_mock.get(urljoin(URI, "identity/oauth/device/code-probe")).mock( | ||
| side_effect=[ | ||
| Response(404), | ||
| Response(400), | ||
| Response(200), | ||
| ] | ||
| ) | ||
|
|
||
| with raises(HTTPError) as excinfo: | ||
| api.get_oauth_device_code_probe("<device_code>") | ||
|
|
||
| assert excinfo.value.response.status_code == 404 | ||
| assert len(respx_mock.calls) == 1 | ||
| assert dict(respx_mock.calls.last.request.url.params) == { | ||
| "device_code": "<device_code>", | ||
| } | ||
|
|
||
|
|
||
| def test_get_oauth_device_code_probe_timeout(respx_mock): | ||
| respx_mock.get(urljoin(URI, "identity/oauth/device/code-probe")).mock( | ||
| side_effect=[ | ||
| Response(400), | ||
| Response(400), | ||
| Response(200), | ||
| ] | ||
| ) | ||
|
|
||
| with raises(TimeoutException): | ||
| api.get_oauth_device_code_probe("<device_code>", timeout=0) | ||
|
|
||
| assert len(respx_mock.calls) == 1 | ||
| assert dict(respx_mock.calls.last.request.url.params) == { | ||
| "device_code": "<device_code>", | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved to test_login.py.
9bf4946 to
05d47de
Compare
Coverage Report for |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| File | Stmts | Miss | Cover | Missing |
|---|---|---|---|---|
| skore-hub-project/src/skore_hub_project | ||||
| __init__.py | 22 | 1 | 95% | 39 |
| protocol.py | 29 | 0 | 100% | |
| skore-hub-project/src/skore_hub_project/artifact | ||||
| __init__.py | 0 | 0 | 100% | |
| artifact.py | 23 | 0 | 100% | |
| serializer.py | 26 | 0 | 100% | |
| upload.py | 36 | 4 | 88% | 175, 177–178, 180 |
| skore-hub-project/src/skore_hub_project/artifact/media | ||||
| __init__.py | 5 | 0 | 100% | |
| data.py | 22 | 0 | 100% | |
| feature_importance.py | 34 | 0 | 100% | |
| media.py | 10 | 0 | 100% | |
| model.py | 9 | 0 | 100% | |
| performance.py | 44 | 0 | 100% | |
| skore-hub-project/src/skore_hub_project/artifact/pickle | ||||
| __init__.py | 2 | 0 | 100% | |
| pickle.py | 24 | 0 | 100% | |
| skore-hub-project/src/skore_hub_project/authentication | ||||
| __init__.py | 0 | 0 | 100% | |
| login.py | 66 | 0 | 100% | |
| logout.py | 4 | 0 | 100% | |
| token.py | 24 | 0 | 100% | |
| uri.py | 18 | 0 | 100% | |
| skore-hub-project/src/skore_hub_project/client | ||||
| __init__.py | 0 | 0 | 100% | |
| client.py | 60 | 0 | 100% | |
| skore-hub-project/src/skore_hub_project/metric | ||||
| __init__.py | 10 | 0 | 100% | |
| accuracy.py | 35 | 0 | 100% | |
| brier_score.py | 35 | 0 | 100% | |
| log_loss.py | 35 | 0 | 100% | |
| metric.py | 49 | 1 | 97% | 25 |
| precision.py | 63 | 0 | 100% | |
| r2.py | 35 | 0 | 100% | |
| recall.py | 63 | 0 | 100% | |
| rmse.py | 35 | 0 | 100% | |
| roc_auc.py | 35 | 0 | 100% | |
| timing.py | 90 | 4 | 95% | 52–53, 115–116 |
| skore-hub-project/src/skore_hub_project/project | ||||
| __init__.py | 0 | 0 | 100% | |
| project.py | 102 | 3 | 97% | 204, 308, 338 |
| skore-hub-project/src/skore_hub_project/report | ||||
| __init__.py | 3 | 0 | 100% | |
| cross_validation_report.py | 64 | 1 | 98% | 204 |
| estimator_report.py | 12 | 0 | 100% | |
| report.py | 52 | 0 | 100% | |
| TOTAL | 1176 | 14 | 98% | |
| Tests | Skipped | Failures | Errors | Time |
|---|---|---|---|---|
| 139 | 0 💤 | 0 ❌ | 0 🔥 | 1m 30s ⏱️ |
ee11cf5 to
8302602
Compare
URI in the tokenURI used for authentication
rouk1
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like it fits the requirements.
Closes #2037.
Create a new URI object that is persisted next to the token during
skore-hub-login.Also, check at runtime the coherence of the URI that user wants to reach: if the token's URI is different of the envar URI, raise an exception.