Skip to content

Commit 542e14d

Browse files
committed
[DOP-23122] Switch KeycloakAuthProvider to async methods
1 parent 02bdf28 commit 542e14d

File tree

9 files changed

+51
-61
lines changed

9 files changed

+51
-61
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Replace sync methods of Keycloak client with async ones. Previously interaction with Keycloak could block asyncio event loop.

poetry.lock

Lines changed: 11 additions & 16 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
@@ -135,7 +135,7 @@ pytest-lazy-fixtures = "^1.1.1"
135135
faker = "^37.4.0"
136136
coverage = "^7.9.1"
137137
gevent = ">=24.11.1,<26.0.0"
138-
responses = "^0.25.7"
138+
respx = "^0.22.0"
139139
dirty-equals = "^0.9.0"
140140

141141
[tool.poetry.group.dev.dependencies]

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, request: Request) -> User:
55+
async def get_current_user(self, access_token: str | None, request: Request) -> 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, *args, **kwargs) -> User:
4141
if not access_token:
4242
raise AuthorizationError("Missing auth credentials")
4343

syncmaster/server/providers/auth/keycloak_provider.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# SPDX-FileCopyrightText: 2023-2024 MTS PJSC
22
# SPDX-License-Identifier: Apache-2.0
33
import logging
4-
from typing import Annotated, Any
4+
from typing import Annotated, Any, NoReturn
55

66
from fastapi import Depends, FastAPI, Request
77
from jwcrypto.common import JWException
@@ -62,25 +62,24 @@ async def get_token_authorization_code_grant(
6262
client_secret: str | None = None,
6363
) -> dict[str, Any]:
6464
try:
65-
token = self.keycloak_openid.token(
65+
return await self.keycloak_openid.a_token(
6666
grant_type="authorization_code",
6767
code=code,
6868
redirect_uri=self.settings.keycloak.redirect_uri,
6969
)
70-
return token
7170
except KeycloakOperationError as e:
7271
raise AuthorizationError("Failed to get token") from e
7372

74-
async def get_current_user(self, access_token: str, request: Request) -> User: # noqa: WPS231
73+
async def get_current_user(self, access_token: str | None, request: Request) -> User: # noqa: WPS231, WPS217
7574
if not access_token:
76-
log.debug("No access token found in session.")
77-
self.redirect_to_auth()
75+
log.debug("No access token found in session")
76+
await self.redirect_to_auth()
7877

7978
refresh_token = request.session.get("refresh_token")
8079
try:
8180
# if user is disabled or blocked in Keycloak after the token is issued, he will
8281
# remain authorized until the token expires (not more than 15 minutes in MTS SSO)
83-
token_info = self.keycloak_openid.decode_token(token=access_token)
82+
token_info = await self.keycloak_openid.a_decode_token(access_token)
8483
except (KeycloakOperationError, JWException) as e:
8584
log.info("Access token is invalid or expired: %s", e)
8685
token_info = None
@@ -89,20 +88,20 @@ async def get_current_user(self, access_token: str, request: Request) -> User:
8988
log.debug("Access token invalid. Attempting to refresh.")
9089

9190
try:
92-
new_tokens = self.keycloak_openid.refresh_token(refresh_token)
91+
new_tokens = await self.keycloak_openid.a_refresh_token(refresh_token)
9392

9493
new_access_token = new_tokens["access_token"]
9594
new_refresh_token = new_tokens["refresh_token"]
9695
request.session["access_token"] = new_access_token
9796
request.session["refresh_token"] = new_refresh_token
9897

99-
token_info = self.keycloak_openid.decode_token(
98+
token_info = await self.keycloak_openid.a_decode_token(
10099
token=new_access_token,
101100
)
102101
log.debug("Access token refreshed and decoded successfully.")
103102
except (KeycloakOperationError, JWException) as e:
104103
log.debug("Failed to refresh access token: %s", e)
105-
self.redirect_to_auth()
104+
await self.redirect_to_auth()
106105

107106
if not token_info:
108107
raise AuthorizationError("Invalid token payload")
@@ -129,8 +128,8 @@ async def get_current_user(self, access_token: str, request: Request) -> User:
129128
)
130129
return user
131130

132-
def redirect_to_auth(self) -> None:
133-
auth_url = self.keycloak_openid.auth_url(
131+
async def redirect_to_auth(self) -> NoReturn:
132+
auth_url = await self.keycloak_openid.a_auth_url(
134133
redirect_uri=self.settings.keycloak.redirect_uri,
135134
scope=self.settings.keycloak.scope,
136135
)
@@ -142,7 +141,7 @@ async def logout(self, user: User, refresh_token: str | None) -> None:
142141
return
143142

144143
try:
145-
self.keycloak_openid.logout(refresh_token)
144+
await self.keycloak_openid.a_logout(refresh_token)
146145
except KeycloakOperationError as err:
147146
msg = f"Can't logout user: {user.username}"
148147
log.debug("%s. Error: %s", msg, err)

tests/test_unit/test_auth/auth_fixtures/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from tests.test_unit.test_auth.auth_fixtures.keycloak_fixture import (
22
create_session_cookie,
3+
mock_keycloak_api,
34
mock_keycloak_logout,
45
mock_keycloak_realm,
56
mock_keycloak_token_refresh,
@@ -9,6 +10,7 @@
910

1011
__all__ = [
1112
"create_session_cookie",
13+
"mock_keycloak_api",
1214
"mock_keycloak_logout",
1315
"mock_keycloak_realm",
1416
"mock_keycloak_token_refresh",

tests/test_unit/test_auth/auth_fixtures/keycloak_fixture.py

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
import jwt
66
import pytest
7-
import responses
7+
import pytest_asyncio
8+
import respx
89
from cryptography.hazmat.primitives.asymmetric import rsa
910
from cryptography.hazmat.primitives.serialization import (
1011
Encoding,
@@ -83,18 +84,25 @@ def _create_session_cookie(user, expire_in_msec=60000) -> str:
8384
return _create_session_cookie
8485

8586

87+
@pytest_asyncio.fixture
88+
async def mock_keycloak_api(settings): # noqa: F811
89+
keycloak_settings = settings.auth.model_dump()["keycloak"]
90+
server_url = keycloak_settings["server_url"]
91+
92+
async with respx.mock(base_url=server_url, assert_all_called=False) as respx_mock:
93+
yield respx_mock
94+
95+
8696
@pytest.fixture
87-
def mock_keycloak_well_known(settings):
97+
def mock_keycloak_well_known(settings, mock_keycloak_api):
8898
keycloak_settings = settings.auth.model_dump()["keycloak"]
8999
server_url = keycloak_settings["server_url"]
90100
realm_name = keycloak_settings["client_id"]
91101
realm_url = f"{server_url}/realms/{realm_name}"
92102
well_known_url = f"{realm_url}/.well-known/openid-configuration"
93103
openid_url = f"{realm_url}/protocol/openid-connect"
94104

95-
responses.add(
96-
responses.GET,
97-
well_known_url,
105+
mock_keycloak_api.get(well_known_url).respond(
98106
json={
99107
"authorization_endpoint": f"{openid_url}/auth",
100108
"token_endpoint": f"{openid_url}/token",
@@ -103,35 +111,33 @@ def mock_keycloak_well_known(settings):
103111
"jwks_uri": f"{openid_url}/certs",
104112
"issuer": realm_url,
105113
},
106-
status=200,
114+
status_code=200,
107115
content_type="application/json",
108116
)
109117

110118

111119
@pytest.fixture
112-
def mock_keycloak_realm(settings, rsa_keys):
120+
def mock_keycloak_realm(settings, rsa_keys, mock_keycloak_api):
113121
keycloak_settings = settings.auth.model_dump()["keycloak"]
114122
server_url = keycloak_settings["server_url"]
115123
realm_name = keycloak_settings["client_id"]
116124
realm_url = f"{server_url}/realms/{realm_name}"
117125
public_pem_str = get_public_key_pem(rsa_keys["public_key"])
118126

119-
responses.add(
120-
responses.GET,
121-
realm_url,
127+
mock_keycloak_api.get(realm_url).respond(
122128
json={
123129
"realm": realm_name,
124130
"public_key": public_pem_str,
125131
"token-service": f"{realm_url}/protocol/openid-connect/token",
126132
"account-service": f"{realm_url}/account",
127133
},
128-
status=200,
134+
status_code=200,
129135
content_type="application/json",
130136
)
131137

132138

133139
@pytest.fixture
134-
def mock_keycloak_token_refresh(settings, rsa_keys):
140+
def mock_keycloak_token_refresh(settings, rsa_keys, mock_keycloak_api):
135141
keycloak_settings = settings.auth.model_dump()["keycloak"]
136142
server_url = keycloak_settings["server_url"]
137143
realm_name = keycloak_settings["client_id"]
@@ -154,30 +160,24 @@ def mock_keycloak_token_refresh(settings, rsa_keys):
154160
new_access_token = jwt.encode(payload, private_pem, algorithm="RS256")
155161
new_refresh_token = "mock_new_refresh_token"
156162

157-
responses.add(
158-
responses.POST,
159-
token_url,
163+
mock_keycloak_api.post(token_url).respond(
160164
json={
161165
"access_token": new_access_token,
162166
"refresh_token": new_refresh_token,
163167
"token_type": "bearer",
164168
"expires_in": expires_in,
165169
},
166-
status=200,
170+
status_code=200,
167171
content_type="application/json",
168172
)
169173

170174

171175
@pytest.fixture
172-
def mock_keycloak_logout(settings):
176+
def mock_keycloak_logout(settings, mock_keycloak_api):
173177
keycloak_settings = settings.auth.model_dump()["keycloak"]
174178
server_url = keycloak_settings["server_url"]
175179
realm_name = keycloak_settings["client_id"]
176180
realm_url = f"{server_url}/realms/{realm_name}"
177181
logout_url = f"{realm_url}/protocol/openid-connect/logout"
178182

179-
responses.add(
180-
responses.POST,
181-
logout_url,
182-
status=204,
183-
)
183+
mock_keycloak_api.post(logout_url).respond(status_code=204)

tests/test_unit/test_auth/test_auth_keycloak.py

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

33
import pytest
4-
import responses
54
from dirty_equals import IsStr
65
from httpx import AsyncClient
76

@@ -12,7 +11,6 @@
1211
pytestmark = [pytest.mark.asyncio, pytest.mark.server]
1312

1413

15-
@responses.activate
1614
@pytest.mark.parametrize(
1715
"settings",
1816
[
@@ -44,7 +42,6 @@ async def test_keycloak_get_user_unauthorized(
4442
}
4543

4644

47-
@responses.activate
4845
@pytest.mark.flaky
4946
@pytest.mark.parametrize(
5047
"settings",
@@ -81,7 +78,6 @@ async def test_keycloak_get_user_authorized(
8178
}
8279

8380

84-
@responses.activate
8581
@pytest.mark.parametrize(
8682
"settings",
8783
[
@@ -120,7 +116,6 @@ async def test_keycloak_get_user_expired_access_token(
120116
}
121117

122118

123-
@responses.activate
124119
@pytest.mark.parametrize(
125120
"settings",
126121
[
@@ -153,7 +148,6 @@ async def test_keycloak_get_user_inactive(
153148
}
154149

155150

156-
@responses.activate
157151
@pytest.mark.parametrize(
158152
"settings",
159153
[
@@ -184,7 +178,6 @@ async def test_keycloak_auth_callback(
184178
assert response.status_code == 204, response.text
185179

186180

187-
@responses.activate
188181
@pytest.mark.parametrize(
189182
"settings",
190183
[

0 commit comments

Comments
 (0)