usso 0.28.25__tar.gz → 0.28.27__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {usso-0.28.25/src/usso.egg-info → usso-0.28.27}/PKG-INFO +1 -1
- {usso-0.28.25 → usso-0.28.27}/pyproject.toml +9 -3
- {usso-0.28.25 → usso-0.28.27}/src/usso/api_key.py +2 -2
- {usso-0.28.25 → usso-0.28.27}/src/usso/authorization.py +41 -58
- {usso-0.28.25 → usso-0.28.27}/src/usso/client.py +2 -2
- {usso-0.28.25 → usso-0.28.27}/src/usso/config.py +6 -6
- {usso-0.28.25 → usso-0.28.27}/src/usso/exceptions.py +24 -10
- {usso-0.28.25 → usso-0.28.27}/src/usso/integrations/django/middleware.py +1 -1
- {usso-0.28.25 → usso-0.28.27}/src/usso/integrations/fastapi/dependency.py +1 -1
- {usso-0.28.25 → usso-0.28.27}/src/usso/integrations/fastapi/handler.py +9 -2
- {usso-0.28.25 → usso-0.28.27}/src/usso/session/async_session.py +7 -4
- {usso-0.28.25 → usso-0.28.27}/src/usso/session/base_session.py +3 -3
- {usso-0.28.25 → usso-0.28.27}/src/usso/session/session.py +7 -5
- {usso-0.28.25 → usso-0.28.27}/src/usso/user.py +6 -6
- {usso-0.28.25 → usso-0.28.27/src/usso.egg-info}/PKG-INFO +1 -1
- {usso-0.28.25 → usso-0.28.27}/tests/test_authorization.py +33 -34
- {usso-0.28.25 → usso-0.28.27}/tests/test_fastapi.py +13 -10
- {usso-0.28.25 → usso-0.28.27}/LICENSE.txt +0 -0
- {usso-0.28.25 → usso-0.28.27}/MANIFEST.in +0 -0
- {usso-0.28.25 → usso-0.28.27}/README.md +0 -0
- {usso-0.28.25 → usso-0.28.27}/pytest.ini +0 -0
- {usso-0.28.25 → usso-0.28.27}/setup.cfg +0 -0
- {usso-0.28.25 → usso-0.28.27}/src/usso/__init__.py +0 -0
- {usso-0.28.25 → usso-0.28.27}/src/usso/integrations/django/__init__.py +0 -0
- {usso-0.28.25 → usso-0.28.27}/src/usso/integrations/fastapi/__init__.py +0 -0
- {usso-0.28.25 → usso-0.28.27}/src/usso/session/__init__.py +0 -0
- {usso-0.28.25 → usso-0.28.27}/src/usso/utils/__init__.py +0 -0
- {usso-0.28.25 → usso-0.28.27}/src/usso/utils/string_utils.py +0 -0
- {usso-0.28.25 → usso-0.28.27}/src/usso.egg-info/SOURCES.txt +0 -0
- {usso-0.28.25 → usso-0.28.27}/src/usso.egg-info/dependency_links.txt +0 -0
- {usso-0.28.25 → usso-0.28.27}/src/usso.egg-info/entry_points.txt +0 -0
- {usso-0.28.25 → usso-0.28.27}/src/usso.egg-info/requires.txt +0 -0
- {usso-0.28.25 → usso-0.28.27}/src/usso.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: usso
|
3
|
-
Version: 0.28.
|
3
|
+
Version: 0.28.27
|
4
4
|
Summary: A plug-and-play client for integrating universal single sign-on (SSO) with Python frameworks, enabling secure and seamless authentication across microservices.
|
5
5
|
Author-email: Mahdi Kiani <mahdikiany@gmail.com>
|
6
6
|
Maintainer-email: Mahdi Kiani <mahdikiany@gmail.com>
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "usso"
|
7
|
-
version = "0.28.
|
7
|
+
version = "0.28.27"
|
8
8
|
description = "A plug-and-play client for integrating universal single sign-on (SSO) with Python frameworks, enabling secure and seamless authentication across microservices."
|
9
9
|
readme = "README.md"
|
10
10
|
requires-python = ">=3.9"
|
@@ -45,7 +45,7 @@ all = [
|
|
45
45
|
"check-manifest",
|
46
46
|
"pytest",
|
47
47
|
"pytest_asyncio",
|
48
|
-
"coverage"
|
48
|
+
"coverage",
|
49
49
|
]
|
50
50
|
|
51
51
|
[project.urls]
|
@@ -69,7 +69,13 @@ unsafe-fixes = true
|
|
69
69
|
preview = true
|
70
70
|
|
71
71
|
[tool.ruff.lint]
|
72
|
-
select = ["E", "F", "W", "I", "UP", "B"]
|
72
|
+
select = ["E", "F", "W", "I", "UP", "B", "T", "C", "A", "ANN"]
|
73
73
|
|
74
74
|
[tool.ruff.format]
|
75
75
|
quote-style = "double"
|
76
|
+
|
77
|
+
[tool.mypy]
|
78
|
+
python_version = "3.13"
|
79
|
+
ignore_missing_imports = true
|
80
|
+
check_untyped_defs = false
|
81
|
+
disallow_untyped_defs = false
|
@@ -10,7 +10,7 @@ from .user import UserData
|
|
10
10
|
logger = logging.getLogger("usso")
|
11
11
|
|
12
12
|
|
13
|
-
def _handle_exception(error_type: str, **kwargs):
|
13
|
+
def _handle_exception(error_type: str, **kwargs: dict) -> None:
|
14
14
|
"""Handle API key related exceptions."""
|
15
15
|
if kwargs.get("raise_exception", True):
|
16
16
|
raise USSOException(
|
@@ -20,7 +20,7 @@ def _handle_exception(error_type: str, **kwargs):
|
|
20
20
|
|
21
21
|
|
22
22
|
@cachetools.func.ttl_cache(maxsize=128, ttl=60)
|
23
|
-
def fetch_api_key_data(api_key_verify_url: str, api_key: str):
|
23
|
+
def fetch_api_key_data(api_key_verify_url: str, api_key: str) -> UserData:
|
24
24
|
"""Fetch user data using an API key.
|
25
25
|
|
26
26
|
Args:
|
@@ -26,6 +26,9 @@ def parse_scope(scope: str) -> tuple[str, list[str], dict[str, str]]:
|
|
26
26
|
"*:*" ->
|
27
27
|
("*", ["*"], {})
|
28
28
|
|
29
|
+
"media//files" ->
|
30
|
+
("", ["media", "*", "files"], {})
|
31
|
+
|
29
32
|
Returns:
|
30
33
|
- action: str (could be empty string if no scheme present)
|
31
34
|
- path_parts: list[str]
|
@@ -47,78 +50,58 @@ def parse_scope(scope: str) -> tuple[str, list[str], dict[str, str]]:
|
|
47
50
|
query = scope[question_idx + 1 :]
|
48
51
|
filters = {k: v[0] for k, v in parse_qs(query).items()}
|
49
52
|
resource_path_parts = resource_path.split("/") if resource_path else ["*"]
|
50
|
-
return action, resource_path_parts, filters
|
53
|
+
return action, [rp or "*" for rp in resource_path_parts], filters
|
51
54
|
|
52
55
|
|
53
|
-
def
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
"""
|
59
|
-
Match resource paths from right to left, supporting wildcards (*).
|
60
|
-
|
61
|
-
Rules:
|
62
|
-
- The final resource name must match exactly or via fnmatch.
|
63
|
-
- Upper-level path parts are matched from right to left.
|
64
|
-
- Wildcards are allowed in any part.
|
65
|
-
|
66
|
-
Examples = [
|
67
|
-
("files", "files", True),
|
68
|
-
("file-manager/files", "files", True),
|
69
|
-
("media/file-manager/files", "files", True),
|
70
|
-
("media//files", "files", True),
|
71
|
-
("media//files", "file-manager/files", True),
|
72
|
-
("files", "file-manager/files", True),
|
73
|
-
("*/files", "file-manager/files", True),
|
74
|
-
("*//files", "file-manager/files", True),
|
75
|
-
("//files", "file-manager/files", True),
|
76
|
-
("//files", "media/file-manager/files", True),
|
77
|
-
("media//files", "media/file-manager/files", True),
|
78
|
-
("media/files/*", "media/files/transactions", True),
|
79
|
-
("*/*/transactions", "media/files/transactions", True),
|
80
|
-
("media/*/transactions", "media/images/transactions", True),
|
81
|
-
("media//files", "media/files", True), # attention
|
82
|
-
|
83
|
-
("files", "file", False),
|
84
|
-
("files", "files/transactions", False),
|
85
|
-
("files", "media/files/transactions", False),
|
86
|
-
("media/files", "media/files/transactions", False),
|
87
|
-
]
|
88
|
-
"""
|
89
|
-
if isinstance(user_path, str):
|
90
|
-
user_parts = user_path.split("/")
|
91
|
-
elif isinstance(user_path, list):
|
92
|
-
user_parts = user_path
|
56
|
+
def _normalize_path(path: list[str] | str) -> list[str]:
|
57
|
+
if isinstance(path, str):
|
58
|
+
return path.split("/")
|
59
|
+
elif isinstance(path, list):
|
60
|
+
return path
|
93
61
|
else:
|
94
|
-
raise ValueError(f"Invalid path type: {type(
|
62
|
+
raise ValueError(f"Invalid path type: {type(path)}")
|
95
63
|
|
96
|
-
if isinstance(requested_path, str):
|
97
|
-
req_parts = requested_path.split("/")
|
98
|
-
elif isinstance(requested_path, list):
|
99
|
-
req_parts = requested_path
|
100
|
-
else:
|
101
|
-
raise ValueError(f"Invalid path type: {type(requested_path)}")
|
102
64
|
|
65
|
+
def _match_path_parts(
|
66
|
+
user_parts: list[str], req_parts: list[str], strict: bool
|
67
|
+
) -> bool:
|
68
|
+
wildcard_found = False
|
103
69
|
# Match resource name (rightmost)
|
104
70
|
if not fnmatch.fnmatch(req_parts[-1], user_parts[-1]):
|
105
71
|
return False
|
106
|
-
|
72
|
+
if "*" in user_parts[-1]:
|
73
|
+
wildcard_found = True
|
107
74
|
# Match rest of the path from right to left
|
108
75
|
user_path_parts = user_parts[:-1]
|
109
76
|
req_path_parts = req_parts[:-1]
|
110
|
-
|
111
77
|
for u, r in zip(
|
112
|
-
reversed(user_path_parts),
|
113
|
-
reversed(req_path_parts),
|
114
|
-
strict=strict,
|
78
|
+
reversed(user_path_parts), reversed(req_path_parts), strict=strict
|
115
79
|
):
|
116
80
|
if r and u and r != "*" and not fnmatch.fnmatch(r, u):
|
117
81
|
return False
|
118
|
-
|
82
|
+
if "*" in u:
|
83
|
+
wildcard_found = True
|
84
|
+
offset = len(user_path_parts) - len(req_path_parts)
|
85
|
+
if offset > 0 and wildcard_found:
|
86
|
+
for u in user_path_parts[:offset]:
|
87
|
+
if u != "*":
|
88
|
+
return False
|
119
89
|
return True
|
120
90
|
|
121
91
|
|
92
|
+
def is_path_match(
|
93
|
+
user_path: list[str] | str,
|
94
|
+
requested_path: list[str] | str,
|
95
|
+
strict: bool = False,
|
96
|
+
) -> bool:
|
97
|
+
"""
|
98
|
+
Match resource paths from right to left, supporting wildcards (*).
|
99
|
+
"""
|
100
|
+
user_parts = _normalize_path(user_path)
|
101
|
+
req_parts = _normalize_path(requested_path)
|
102
|
+
return _match_path_parts(user_parts, req_parts, strict)
|
103
|
+
|
104
|
+
|
122
105
|
def is_filter_match(user_filters: dict, requested_filters: dict) -> bool:
|
123
106
|
"""All user filters must match requested filters."""
|
124
107
|
for k, v in user_filters.items():
|
@@ -209,7 +192,7 @@ def is_authorized(
|
|
209
192
|
if not is_path_match(user_path, requested_path, strict=strict):
|
210
193
|
return False
|
211
194
|
|
212
|
-
if not is_filter_match(user_filters, reuested_filter):
|
195
|
+
if not is_filter_match(user_filters, reuested_filter or {}):
|
213
196
|
return False
|
214
197
|
|
215
198
|
if requested_action:
|
@@ -243,15 +226,15 @@ def check_access(
|
|
243
226
|
if isinstance(filters, dict):
|
244
227
|
filters = [{k: v} for k, v in filters.items()]
|
245
228
|
elif filters is None:
|
246
|
-
filters = [
|
229
|
+
filters = [{}]
|
247
230
|
|
248
231
|
for scope in user_scopes:
|
249
|
-
for
|
232
|
+
for filt in filters:
|
250
233
|
if is_authorized(
|
251
234
|
user_scope=scope,
|
252
235
|
requested_path=resource_path,
|
253
236
|
requested_action=action,
|
254
|
-
reuested_filter=
|
237
|
+
reuested_filter=filt,
|
255
238
|
strict=strict,
|
256
239
|
):
|
257
240
|
return True
|
@@ -22,7 +22,7 @@ class UssoAuth:
|
|
22
22
|
self,
|
23
23
|
*,
|
24
24
|
jwt_config: AvailableJwtConfigs | None = None,
|
25
|
-
):
|
25
|
+
) -> None:
|
26
26
|
"""Initialize the USSO authentication client.
|
27
27
|
|
28
28
|
Args:
|
@@ -38,7 +38,7 @@ class UssoAuth:
|
|
38
38
|
*,
|
39
39
|
expected_token_type: str | None = "access",
|
40
40
|
raise_exception: bool = True,
|
41
|
-
**kwargs,
|
41
|
+
**kwargs: dict,
|
42
42
|
) -> UserData | None:
|
43
43
|
"""Get user data from a JWT token.
|
44
44
|
|
@@ -13,7 +13,7 @@ class HeaderConfig(BaseModel):
|
|
13
13
|
name: str = "usso_access_token"
|
14
14
|
|
15
15
|
@model_validator(mode="before")
|
16
|
-
def validate_header(cls, data: dict):
|
16
|
+
def validate_header(cls, data: dict) -> dict:
|
17
17
|
if data.get("type") == "Authorization" and not data.get("name"):
|
18
18
|
data["name"] = "Bearer"
|
19
19
|
elif data.get("type") == "Cookie":
|
@@ -22,10 +22,10 @@ class HeaderConfig(BaseModel):
|
|
22
22
|
data["name"] = data.get("name", "x-usso-access-token")
|
23
23
|
return data
|
24
24
|
|
25
|
-
def __hash__(self):
|
25
|
+
def __hash__(self) -> int:
|
26
26
|
return hash(self.model_dump_json())
|
27
27
|
|
28
|
-
def get_key(self, request) -> str | None:
|
28
|
+
def get_key(self, request: object) -> str | None: # type: ignore
|
29
29
|
headers: dict[str, Any] = getattr(request, "headers", {})
|
30
30
|
cookies: dict[str, str] = getattr(
|
31
31
|
request, "cookies", headers.get("Cookie", {})
|
@@ -54,18 +54,18 @@ class AuthConfig(usso_jwt.config.JWTConfig):
|
|
54
54
|
jwt_header: HeaderConfig | None = HeaderConfig()
|
55
55
|
static_api_keys: list[str] | None = None
|
56
56
|
|
57
|
-
def get_api_key(self, request) -> str | None:
|
57
|
+
def get_api_key(self, request: object) -> str | None:
|
58
58
|
if self.api_key_header:
|
59
59
|
return self.api_key_header.get_key(request)
|
60
60
|
return None
|
61
61
|
|
62
|
-
def get_jwt(self, request) -> str | None:
|
62
|
+
def get_jwt(self, request: object) -> str | None:
|
63
63
|
if self.jwt_header:
|
64
64
|
return self.jwt_header.get_key(request)
|
65
65
|
return None
|
66
66
|
|
67
67
|
def verify_token(
|
68
|
-
self, token: str, *, raise_exception: bool = True, **kwargs
|
68
|
+
self, token: str, *, raise_exception: bool = True, **kwargs: dict
|
69
69
|
) -> bool:
|
70
70
|
from usso_jwt import exceptions as jwt_exceptions
|
71
71
|
from usso_jwt import schemas
|
@@ -14,30 +14,44 @@ error_messages = {
|
|
14
14
|
|
15
15
|
class USSOException(Exception):
|
16
16
|
def __init__(
|
17
|
-
self,
|
18
|
-
|
17
|
+
self,
|
18
|
+
status_code: int,
|
19
|
+
error: str,
|
20
|
+
detail: str | None = None,
|
21
|
+
message: dict | None = None,
|
22
|
+
**kwargs: dict,
|
23
|
+
) -> None:
|
19
24
|
self.status_code = status_code
|
20
25
|
self.error = error
|
21
|
-
|
26
|
+
msg: dict = {}
|
22
27
|
if message is None:
|
23
|
-
|
24
|
-
|
28
|
+
if detail:
|
29
|
+
msg["en"] = detail
|
30
|
+
else:
|
31
|
+
msg["en"] = error_messages.get(error, error)
|
32
|
+
else:
|
33
|
+
msg = message
|
34
|
+
|
35
|
+
self.message = msg
|
36
|
+
self.detail = detail or str(self.message)
|
37
|
+
self.data = kwargs
|
38
|
+
super().__init__(detail)
|
25
39
|
|
26
40
|
|
27
41
|
class PermissionDenied(USSOException):
|
28
42
|
def __init__(
|
29
43
|
self,
|
30
44
|
error: str = "permission_denied",
|
31
|
-
|
32
|
-
|
33
|
-
**kwargs,
|
34
|
-
):
|
45
|
+
detail: str | None = None,
|
46
|
+
message: dict | None = None,
|
47
|
+
**kwargs: dict,
|
48
|
+
) -> None:
|
35
49
|
super().__init__(
|
36
50
|
403, error=error, message=message, detail=detail, **kwargs
|
37
51
|
)
|
38
52
|
|
39
53
|
|
40
|
-
def _handle_exception(error_type: str, **kwargs):
|
54
|
+
def _handle_exception(error_type: str, **kwargs: dict) -> None:
|
41
55
|
"""Handle JWT-related exceptions."""
|
42
56
|
if kwargs.get("raise_exception", True):
|
43
57
|
raise USSOException(
|
@@ -17,7 +17,7 @@ class USSOAuthenticationMiddleware(MiddlewareMixin):
|
|
17
17
|
def jwt_config(self) -> AuthConfig:
|
18
18
|
return settings.USSO_JWT_CONFIG
|
19
19
|
|
20
|
-
def process_request(self, request: HttpRequest):
|
20
|
+
def process_request(self, request: HttpRequest) -> None:
|
21
21
|
"""
|
22
22
|
Middleware to authenticate users by JWT token and create or
|
23
23
|
return a user in the database.
|
@@ -4,10 +4,17 @@ from fastapi.responses import JSONResponse
|
|
4
4
|
from ...exceptions import USSOException
|
5
5
|
|
6
6
|
|
7
|
-
async def usso_exception_handler(
|
7
|
+
async def usso_exception_handler(
|
8
|
+
request: Request, exc: USSOException
|
9
|
+
) -> JSONResponse:
|
8
10
|
return JSONResponse(
|
9
11
|
status_code=exc.status_code,
|
10
|
-
content={
|
12
|
+
content={
|
13
|
+
"message": exc.message,
|
14
|
+
"error": exc.error,
|
15
|
+
"detail": exc.detail,
|
16
|
+
**exc.data,
|
17
|
+
},
|
11
18
|
)
|
12
19
|
|
13
20
|
|
@@ -17,8 +17,9 @@ class AsyncUssoSession(httpx.AsyncClient, BaseUssoSession):
|
|
17
17
|
usso_api_key: str | None = os.getenv("USSO_ADMIN_API_KEY"),
|
18
18
|
user_id: str | None = None,
|
19
19
|
client: "AsyncUssoSession" = None,
|
20
|
-
|
21
|
-
|
20
|
+
**kwargs: dict,
|
21
|
+
) -> None:
|
22
|
+
httpx.AsyncClient.__init__(self, **kwargs)
|
22
23
|
BaseUssoSession.__init__(
|
23
24
|
self,
|
24
25
|
usso_base_url=usso_base_url,
|
@@ -100,7 +101,7 @@ class AsyncUssoSession(httpx.AsyncClient, BaseUssoSession):
|
|
100
101
|
)
|
101
102
|
return self._handle_refresh_response(response)
|
102
103
|
|
103
|
-
async def get_session(self):
|
104
|
+
async def get_session(self) -> "AsyncUssoSession":
|
104
105
|
if hasattr(self, "api_key") and self.api_key:
|
105
106
|
return self
|
106
107
|
|
@@ -108,6 +109,8 @@ class AsyncUssoSession(httpx.AsyncClient, BaseUssoSession):
|
|
108
109
|
await self._refresh()
|
109
110
|
return self
|
110
111
|
|
111
|
-
async def _request(
|
112
|
+
async def _request(
|
113
|
+
self, method: str, url: str, **kwargs: dict
|
114
|
+
) -> httpx.Response:
|
112
115
|
session = await self.get_session()
|
113
116
|
return await session.request(method, url, **kwargs)
|
@@ -14,7 +14,7 @@ class BaseUssoSession:
|
|
14
14
|
app_secret: str | None = None,
|
15
15
|
usso_url: str = "https://sso.usso.io",
|
16
16
|
client: Optional["BaseUssoSession"] = None,
|
17
|
-
):
|
17
|
+
) -> None:
|
18
18
|
if client:
|
19
19
|
self.copy_attributes_from(client)
|
20
20
|
return
|
@@ -54,7 +54,7 @@ class BaseUssoSession:
|
|
54
54
|
self.access_token = None
|
55
55
|
self.headers = getattr(self, "headers", {})
|
56
56
|
|
57
|
-
def copy_attributes_from(self, client: "BaseUssoSession"):
|
57
|
+
def copy_attributes_from(self, client: "BaseUssoSession") -> None:
|
58
58
|
self.usso_url = client.usso_url
|
59
59
|
self.usso_refresh_url = client.usso_refresh_url
|
60
60
|
self._refresh_token = client._refresh_token
|
@@ -64,7 +64,7 @@ class BaseUssoSession:
|
|
64
64
|
self.headers = client.headers.copy()
|
65
65
|
|
66
66
|
@property
|
67
|
-
def refresh_token(self):
|
67
|
+
def refresh_token(self) -> JWT:
|
68
68
|
if (
|
69
69
|
self._refresh_token
|
70
70
|
and self._refresh_token.verify( # noqa: W503
|
@@ -14,8 +14,8 @@ class UssoSession(httpx.Client, BaseUssoSession):
|
|
14
14
|
refresh_token: str | None = os.getenv("USSO_REFRESH_TOKEN"),
|
15
15
|
usso_url: str | None = os.getenv("USSO_URL"),
|
16
16
|
client: "UssoSession" = None,
|
17
|
-
**kwargs,
|
18
|
-
):
|
17
|
+
**kwargs: dict,
|
18
|
+
) -> None:
|
19
19
|
httpx.Client.__init__(self, **kwargs)
|
20
20
|
|
21
21
|
BaseUssoSession.__init__(
|
@@ -28,7 +28,7 @@ class UssoSession(httpx.Client, BaseUssoSession):
|
|
28
28
|
if not self.api_key:
|
29
29
|
self._refresh()
|
30
30
|
|
31
|
-
def _refresh(self):
|
31
|
+
def _refresh(self) -> dict:
|
32
32
|
assert self.refresh_token, "refresh_token is required"
|
33
33
|
|
34
34
|
response = httpx.post(
|
@@ -43,7 +43,7 @@ class UssoSession(httpx.Client, BaseUssoSession):
|
|
43
43
|
self.headers.update({"Authorization": f"Bearer {self.access_token}"})
|
44
44
|
return response.json()
|
45
45
|
|
46
|
-
def get_session(self):
|
46
|
+
def get_session(self) -> "UssoSession":
|
47
47
|
if self.api_key:
|
48
48
|
return self
|
49
49
|
|
@@ -51,6 +51,8 @@ class UssoSession(httpx.Client, BaseUssoSession):
|
|
51
51
|
self._refresh()
|
52
52
|
return self
|
53
53
|
|
54
|
-
def _request(
|
54
|
+
def _request(
|
55
|
+
self, method: str, url: str, **kwargs: dict
|
56
|
+
) -> httpx.Response:
|
55
57
|
self.get_session()
|
56
58
|
return super().request(self, method, url, **kwargs)
|
@@ -55,8 +55,8 @@ class UserData(BaseModel):
|
|
55
55
|
acr: str | None = None,
|
56
56
|
amr: list[str] | None = None,
|
57
57
|
signing_level: str | None = None,
|
58
|
-
**kwargs,
|
59
|
-
):
|
58
|
+
**kwargs: dict,
|
59
|
+
) -> None:
|
60
60
|
super().__init__(
|
61
61
|
jti=jti,
|
62
62
|
token_type=token_type,
|
@@ -109,9 +109,9 @@ class UserData(BaseModel):
|
|
109
109
|
self,
|
110
110
|
*,
|
111
111
|
mode: Literal["json", "python"] | str = "python",
|
112
|
-
include=None,
|
113
|
-
exclude=None,
|
114
|
-
context:
|
112
|
+
include: set[str] | list[str] | None = None,
|
113
|
+
exclude: set[str] | list[str] | None = None,
|
114
|
+
context: object | None = None,
|
115
115
|
by_alias: bool | None = None,
|
116
116
|
exclude_unset: bool = False,
|
117
117
|
exclude_defaults: bool = False,
|
@@ -120,7 +120,7 @@ class UserData(BaseModel):
|
|
120
120
|
warnings: bool | Literal["none", "warn", "error"] = True,
|
121
121
|
fallback: Callable[[Any], Any] | None = None,
|
122
122
|
serialize_as_any: bool = False,
|
123
|
-
):
|
123
|
+
) -> dict:
|
124
124
|
return super().model_dump(
|
125
125
|
mode=mode,
|
126
126
|
include=include,
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: usso
|
3
|
-
Version: 0.28.
|
3
|
+
Version: 0.28.27
|
4
4
|
Summary: A plug-and-play client for integrating universal single sign-on (SSO) with Python frameworks, enabling secure and seamless authentication across microservices.
|
5
5
|
Author-email: Mahdi Kiani <mahdikiany@gmail.com>
|
6
6
|
Maintainer-email: Mahdi Kiani <mahdikiany@gmail.com>
|
@@ -22,11 +22,6 @@ from src.usso.authorization import (
|
|
22
22
|
("//files", "file-manager/files", True),
|
23
23
|
("//files", "media/file-manager/files", True),
|
24
24
|
("media//files", "media/file-manager/files", True),
|
25
|
-
(
|
26
|
-
"media//files",
|
27
|
-
"media/files",
|
28
|
-
True,
|
29
|
-
), # !! atention it matches because the middle part is empty
|
30
25
|
("media/files/*", "media/files/transactions", True),
|
31
26
|
("*/*/transactions", "media/files/transactions", True),
|
32
27
|
("media/*/transactions", "media/images/transactions", True),
|
@@ -34,14 +29,18 @@ from src.usso.authorization import (
|
|
34
29
|
("files", "files/transactions", False),
|
35
30
|
("files", "media/files/transactions", False),
|
36
31
|
("media/files", "media/files/transactions", False),
|
32
|
+
("finance/*/*", "wallet", False),
|
33
|
+
("media//files", "media/files", True),
|
37
34
|
],
|
38
35
|
)
|
39
|
-
def test_path_match(
|
36
|
+
def test_path_match(
|
37
|
+
user_path: str, requested_path: str, expected: bool
|
38
|
+
) -> None:
|
40
39
|
assert is_path_match(user_path, requested_path, strict=False) == expected
|
41
40
|
|
42
41
|
|
43
42
|
# Define pytest tests
|
44
|
-
def test_exact_match_id():
|
43
|
+
def test_exact_match_id() -> None:
|
45
44
|
scopes = ["read:media/file-manager/files?uid=file123"]
|
46
45
|
assert (
|
47
46
|
check_access(
|
@@ -49,9 +48,9 @@ def test_exact_match_id():
|
|
49
48
|
"files",
|
50
49
|
action="read",
|
51
50
|
filters=[
|
52
|
-
|
53
|
-
|
54
|
-
|
51
|
+
{"namespace": "media"},
|
52
|
+
{"service": "file-manager"},
|
53
|
+
{"uid": "file123"},
|
55
54
|
],
|
56
55
|
)
|
57
56
|
is True
|
@@ -59,7 +58,7 @@ def test_exact_match_id():
|
|
59
58
|
|
60
59
|
|
61
60
|
# Define pytest tests
|
62
|
-
def test_wildcard():
|
61
|
+
def test_wildcard() -> None:
|
63
62
|
scopes = [
|
64
63
|
"update:media/files/transactions?user_id=abc",
|
65
64
|
"read:media/files/*",
|
@@ -85,7 +84,7 @@ def test_wildcard():
|
|
85
84
|
)
|
86
85
|
|
87
86
|
|
88
|
-
def test_insufficient_privilege():
|
87
|
+
def test_insufficient_privilege() -> None:
|
89
88
|
scopes = ["read:media/files/file:uid:file123"]
|
90
89
|
assert (
|
91
90
|
check_access(
|
@@ -93,33 +92,33 @@ def test_insufficient_privilege():
|
|
93
92
|
"file",
|
94
93
|
"update",
|
95
94
|
filters=[
|
96
|
-
|
97
|
-
|
98
|
-
|
95
|
+
{"namespace": "finance"},
|
96
|
+
{"service": "wallet"},
|
97
|
+
{"workspace_id": "ws_7"},
|
99
98
|
],
|
100
99
|
)
|
101
100
|
is False
|
102
101
|
)
|
103
102
|
|
104
103
|
|
105
|
-
def test_wildcard_match():
|
104
|
+
def test_wildcard_match() -> None:
|
106
105
|
scopes = ["manage:media/files/file?*"]
|
107
106
|
assert (
|
108
107
|
check_access(
|
109
108
|
scopes,
|
110
109
|
"file",
|
111
110
|
"update",
|
112
|
-
filters=
|
113
|
-
namespace
|
114
|
-
service
|
115
|
-
workspace_id
|
116
|
-
|
111
|
+
filters={
|
112
|
+
"namespace": "finance",
|
113
|
+
"service": "wallet",
|
114
|
+
"workspace_id": "ws_7",
|
115
|
+
},
|
117
116
|
)
|
118
117
|
is True
|
119
118
|
)
|
120
119
|
|
121
120
|
|
122
|
-
def test_match_by_user_id():
|
121
|
+
def test_match_by_user_id() -> None:
|
123
122
|
scopes = ["manage:finance/wallet/transaction?user=user_1"]
|
124
123
|
assert (
|
125
124
|
check_access(
|
@@ -127,9 +126,9 @@ def test_match_by_user_id():
|
|
127
126
|
"transaction",
|
128
127
|
"update",
|
129
128
|
filters=[
|
130
|
-
|
131
|
-
|
132
|
-
|
129
|
+
{"namespace": "finance"},
|
130
|
+
{"service": "wallet"},
|
131
|
+
{"workspace_id": "ws_7"},
|
133
132
|
],
|
134
133
|
)
|
135
134
|
is False
|
@@ -139,13 +138,13 @@ def test_match_by_user_id():
|
|
139
138
|
scopes,
|
140
139
|
"transaction",
|
141
140
|
"update",
|
142
|
-
filters=
|
141
|
+
filters={"namespace": "finance", "user": "user_1"},
|
143
142
|
)
|
144
143
|
is True
|
145
144
|
)
|
146
145
|
|
147
146
|
|
148
|
-
def test_match_by_workspace_id():
|
147
|
+
def test_match_by_workspace_id() -> None:
|
149
148
|
scopes = ["delete:finance/wallet/transaction?workspace_id=ws_7"]
|
150
149
|
assert (
|
151
150
|
check_access(
|
@@ -153,31 +152,31 @@ def test_match_by_workspace_id():
|
|
153
152
|
"transaction",
|
154
153
|
"delete",
|
155
154
|
filters=[
|
156
|
-
|
157
|
-
|
158
|
-
|
155
|
+
{"namespace": "finance"},
|
156
|
+
{"service": "wallet"},
|
157
|
+
{"workspace_id": "ws_7"},
|
159
158
|
],
|
160
159
|
)
|
161
160
|
is True
|
162
161
|
)
|
163
162
|
|
164
163
|
|
165
|
-
def test_minimal_params_success():
|
164
|
+
def test_minimal_params_success() -> None:
|
166
165
|
scopes = ["create:file?*"]
|
167
166
|
assert check_access(scopes, "file", "create") is True
|
168
167
|
|
169
168
|
|
170
|
-
def test_minimal_params_fail():
|
169
|
+
def test_minimal_params_fail() -> None:
|
171
170
|
scopes = ["read:file?*"]
|
172
171
|
assert check_access(scopes, "file", "create") is False
|
173
172
|
|
174
173
|
|
175
|
-
def test_minimal_params_read_create_fail():
|
174
|
+
def test_minimal_params_read_create_fail() -> None:
|
176
175
|
scopes = ["file"]
|
177
176
|
assert check_access(scopes, "file", "create") is False
|
178
177
|
|
179
178
|
|
180
|
-
def test_scope_subset():
|
179
|
+
def test_scope_subset() -> None:
|
181
180
|
assert is_subset_scope(
|
182
181
|
subset_scope="read:media/files?user_id=123",
|
183
182
|
super_scope="read:media/files",
|
@@ -5,7 +5,7 @@ from collections.abc import AsyncGenerator
|
|
5
5
|
import httpx
|
6
6
|
import pytest
|
7
7
|
import pytest_asyncio
|
8
|
-
from fastapi import Depends, WebSocket
|
8
|
+
from fastapi import Depends, FastAPI, WebSocket
|
9
9
|
from starlette.testclient import TestClient
|
10
10
|
from usso_jwt.algorithms import AbstractKey
|
11
11
|
|
@@ -18,7 +18,7 @@ from src.usso.integrations.fastapi import (
|
|
18
18
|
|
19
19
|
|
20
20
|
@pytest.fixture(scope="session")
|
21
|
-
def app(test_key: AbstractKey):
|
21
|
+
def app(test_key: AbstractKey) -> FastAPI:
|
22
22
|
import fastapi
|
23
23
|
|
24
24
|
os.environ["JWT_CONFIG"] = json.dumps({
|
@@ -36,14 +36,14 @@ def app(test_key: AbstractKey):
|
|
36
36
|
@app.get("/user")
|
37
37
|
async def get_user(
|
38
38
|
user: UserData = Depends(usso.usso_access_security), # noqa: B008
|
39
|
-
):
|
39
|
+
) -> dict:
|
40
40
|
return user.model_dump()
|
41
41
|
|
42
42
|
@app.websocket("/ws")
|
43
43
|
async def websocket_endpoint(
|
44
44
|
websocket: WebSocket,
|
45
45
|
user: UserData = Depends(usso.jwt_access_security_ws), # noqa: B008
|
46
|
-
):
|
46
|
+
) -> None:
|
47
47
|
await websocket.accept()
|
48
48
|
await websocket.send_json({"msg": user.model_dump()})
|
49
49
|
# await websocket.send_json({"msg": "Hello WebSocket"})
|
@@ -53,7 +53,7 @@ def app(test_key: AbstractKey):
|
|
53
53
|
|
54
54
|
|
55
55
|
@pytest_asyncio.fixture(scope="session")
|
56
|
-
async def client(app) -> AsyncGenerator[httpx.AsyncClient]:
|
56
|
+
async def client(app: FastAPI) -> AsyncGenerator[httpx.AsyncClient]:
|
57
57
|
"""Fixture to provide an AsyncClient for FastAPI app."""
|
58
58
|
|
59
59
|
async with httpx.AsyncClient(
|
@@ -64,14 +64,13 @@ async def client(app) -> AsyncGenerator[httpx.AsyncClient]:
|
|
64
64
|
|
65
65
|
|
66
66
|
@pytest.mark.asyncio
|
67
|
-
async def test_get_user_no_token(client: httpx.AsyncClient):
|
67
|
+
async def test_get_user_no_token(client: httpx.AsyncClient) -> None:
|
68
68
|
response = await client.get("/user")
|
69
|
-
print(response.json())
|
70
69
|
assert response.status_code == 401
|
71
70
|
|
72
71
|
|
73
72
|
@pytest.mark.asyncio
|
74
|
-
async def test_get_user_with_invalid_token(client: httpx.AsyncClient):
|
73
|
+
async def test_get_user_with_invalid_token(client: httpx.AsyncClient) -> None:
|
75
74
|
response = await client.get(
|
76
75
|
"/user",
|
77
76
|
headers={"Authorization": "Bearer test"},
|
@@ -84,7 +83,7 @@ async def test_get_user_with_token(
|
|
84
83
|
client: httpx.AsyncClient,
|
85
84
|
test_valid_token: str,
|
86
85
|
test_valid_payload: dict,
|
87
|
-
):
|
86
|
+
) -> None:
|
88
87
|
response = await client.get(
|
89
88
|
"/user",
|
90
89
|
headers={"Authorization": f"Bearer {test_valid_token}"},
|
@@ -93,7 +92,11 @@ async def test_get_user_with_token(
|
|
93
92
|
assert response.json().get("claims") == test_valid_payload
|
94
93
|
|
95
94
|
|
96
|
-
def test_websocket(
|
95
|
+
def test_websocket(
|
96
|
+
app: FastAPI,
|
97
|
+
test_valid_token: str,
|
98
|
+
test_valid_payload: dict,
|
99
|
+
) -> None:
|
97
100
|
client = TestClient(app)
|
98
101
|
with client.websocket_connect(
|
99
102
|
"/ws", headers={"Authorization": f"Bearer {test_valid_token}"}
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|