Skip to content

Commit 7aa47ff

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

File tree

8 files changed

+75
-57
lines changed

8 files changed

+75
-57
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: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
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
1010
from itsdangerous import TimestampSigner
1111
from jose import jwt
1212

13+
from syncmaster.server.settings.auth.keycloak import KeycloakSettings
14+
1315

1416
@pytest.fixture(scope="session")
1517
def rsa_keys():
@@ -80,14 +82,15 @@ def _create_session_cookie(user, expire_in_msec=5000) -> str:
8082

8183

8284
@pytest.fixture
85+
@respx.mock
8386
def mock_keycloak_well_known(settings):
84-
server_url = settings.auth.dict()["keycloak"]["server_url"]
85-
realm_name = settings.auth.dict()["keycloak"]["client_id"]
87+
keycloak_settings = KeycloakSettings.model_validate(settings.auth.dict()["keycloak"])
88+
server_url = keycloak_settings.server_url
89+
realm_name = keycloak_settings.realm_name
8690
well_known_url = f"{server_url}/realms/{realm_name}/.well-known/openid-configuration"
8791

88-
responses.add(
89-
responses.GET,
90-
well_known_url,
92+
respx.get(well_known_url).respond(
93+
status_code=200,
9194
json={
9295
"authorization_endpoint": f"{server_url}/realms/{realm_name}/protocol/openid-connect/auth",
9396
"token_endpoint": f"{server_url}/realms/{realm_name}/protocol/openid-connect/token",
@@ -96,36 +99,37 @@ def mock_keycloak_well_known(settings):
9699
"jwks_uri": f"{server_url}/realms/{realm_name}/protocol/openid-connect/certs",
97100
"issuer": f"{server_url}/realms/{realm_name}",
98101
},
99-
status=200,
100102
content_type="application/json",
101103
)
102104

103105

104106
@pytest.fixture
107+
@respx.mock
105108
def mock_keycloak_realm(settings, rsa_keys):
106-
server_url = settings.auth.dict()["keycloak"]["server_url"]
107-
realm_name = settings.auth.dict()["keycloak"]["client_id"]
109+
keycloak_settings = KeycloakSettings.model_validate(settings.auth.dict()["keycloak"])
110+
server_url = keycloak_settings.server_url
111+
realm_name = keycloak_settings.realm_name
108112
realm_url = f"{server_url}/realms/{realm_name}"
109113
public_pem_str = get_public_key_pem(rsa_keys["public_key"])
110114

111-
responses.add(
112-
responses.GET,
113-
realm_url,
115+
respx.get(realm_url).respond(
116+
status_code=200,
114117
json={
115118
"realm": realm_name,
116119
"public_key": public_pem_str,
117120
"token-service": f"{server_url}/realms/{realm_name}/protocol/openid-connect/token",
118121
"account-service": f"{server_url}/realms/{realm_name}/account",
119122
},
120-
status=200,
121123
content_type="application/json",
122124
)
123125

124126

125127
@pytest.fixture
128+
@respx.mock
126129
def mock_keycloak_token_refresh(settings, rsa_keys):
127-
server_url = settings.auth.dict()["keycloak"]["server_url"]
128-
realm_name = settings.auth.dict()["keycloak"]["client_id"]
130+
keycloak_settings = KeycloakSettings.model_validate(settings.auth.dict()["keycloak"])
131+
server_url = keycloak_settings.server_url
132+
realm_name = keycloak_settings.realm_name
129133
token_url = f"{server_url}/realms/{realm_name}/protocol/openid-connect/token"
130134

131135
# generate new access and refresh tokens
@@ -144,15 +148,13 @@ def mock_keycloak_token_refresh(settings, rsa_keys):
144148
new_access_token = jwt.encode(payload, private_pem, algorithm="RS256")
145149
new_refresh_token = "mock_new_refresh_token"
146150

147-
responses.add(
148-
responses.POST,
149-
token_url,
151+
respx.post(token_url).respond(
152+
status_code=200,
150153
json={
151154
"access_token": new_access_token,
152155
"refresh_token": new_refresh_token,
153156
"token_type": "bearer",
154157
"expires_in": expires_in,
155158
},
156-
status=200,
157159
content_type="application/json",
158160
)

tests/test_unit/test_auth/test_auth_keycloak.py

Lines changed: 0 additions & 6 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 httpx import AsyncClient
65

76
from syncmaster.server.settings import ServerAppSettings as Settings
@@ -11,7 +10,6 @@
1110
pytestmark = [pytest.mark.asyncio, pytest.mark.server]
1211

1312

14-
@responses.activate
1513
@pytest.mark.parametrize(
1614
"settings",
1715
[
@@ -33,7 +31,6 @@ async def test_get_keycloak_user_unauthorized(client: AsyncClient, mock_keycloak
3331
)
3432

3533

36-
@responses.activate
3734
@pytest.mark.parametrize(
3835
"settings",
3936
[
@@ -71,7 +68,6 @@ async def test_get_keycloak_user_authorized(
7168
}
7269

7370

74-
@responses.activate
7571
@pytest.mark.parametrize(
7672
"settings",
7773
[
@@ -116,7 +112,6 @@ async def test_get_keycloak_user_expired_access_token(
116112
}
117113

118114

119-
@responses.activate
120115
@pytest.mark.parametrize(
121116
"settings",
122117
[
@@ -155,7 +150,6 @@ async def test_get_keycloak_deleted_user(
155150
}
156151

157152

158-
@responses.activate
159153
@pytest.mark.parametrize(
160154
"settings",
161155
[

0 commit comments

Comments
 (0)