usso 0.28.26__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.26/src/usso.egg-info → usso-0.28.27}/PKG-INFO +1 -1
  2. {usso-0.28.26 → usso-0.28.27}/pyproject.toml +9 -3
  3. {usso-0.28.26 → usso-0.28.27}/src/usso/api_key.py +2 -2
  4. {usso-0.28.26 → usso-0.28.27}/src/usso/authorization.py +32 -62
  5. {usso-0.28.26 → usso-0.28.27}/src/usso/client.py +2 -2
  6. {usso-0.28.26 → usso-0.28.27}/src/usso/config.py +6 -6
  7. {usso-0.28.26 → usso-0.28.27}/src/usso/exceptions.py +24 -10
  8. {usso-0.28.26 → usso-0.28.27}/src/usso/integrations/django/middleware.py +1 -1
  9. {usso-0.28.26 → usso-0.28.27}/src/usso/integrations/fastapi/dependency.py +1 -1
  10. {usso-0.28.26 → usso-0.28.27}/src/usso/integrations/fastapi/handler.py +9 -2
  11. {usso-0.28.26 → usso-0.28.27}/src/usso/session/async_session.py +7 -4
  12. {usso-0.28.26 → usso-0.28.27}/src/usso/session/base_session.py +3 -3
  13. {usso-0.28.26 → usso-0.28.27}/src/usso/session/session.py +7 -5
  14. {usso-0.28.26 → usso-0.28.27}/src/usso/user.py +6 -6
  15. {usso-0.28.26 → usso-0.28.27/src/usso.egg-info}/PKG-INFO +1 -1
  16. {usso-0.28.26 → usso-0.28.27}/tests/test_authorization.py +32 -34
  17. {usso-0.28.26 → usso-0.28.27}/tests/test_fastapi.py +13 -10
  18. {usso-0.28.26 → usso-0.28.27}/LICENSE.txt +0 -0
  19. {usso-0.28.26 → usso-0.28.27}/MANIFEST.in +0 -0
  20. {usso-0.28.26 → usso-0.28.27}/README.md +0 -0
  21. {usso-0.28.26 → usso-0.28.27}/pytest.ini +0 -0
  22. {usso-0.28.26 → usso-0.28.27}/setup.cfg +0 -0
  23. {usso-0.28.26 → usso-0.28.27}/src/usso/__init__.py +0 -0
  24. {usso-0.28.26 → usso-0.28.27}/src/usso/integrations/django/__init__.py +0 -0
  25. {usso-0.28.26 → usso-0.28.27}/src/usso/integrations/fastapi/__init__.py +0 -0
  26. {usso-0.28.26 → usso-0.28.27}/src/usso/session/__init__.py +0 -0
  27. {usso-0.28.26 → usso-0.28.27}/src/usso/utils/__init__.py +0 -0
  28. {usso-0.28.26 → usso-0.28.27}/src/usso/utils/string_utils.py +0 -0
  29. {usso-0.28.26 → usso-0.28.27}/src/usso.egg-info/SOURCES.txt +0 -0
  30. {usso-0.28.26 → usso-0.28.27}/src/usso.egg-info/dependency_links.txt +0 -0
  31. {usso-0.28.26 → usso-0.28.27}/src/usso.egg-info/entry_points.txt +0 -0
  32. {usso-0.28.26 → usso-0.28.27}/src/usso.egg-info/requires.txt +0 -0
  33. {usso-0.28.26 → 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.26
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.26"
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,91 +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
51
-
53
+ return action, [rp or "*" for rp in resource_path_parts], filters
52
54
 
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
55
 
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
- ("finance/*/*", "wallet", False),
88
- ]
89
- """
90
- if isinstance(user_path, str):
91
- user_parts = user_path.split("/")
92
- elif isinstance(user_path, list):
93
- 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
94
61
  else:
95
- raise ValueError(f"Invalid path type: {type(user_path)}")
62
+ raise ValueError(f"Invalid path type: {type(path)}")
96
63
 
97
- if isinstance(requested_path, str):
98
- req_parts = requested_path.split("/")
99
- elif isinstance(requested_path, list):
100
- req_parts = requested_path
101
- else:
102
- raise ValueError(f"Invalid path type: {type(requested_path)}")
103
64
 
65
+ def _match_path_parts(
66
+ user_parts: list[str], req_parts: list[str], strict: bool
67
+ ) -> bool:
104
68
  wildcard_found = False
105
69
  # Match resource name (rightmost)
106
70
  if not fnmatch.fnmatch(req_parts[-1], user_parts[-1]):
107
71
  return False
108
-
109
72
  if "*" in user_parts[-1]:
110
73
  wildcard_found = True
111
-
112
74
  # Match rest of the path from right to left
113
75
  user_path_parts = user_parts[:-1]
114
76
  req_path_parts = req_parts[:-1]
115
-
116
77
  for u, r in zip(
117
- reversed(user_path_parts),
118
- reversed(req_path_parts),
119
- strict=strict,
78
+ reversed(user_path_parts), reversed(req_path_parts), strict=strict
120
79
  ):
121
80
  if r and u and r != "*" and not fnmatch.fnmatch(r, u):
122
81
  return False
123
82
  if "*" in u:
124
83
  wildcard_found = True
125
-
126
84
  offset = len(user_path_parts) - len(req_path_parts)
127
85
  if offset > 0 and wildcard_found:
128
- for u in user_path_parts[-offset:]:
86
+ for u in user_path_parts[:offset]:
129
87
  if u != "*":
130
88
  return False
131
-
132
89
  return True
133
90
 
134
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
+
135
105
  def is_filter_match(user_filters: dict, requested_filters: dict) -> bool:
136
106
  """All user filters must match requested filters."""
137
107
  for k, v in user_filters.items():
@@ -222,7 +192,7 @@ def is_authorized(
222
192
  if not is_path_match(user_path, requested_path, strict=strict):
223
193
  return False
224
194
 
225
- if not is_filter_match(user_filters, reuested_filter):
195
+ if not is_filter_match(user_filters, reuested_filter or {}):
226
196
  return False
227
197
 
228
198
  if requested_action:
@@ -256,15 +226,15 @@ def check_access(
256
226
  if isinstance(filters, dict):
257
227
  filters = [{k: v} for k, v in filters.items()]
258
228
  elif filters is None:
259
- filters = ["*"]
229
+ filters = [{}]
260
230
 
261
231
  for scope in user_scopes:
262
- for filter in filters:
232
+ for filt in filters:
263
233
  if is_authorized(
264
234
  user_scope=scope,
265
235
  requested_path=resource_path,
266
236
  requested_action=action,
267
- reuested_filter=filter,
237
+ reuested_filter=filt,
268
238
  strict=strict,
269
239
  ):
270
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.26
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),
@@ -35,14 +30,17 @@ from src.usso.authorization import (
35
30
  ("files", "media/files/transactions", False),
36
31
  ("media/files", "media/files/transactions", False),
37
32
  ("finance/*/*", "wallet", False),
33
+ ("media//files", "media/files", True),
38
34
  ],
39
35
  )
40
- 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:
41
39
  assert is_path_match(user_path, requested_path, strict=False) == expected
42
40
 
43
41
 
44
42
  # Define pytest tests
45
- def test_exact_match_id():
43
+ def test_exact_match_id() -> None:
46
44
  scopes = ["read:media/file-manager/files?uid=file123"]
47
45
  assert (
48
46
  check_access(
@@ -50,9 +48,9 @@ def test_exact_match_id():
50
48
  "files",
51
49
  action="read",
52
50
  filters=[
53
- dict(namespace="media"),
54
- dict(service="file-manager"),
55
- dict(uid="file123"),
51
+ {"namespace": "media"},
52
+ {"service": "file-manager"},
53
+ {"uid": "file123"},
56
54
  ],
57
55
  )
58
56
  is True
@@ -60,7 +58,7 @@ def test_exact_match_id():
60
58
 
61
59
 
62
60
  # Define pytest tests
63
- def test_wildcard():
61
+ def test_wildcard() -> None:
64
62
  scopes = [
65
63
  "update:media/files/transactions?user_id=abc",
66
64
  "read:media/files/*",
@@ -86,7 +84,7 @@ def test_wildcard():
86
84
  )
87
85
 
88
86
 
89
- def test_insufficient_privilege():
87
+ def test_insufficient_privilege() -> None:
90
88
  scopes = ["read:media/files/file:uid:file123"]
91
89
  assert (
92
90
  check_access(
@@ -94,33 +92,33 @@ def test_insufficient_privilege():
94
92
  "file",
95
93
  "update",
96
94
  filters=[
97
- dict(namespace="finance"),
98
- dict(service="wallet"),
99
- dict(workspace_id="ws_7"),
95
+ {"namespace": "finance"},
96
+ {"service": "wallet"},
97
+ {"workspace_id": "ws_7"},
100
98
  ],
101
99
  )
102
100
  is False
103
101
  )
104
102
 
105
103
 
106
- def test_wildcard_match():
104
+ def test_wildcard_match() -> None:
107
105
  scopes = ["manage:media/files/file?*"]
108
106
  assert (
109
107
  check_access(
110
108
  scopes,
111
109
  "file",
112
110
  "update",
113
- filters=dict(
114
- namespace="finance",
115
- service="wallet",
116
- workspace_id="ws_7",
117
- ),
111
+ filters={
112
+ "namespace": "finance",
113
+ "service": "wallet",
114
+ "workspace_id": "ws_7",
115
+ },
118
116
  )
119
117
  is True
120
118
  )
121
119
 
122
120
 
123
- def test_match_by_user_id():
121
+ def test_match_by_user_id() -> None:
124
122
  scopes = ["manage:finance/wallet/transaction?user=user_1"]
125
123
  assert (
126
124
  check_access(
@@ -128,9 +126,9 @@ def test_match_by_user_id():
128
126
  "transaction",
129
127
  "update",
130
128
  filters=[
131
- dict(namespace="finance"),
132
- dict(service="wallet"),
133
- dict(workspace_id="ws_7"),
129
+ {"namespace": "finance"},
130
+ {"service": "wallet"},
131
+ {"workspace_id": "ws_7"},
134
132
  ],
135
133
  )
136
134
  is False
@@ -140,13 +138,13 @@ def test_match_by_user_id():
140
138
  scopes,
141
139
  "transaction",
142
140
  "update",
143
- filters=dict(namespace="finance", user="user_1"),
141
+ filters={"namespace": "finance", "user": "user_1"},
144
142
  )
145
143
  is True
146
144
  )
147
145
 
148
146
 
149
- def test_match_by_workspace_id():
147
+ def test_match_by_workspace_id() -> None:
150
148
  scopes = ["delete:finance/wallet/transaction?workspace_id=ws_7"]
151
149
  assert (
152
150
  check_access(
@@ -154,31 +152,31 @@ def test_match_by_workspace_id():
154
152
  "transaction",
155
153
  "delete",
156
154
  filters=[
157
- dict(namespace="finance"),
158
- dict(service="wallet"),
159
- dict(workspace_id="ws_7"),
155
+ {"namespace": "finance"},
156
+ {"service": "wallet"},
157
+ {"workspace_id": "ws_7"},
160
158
  ],
161
159
  )
162
160
  is True
163
161
  )
164
162
 
165
163
 
166
- def test_minimal_params_success():
164
+ def test_minimal_params_success() -> None:
167
165
  scopes = ["create:file?*"]
168
166
  assert check_access(scopes, "file", "create") is True
169
167
 
170
168
 
171
- def test_minimal_params_fail():
169
+ def test_minimal_params_fail() -> None:
172
170
  scopes = ["read:file?*"]
173
171
  assert check_access(scopes, "file", "create") is False
174
172
 
175
173
 
176
- def test_minimal_params_read_create_fail():
174
+ def test_minimal_params_read_create_fail() -> None:
177
175
  scopes = ["file"]
178
176
  assert check_access(scopes, "file", "create") is False
179
177
 
180
178
 
181
- def test_scope_subset():
179
+ def test_scope_subset() -> None:
182
180
  assert is_subset_scope(
183
181
  subset_scope="read:media/files?user_id=123",
184
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