Skip to content

Commit 8b2c64b

Browse files
committed
[DOP-23122] Use async methods of Keycloak client
1 parent 162b0a0 commit 8b2c64b

File tree

8 files changed

+64
-48
lines changed

8 files changed

+64
-48
lines changed

poetry.lock

Lines changed: 10 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ onetl = {extras = ["spark", "s3", "hdfs"], version = "^0.12.0"}
139139
faker = ">=28.4.1,<34.0.0"
140140
coverage = "^7.6.1"
141141
gevent = "^24.2.1"
142-
responses = "*"
142+
respx = "*"
143143

144144
[tool.poetry.group.dev.dependencies]
145145
mypy = "^1.11.2"

syncmaster/server/providers/auth/base_provider.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def __init__(
5252
...
5353

5454
@abstractmethod
55-
async def get_current_user(self, access_token: Any, *args, **kwargs) -> User:
55+
async def get_current_user(self, access_token: str | None, **kwargs) -> User:
5656
"""
5757
This method should return currently logged in user.
5858

syncmaster/server/providers/auth/dummy_provider.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def setup(cls, app: FastAPI) -> FastAPI:
3737
app.dependency_overrides[DummyAuthProviderSettings] = lambda: settings
3838
return app
3939

40-
async def get_current_user(self, access_token: str, *args, **kwargs) -> User:
40+
async def get_current_user(self, access_token: str | None, **kwargs) -> User:
4141
if not access_token:
4242
raise AuthorizationError("Missing auth credentials")
4343

syncmaster/server/providers/auth/keycloak_provider.py

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from fastapi import Depends, FastAPI, Request
77
from keycloak import KeycloakOpenID
88

9+
from syncmaster.db.models import User
910
from syncmaster.exceptions import EntityNotFoundError
1011
from syncmaster.exceptions.auth import AuthorizationError
1112
from syncmaster.exceptions.redirect import RedirectException
@@ -63,7 +64,7 @@ async def get_token_authorization_code_grant(
6364
) -> dict[str, Any]:
6465
try:
6566
redirect_uri = redirect_uri or self.settings.keycloak.redirect_uri
66-
token = self.keycloak_openid.token(
67+
token = await self.keycloak_openid.a_token(
6768
grant_type="authorization_code",
6869
code=code,
6970
redirect_uri=redirect_uri,
@@ -72,10 +73,8 @@ async def get_token_authorization_code_grant(
7273
except Exception as e:
7374
raise AuthorizationError("Failed to get token") from e
7475

75-
async def get_current_user(self, access_token: str, *args, **kwargs) -> Any:
76+
async def get_current_user(self, access_token: str | None, **kwargs) -> User:
7677
request: Request = kwargs["request"]
77-
refresh_token = request.session.get("refresh_token")
78-
7978
if not access_token:
8079
log.debug("No access token found in session.")
8180
self.redirect_to_auth(request.url.path)
@@ -86,8 +85,9 @@ async def get_current_user(self, access_token: str, *args, **kwargs) -> Any:
8685
token_info = self.keycloak_openid.decode_token(token=access_token)
8786
except Exception as e:
8887
log.info("Access token is invalid or expired: %s", e)
89-
token_info = None
88+
token_info = {}
9089

90+
refresh_token = request.session.get("refresh_token")
9191
if not token_info and refresh_token:
9292
log.debug("Access token invalid. Attempting to refresh.")
9393

@@ -99,9 +99,7 @@ async def get_current_user(self, access_token: str, *args, **kwargs) -> Any:
9999
request.session["access_token"] = new_access_token
100100
request.session["refresh_token"] = new_refresh_token
101101

102-
token_info = self.keycloak_openid.decode_token(
103-
token=new_access_token,
104-
)
102+
token_info = self.keycloak_openid.decode_token(token=new_access_token)
105103
log.debug("Access token refreshed and decoded successfully.")
106104
except Exception as e:
107105
log.debug("Failed to refresh access token: %s", e)
@@ -110,19 +108,19 @@ async def get_current_user(self, access_token: str, *args, **kwargs) -> Any:
110108
# these names are hardcoded in keycloak:
111109
# https://github.com/keycloak/keycloak/blob/3ca3a4ad349b4d457f6829eaf2ae05f1e01408be/core/src/main/java/org/keycloak/representations/IDToken.java
112110
user_id = token_info.get("sub")
111+
if not user_id:
112+
raise AuthorizationError("Invalid token payload")
113+
113114
login = token_info.get("preferred_username")
114115
email = token_info.get("email")
115116
first_name = token_info.get("given_name")
116117
middle_name = token_info.get("middle_name")
117118
last_name = token_info.get("family_name")
118119

119-
if not user_id:
120-
raise AuthorizationError("Invalid token payload")
121-
122-
async with self._uow:
123-
try:
124-
user = await self._uow.user.read_by_username(login)
125-
except EntityNotFoundError:
120+
try:
121+
user = await self._uow.user.read_by_username(login)
122+
except EntityNotFoundError:
123+
async with self._uow:
126124
user = await self._uow.user.create(
127125
username=login,
128126
email=email,
@@ -134,7 +132,7 @@ async def get_current_user(self, access_token: str, *args, **kwargs) -> Any:
134132
return user
135133

136134
async def refresh_access_token(self, refresh_token: str) -> dict[str, Any]:
137-
new_tokens = self.keycloak_openid.refresh_token(refresh_token)
135+
new_tokens = await self.keycloak_openid.a_refresh_token(refresh_token)
138136
return new_tokens
139137

140138
def redirect_to_auth(self, path: str) -> None:
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# SPDX-FileCopyrightText: 2023-2024 MTS PJSC
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from pydantic import BaseModel, Field, ImportString
5+
6+
7+
class AuthSettings(BaseModel):
8+
"""Authorization-related settings.
9+
10+
Here you can set auth provider class.
11+
12+
Examples
13+
--------
14+
15+
.. code-block:: bash
16+
17+
SYNCMASTER__AUTH__PROVIDER=syncmaster.server.providers.auth.dummy_provider.DummyAuthProvider
18+
"""
19+
20+
provider: ImportString = Field( # type: ignore[assignment]
21+
default="syncmaster.server.providers.auth.dummy_provider.DummyAuthProvider",
22+
description="Full name of auth provider class",
23+
validate_default=True,
24+
)
25+
26+
class Config:
27+
extra = "allow"

tests/test_unit/test_auth/auth_fixtures/keycloak_fixture.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from base64 import b64encode
44

55
import pytest
6-
import responses
6+
import respx
77
from cryptography.hazmat.primitives import serialization
88
from cryptography.hazmat.primitives.asymmetric import rsa
99
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
@@ -85,9 +85,7 @@ def mock_keycloak_well_known(settings):
8585
realm_name = settings.auth.dict()["keycloak"]["client_id"]
8686
well_known_url = f"{server_url}/realms/{realm_name}/.well-known/openid-configuration"
8787

88-
responses.add(
89-
responses.GET,
90-
well_known_url,
88+
respx.get(well_known_url).respond(
9189
json={
9290
"authorization_endpoint": f"{server_url}/realms/{realm_name}/protocol/openid-connect/auth",
9391
"token_endpoint": f"{server_url}/realms/{realm_name}/protocol/openid-connect/token",
@@ -108,9 +106,7 @@ def mock_keycloak_realm(settings, rsa_keys):
108106
realm_url = f"{server_url}/realms/{realm_name}"
109107
public_pem_str = get_public_key_pem(rsa_keys["public_key"])
110108

111-
responses.add(
112-
responses.GET,
113-
realm_url,
109+
respx.get(realm_url).respond(
114110
json={
115111
"realm": realm_name,
116112
"public_key": public_pem_str,
@@ -144,9 +140,7 @@ def mock_keycloak_token_refresh(settings, rsa_keys):
144140
new_access_token = jwt.encode(payload, private_pem, algorithm="RS256")
145141
new_refresh_token = "mock_new_refresh_token"
146142

147-
responses.add(
148-
responses.POST,
149-
token_url,
143+
respx.post(token_url).respond(
150144
json={
151145
"access_token": new_access_token,
152146
"refresh_token": new_refresh_token,

tests/test_unit/test_auth/test_auth_keycloak.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import logging
22

33
import pytest
4-
import responses
4+
import respx
55
from httpx import AsyncClient
66

77
from syncmaster.server.settings import ServerAppSettings as Settings
@@ -11,7 +11,7 @@
1111
pytestmark = [pytest.mark.asyncio, pytest.mark.server]
1212

1313

14-
@responses.activate
14+
@respx.mock
1515
@pytest.mark.parametrize(
1616
"settings",
1717
[
@@ -33,7 +33,7 @@ async def test_get_keycloak_user_unauthorized(client: AsyncClient, mock_keycloak
3333
)
3434

3535

36-
@responses.activate
36+
@respx.mock
3737
@pytest.mark.parametrize(
3838
"settings",
3939
[
@@ -71,7 +71,7 @@ async def test_get_keycloak_user_authorized(
7171
}
7272

7373

74-
@responses.activate
74+
@respx.mock
7575
@pytest.mark.parametrize(
7676
"settings",
7777
[
@@ -116,7 +116,7 @@ async def test_get_keycloak_user_expired_access_token(
116116
}
117117

118118

119-
@responses.activate
119+
@respx.mock
120120
@pytest.mark.parametrize(
121121
"settings",
122122
[
@@ -155,7 +155,7 @@ async def test_get_keycloak_deleted_user(
155155
}
156156

157157

158-
@responses.activate
158+
@respx.mock
159159
@pytest.mark.parametrize(
160160
"settings",
161161
[

0 commit comments

Comments
 (0)