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.
- {usso-0.28.30.dev1/src/usso.egg-info → usso-0.28.32}/PKG-INFO +1 -1
- {usso-0.28.30.dev1 → usso-0.28.32}/pyproject.toml +1 -1
- {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/__init__.py +2 -2
- {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/authorization.py +21 -5
- {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/client.py +6 -4
- {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/exceptions.py +1 -1
- {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/integrations/fastapi/dependency.py +31 -1
- {usso-0.28.30.dev1 → usso-0.28.32/src/usso.egg-info}/PKG-INFO +1 -1
- {usso-0.28.30.dev1 → usso-0.28.32}/tests/test_authorization.py +61 -0
- {usso-0.28.30.dev1 → usso-0.28.32}/LICENSE.txt +0 -0
- {usso-0.28.30.dev1 → usso-0.28.32}/MANIFEST.in +0 -0
- {usso-0.28.30.dev1 → usso-0.28.32}/README.md +0 -0
- {usso-0.28.30.dev1 → usso-0.28.32}/pytest.ini +0 -0
- {usso-0.28.30.dev1 → usso-0.28.32}/setup.cfg +0 -0
- {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/api_key.py +0 -0
- {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/config.py +0 -0
- {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/integrations/django/__init__.py +0 -0
- {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/integrations/django/middleware.py +0 -0
- {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/integrations/fastapi/__init__.py +0 -0
- {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/integrations/fastapi/handler.py +0 -0
- {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/session/__init__.py +0 -0
- {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/session/async_session.py +0 -0
- {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/session/base_session.py +0 -0
- {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/session/session.py +0 -0
- {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/user.py +0 -0
- {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/utils/__init__.py +0 -0
- {usso-0.28.30.dev1 → usso-0.28.32}/src/usso/utils/string_utils.py +0 -0
- {usso-0.28.30.dev1 → usso-0.28.32}/src/usso.egg-info/SOURCES.txt +0 -0
- {usso-0.28.30.dev1 → usso-0.28.32}/src/usso.egg-info/dependency_links.txt +0 -0
- {usso-0.28.30.dev1 → usso-0.28.32}/src/usso.egg-info/entry_points.txt +0 -0
- {usso-0.28.30.dev1 → usso-0.28.32}/src/usso.egg-info/requires.txt +0 -0
- {usso-0.28.30.dev1 → usso-0.28.32}/src/usso.egg-info/top_level.txt +0 -0
- {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.
|
|
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.
|
|
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
|
|
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
|
|
186
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
@@ -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.
|
|
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
|
|
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
|