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.
Files changed (33) hide show
  1. {usso-0.28.25/src/usso.egg-info → usso-0.28.27}/PKG-INFO +1 -1
  2. {usso-0.28.25 → usso-0.28.27}/pyproject.toml +9 -3
  3. {usso-0.28.25 → usso-0.28.27}/src/usso/api_key.py +2 -2
  4. {usso-0.28.25 → usso-0.28.27}/src/usso/authorization.py +41 -58
  5. {usso-0.28.25 → usso-0.28.27}/src/usso/client.py +2 -2
  6. {usso-0.28.25 → usso-0.28.27}/src/usso/config.py +6 -6
  7. {usso-0.28.25 → usso-0.28.27}/src/usso/exceptions.py +24 -10
  8. {usso-0.28.25 → usso-0.28.27}/src/usso/integrations/django/middleware.py +1 -1
  9. {usso-0.28.25 → usso-0.28.27}/src/usso/integrations/fastapi/dependency.py +1 -1
  10. {usso-0.28.25 → usso-0.28.27}/src/usso/integrations/fastapi/handler.py +9 -2
  11. {usso-0.28.25 → usso-0.28.27}/src/usso/session/async_session.py +7 -4
  12. {usso-0.28.25 → usso-0.28.27}/src/usso/session/base_session.py +3 -3
  13. {usso-0.28.25 → usso-0.28.27}/src/usso/session/session.py +7 -5
  14. {usso-0.28.25 → usso-0.28.27}/src/usso/user.py +6 -6
  15. {usso-0.28.25 → usso-0.28.27/src/usso.egg-info}/PKG-INFO +1 -1
  16. {usso-0.28.25 → usso-0.28.27}/tests/test_authorization.py +33 -34
  17. {usso-0.28.25 → usso-0.28.27}/tests/test_fastapi.py +13 -10
  18. {usso-0.28.25 → usso-0.28.27}/LICENSE.txt +0 -0
  19. {usso-0.28.25 → usso-0.28.27}/MANIFEST.in +0 -0
  20. {usso-0.28.25 → usso-0.28.27}/README.md +0 -0
  21. {usso-0.28.25 → usso-0.28.27}/pytest.ini +0 -0
  22. {usso-0.28.25 → usso-0.28.27}/setup.cfg +0 -0
  23. {usso-0.28.25 → usso-0.28.27}/src/usso/__init__.py +0 -0
  24. {usso-0.28.25 → usso-0.28.27}/src/usso/integrations/django/__init__.py +0 -0
  25. {usso-0.28.25 → usso-0.28.27}/src/usso/integrations/fastapi/__init__.py +0 -0
  26. {usso-0.28.25 → usso-0.28.27}/src/usso/session/__init__.py +0 -0
  27. {usso-0.28.25 → usso-0.28.27}/src/usso/utils/__init__.py +0 -0
  28. {usso-0.28.25 → usso-0.28.27}/src/usso/utils/string_utils.py +0 -0
  29. {usso-0.28.25 → usso-0.28.27}/src/usso.egg-info/SOURCES.txt +0 -0
  30. {usso-0.28.25 → usso-0.28.27}/src/usso.egg-info/dependency_links.txt +0 -0
  31. {usso-0.28.25 → usso-0.28.27}/src/usso.egg-info/entry_points.txt +0 -0
  32. {usso-0.28.25 → usso-0.28.27}/src/usso.egg-info/requires.txt +0 -0
  33. {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.25
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.25"
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 is_path_match(
54
- user_path: list[str] | str,
55
- requested_path: list[str] | str,
56
- strict: bool = False,
57
- ) -> bool:
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(user_path)}")
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 filter in filters:
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=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, status_code: int, error: str, message: dict | None = None
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
- self.message = message
26
+ msg: dict = {}
22
27
  if message is None:
23
- self.message = error_messages[error]
24
- super().__init__(message)
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
- message: dict = None,
32
- detail: str = None,
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.
@@ -16,7 +16,7 @@ class USSOAuthentication(UssoAuth):
16
16
  jwt_config: AvailableJwtConfigs | None = None,
17
17
  raise_exception: bool = True,
18
18
  expected_token_type: str = "access",
19
- ):
19
+ ) -> None:
20
20
  if jwt_config is None:
21
21
  jwt_config = AuthConfig()
22
22
 
@@ -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(request: Request, exc: USSOException):
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={"message": exc.message, "error": exc.error},
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
- httpx.AsyncClient.__init__(self)
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(self, method: str, url: str, **kwargs):
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(self, method: str, url: str, **kwargs):
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: Any | None = None,
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.25
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(user_path, requested_path, expected):
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
- dict(namespace="media"),
53
- dict(service="file-manager"),
54
- dict(uid="file123"),
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
- dict(namespace="finance"),
97
- dict(service="wallet"),
98
- dict(workspace_id="ws_7"),
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=dict(
113
- namespace="finance",
114
- service="wallet",
115
- workspace_id="ws_7",
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
- dict(namespace="finance"),
131
- dict(service="wallet"),
132
- dict(workspace_id="ws_7"),
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=dict(namespace="finance", user="user_1"),
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
- dict(namespace="finance"),
157
- dict(service="wallet"),
158
- dict(workspace_id="ws_7"),
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(app, test_valid_token: str, test_valid_payload: dict):
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