usso 0.28.30.dev1__tar.gz → 0.28.32__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.30.dev1/src/usso.egg-info → usso-0.28.32}/PKG-INFO +1 -1
  2. {usso-0.28.30.dev1 → usso-0.28.32}/pyproject.toml +1 -1
  3. {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/__init__.py +2 -2
  4. {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/authorization.py +21 -5
  5. {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/client.py +6 -4
  6. {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/exceptions.py +1 -1
  7. {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/integrations/fastapi/dependency.py +31 -1
  8. {usso-0.28.30.dev1 → usso-0.28.32/src/usso.egg-info}/PKG-INFO +1 -1
  9. {usso-0.28.30.dev1 → usso-0.28.32}/tests/test_authorization.py +61 -0
  10. {usso-0.28.30.dev1 → usso-0.28.32}/LICENSE.txt +0 -0
  11. {usso-0.28.30.dev1 → usso-0.28.32}/MANIFEST.in +0 -0
  12. {usso-0.28.30.dev1 → usso-0.28.32}/README.md +0 -0
  13. {usso-0.28.30.dev1 → usso-0.28.32}/pytest.ini +0 -0
  14. {usso-0.28.30.dev1 → usso-0.28.32}/setup.cfg +0 -0
  15. {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/api_key.py +0 -0
  16. {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/config.py +0 -0
  17. {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/integrations/django/__init__.py +0 -0
  18. {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/integrations/django/middleware.py +0 -0
  19. {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/integrations/fastapi/__init__.py +0 -0
  20. {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/integrations/fastapi/handler.py +0 -0
  21. {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/session/__init__.py +0 -0
  22. {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/session/async_session.py +0 -0
  23. {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/session/base_session.py +0 -0
  24. {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/session/session.py +0 -0
  25. {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/user.py +0 -0
  26. {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/utils/__init__.py +0 -0
  27. {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/utils/string_utils.py +0 -0
  28. {usso-0.28.30.dev1 → usso-0.28.32}/src/usso.egg-info/SOURCES.txt +0 -0
  29. {usso-0.28.30.dev1 → usso-0.28.32}/src/usso.egg-info/dependency_links.txt +0 -0
  30. {usso-0.28.30.dev1 → usso-0.28.32}/src/usso.egg-info/entry_points.txt +0 -0
  31. {usso-0.28.30.dev1 → usso-0.28.32}/src/usso.egg-info/requires.txt +0 -0
  32. {usso-0.28.30.dev1 → usso-0.28.32}/src/usso.egg-info/top_level.txt +0 -0
  33. {usso-0.28.30.dev1 → usso-0.28.32}/tests/test_fastapi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usso
3
- Version: 0.28.30.dev1
3
+ Version: 0.28.32
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.30dev1"
7
+ version = "0.28.32"
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"
@@ -5,8 +5,8 @@ with Python frameworks, enabling secure and seamless authentication
5
5
  across microservices.
6
6
  """
7
7
 
8
- from .client import AuthConfig, UssoAuth
9
- from .config import APIHeaderConfig, HeaderConfig
8
+ from .client import UssoAuth
9
+ from .config import APIHeaderConfig, AuthConfig, HeaderConfig
10
10
  from .exceptions import USSOException
11
11
  from .user import UserData
12
12
 
@@ -7,8 +7,10 @@ PRIVILEGE_LEVELS = {
7
7
  "update": 30,
8
8
  "delete": 40,
9
9
  "manage": 50,
10
+ "admin": 60,
10
11
  "owner": 90,
11
12
  "*": 90,
13
+ "superadmin": 100,
12
14
  }
13
15
 
14
16
 
@@ -179,11 +181,25 @@ def broadest_scope_filter(filters: list[dict]) -> dict:
179
181
  return min(filters, key=restriction_score)
180
182
 
181
183
 
184
+ def owner_authorization(
185
+ requested_filter: dict[str, str] | None = None,
186
+ user_id: str | None = None,
187
+ self_action: str = "owner",
188
+ action: str = "read",
189
+ ) -> bool:
190
+ user_level = PRIVILEGE_LEVELS.get(self_action or "read", 10)
191
+ req_level = PRIVILEGE_LEVELS.get(action or "read", 10)
192
+
193
+ if user_id and requested_filter.get("user_id") == user_id:
194
+ return user_level >= req_level
195
+ return False
196
+
197
+
182
198
  def is_authorized(
183
199
  user_scope: str,
184
200
  requested_path: str,
185
- requested_action: str | None = None,
186
- reuested_filter: dict[str, str] | None = None,
201
+ requested_action: str = "read",
202
+ requested_filter: dict[str, str] | None = None,
187
203
  *,
188
204
  strict: bool = False,
189
205
  ) -> bool:
@@ -192,12 +208,12 @@ def is_authorized(
192
208
  if not is_path_match(user_path, requested_path, strict=strict):
193
209
  return False
194
210
 
195
- if not is_filter_match(user_filters, reuested_filter or {}):
211
+ if not is_filter_match(user_filters, requested_filter or {}):
196
212
  return False
197
213
 
198
214
  if requested_action:
199
215
  user_level = PRIVILEGE_LEVELS.get(user_action or "read", 10)
200
- req_level = PRIVILEGE_LEVELS.get(requested_action, 0)
216
+ req_level = PRIVILEGE_LEVELS.get(requested_action, 10)
201
217
  return user_level >= req_level
202
218
 
203
219
  return True
@@ -234,7 +250,7 @@ def check_access(
234
250
  user_scope=scope,
235
251
  requested_path=resource_path,
236
252
  requested_action=action,
237
- reuested_filter=filt,
253
+ requested_filter=filt,
238
254
  strict=strict,
239
255
  ):
240
256
  return True
@@ -1,4 +1,6 @@
1
+ import json
1
2
  import logging
3
+ import os
2
4
  from urllib.parse import urlparse
3
5
 
4
6
  import usso_jwt.exceptions
@@ -32,7 +34,10 @@ class UssoAuth:
32
34
  jwt_config: JWT configuration(s) to use for token validation
33
35
  """
34
36
  if jwt_config is None:
35
- jwt_config = AuthConfig()
37
+ if os.getenv("JWT_CONFIGS"):
38
+ jwt_config = json.loads(os.getenv("JWT_CONFIGS"))
39
+ else:
40
+ jwt_config = AuthConfig()
36
41
  self.jwt_configs = AuthConfig.validate_jwt_configs(jwt_config)
37
42
  self.from_base_usso_url = from_base_usso_url
38
43
 
@@ -73,7 +78,6 @@ class UssoAuth:
73
78
  f"{self.from_base_usso_url}/.well-known/jwks.json?"
74
79
  f"domain={iss_domain}"
75
80
  )
76
- logging.error(f"{iss_domain=} {jwks_url=}")
77
81
  jwt_obj.config.jwks_url = jwks_url
78
82
  if jwt_obj.verify(
79
83
  expected_token_type=expected_token_type,
@@ -82,8 +86,6 @@ class UssoAuth:
82
86
  return jwt_obj.payload
83
87
  except usso_jwt.exceptions.JWTError as e:
84
88
  exp = e
85
- else:
86
- logging.error("No from_base_usso_url")
87
89
 
88
90
  for jwk_config in self.jwt_configs:
89
91
  try:
@@ -47,7 +47,7 @@ class PermissionDenied(USSOException):
47
47
  **kwargs: dict,
48
48
  ) -> None:
49
49
  super().__init__(
50
- 403, error=error, message=message, detail=detail, **kwargs
50
+ 403, error=error, detail=detail, message=message, **kwargs
51
51
  )
52
52
 
53
53
 
@@ -1,10 +1,11 @@
1
1
  import logging
2
+ from collections.abc import Callable
2
3
 
3
4
  from fastapi import Request, WebSocket
4
5
 
5
6
  from ...client import UssoAuth
6
7
  from ...config import AuthConfig, AvailableJwtConfigs
7
- from ...exceptions import _handle_exception
8
+ from ...exceptions import PermissionDenied, _handle_exception
8
9
  from ...user import UserData
9
10
 
10
11
  logger = logging.getLogger("usso")
@@ -84,3 +85,32 @@ class USSOAuthentication(UssoAuth):
84
85
  message="No token provided",
85
86
  raise_exception=self.raise_exception,
86
87
  )
88
+
89
+ def authorize(
90
+ self,
91
+ *,
92
+ action: str = "read",
93
+ resource_path: str,
94
+ filter_data: dict | None = None,
95
+ ) -> Callable[[Request], UserData]:
96
+ def _authorize(request: Request) -> UserData:
97
+ from ... import authorization
98
+
99
+ user = self.usso_access_security(request)
100
+ user_scopes = user.scopes or []
101
+ if not authorization.check_access(
102
+ user_scopes=user_scopes,
103
+ resource_path=resource_path,
104
+ action=action,
105
+ filters=filter_data,
106
+ ):
107
+ raise PermissionDenied(
108
+ detail=(
109
+ f"User {user.uid} is not authorized "
110
+ f"to {action} {resource_path}"
111
+ )
112
+ )
113
+
114
+ return user
115
+
116
+ return _authorize
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usso
3
- Version: 0.28.30.dev1
3
+ Version: 0.28.32
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>
@@ -3,11 +3,72 @@ import pytest
3
3
  from src.usso.authorization import (
4
4
  check_access,
5
5
  has_subset_scope,
6
+ is_authorized,
6
7
  is_path_match,
7
8
  is_subset_scope,
9
+ owner_authorization,
8
10
  )
9
11
 
10
12
 
13
+ @pytest.mark.parametrize(
14
+ "requested_filter, user_id, self_action, action, expected",
15
+ [
16
+ ({"user_id": "123"}, "123", "owner", "read", True),
17
+ ({}, "123", "owner", "create", False),
18
+ ({"user_id": "123"}, "123", "read", "create", False),
19
+ ],
20
+ )
21
+ def test_owner_authorization(
22
+ requested_filter: dict[str, str],
23
+ user_id: str,
24
+ self_action: str,
25
+ action: str,
26
+ expected: bool,
27
+ ) -> None:
28
+ assert (
29
+ owner_authorization(requested_filter, user_id, self_action, action)
30
+ == expected
31
+ )
32
+
33
+
34
+ @pytest.mark.parametrize(
35
+ "user_scope,requested_path,requested_action,requested_filter,strict,expected",
36
+ [
37
+ (
38
+ "read:media/files",
39
+ "media/files",
40
+ "read",
41
+ {"user_id": "123"},
42
+ False,
43
+ True,
44
+ ),
45
+ ("read:media/files", "media/files", "read", None, False, True),
46
+ ("media/files", "media/files", "read", None, False, True),
47
+ ("media/files", "files", "create", None, False, False),
48
+ ("media/*", "files", "read", None, False, False),
49
+ ("read:media/files", "media/files", "create", None, False, False),
50
+ ],
51
+ )
52
+ def test_is_authorized(
53
+ user_scope: str,
54
+ requested_path: str,
55
+ requested_action: str,
56
+ requested_filter: dict[str, str] | None,
57
+ strict: bool,
58
+ expected: bool,
59
+ ) -> None:
60
+ assert (
61
+ is_authorized(
62
+ user_scope,
63
+ requested_path,
64
+ requested_action,
65
+ requested_filter,
66
+ strict=strict,
67
+ )
68
+ == expected
69
+ )
70
+
71
+
11
72
  @pytest.mark.parametrize(
12
73
  "user_path, requested_path, expected",
13
74
  [
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes