Skip to content

Commit 06d22d9

Browse files
authored
Merge pull request #52 from permitio/gabriel/per-8596-python-sdk-add-support-for-bulk-check
Add Bulk Check
2 parents e2f593e + a201ac8 commit 06d22d9

8 files changed

Lines changed: 294 additions & 3 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,4 @@ dmypy.json
130130

131131
# editors
132132
.vscode/
133+
.DS_Store # macOS

permit/enforcement/enforcer.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ def set_if_not_none(d: dict, k: str, v):
2323
Action = str
2424
Resource = Union[dict, str]
2525

26+
CheckQuery = {
27+
"user": User,
28+
"action": Action,
29+
"resource": Resource,
30+
}
31+
2632

2733
class Enforcer:
2834
def __init__(self, config: PermitConfig):
@@ -42,6 +48,125 @@ def context_store(self):
4248
"""
4349
return self._context_store
4450

51+
async def bulk_check(
52+
self,
53+
checks: list[CheckQuery],
54+
context: Context = {},
55+
) -> list[bool]:
56+
"""
57+
Checks if a user is authorized to perform an action on a resource within the specified context.
58+
59+
Args:
60+
checks: A list of CheckQuery objects representing the authorization queries to be performed.
61+
context: The context object representing the context in which the action is performed. Defaults to None.
62+
63+
Returns:
64+
list[bool]: A list of booleans indicating whether the user is authorized for each resource.
65+
66+
Raises:
67+
PermitConnectionError: If an error occurs while sending the authorization request to the PDP.
68+
69+
Examples:
70+
71+
# Bulk query of multiple check conventions
72+
await permit.bulk_check([
73+
{
74+
"user": user,
75+
"action": "close",
76+
"resource": {type: "issue", key: "1234"},
77+
},
78+
{
79+
"user": {key: "user"},
80+
"action": "close",
81+
"resource": "issue:1235",
82+
},
83+
{
84+
"user": "user_a",
85+
"action": "close",
86+
"resource": "issue",
87+
},
88+
])
89+
"""
90+
91+
input = []
92+
for check in checks:
93+
normalized_user: UserInput = (
94+
UserInput(key=check["user"])
95+
if isinstance(check["user"], str)
96+
else UserInput(**check["user"])
97+
)
98+
normalized_resource: ResourceInput = self._normalize_resource(
99+
(
100+
self._resource_from_string(check["resource"])
101+
if isinstance(check["resource"], str)
102+
else ResourceInput(**check["resource"])
103+
)
104+
)
105+
query_context = self._context_store.get_derived_context(context)
106+
input.append(
107+
dict(
108+
user=normalized_user.dict(exclude_unset=True),
109+
action=check["action"],
110+
resource=normalized_resource.dict(exclude_unset=True),
111+
context=query_context,
112+
)
113+
)
114+
115+
async with aiohttp.ClientSession(headers=self._headers) as session:
116+
check_url = f"{self._base_url}/allowed/bulk"
117+
try:
118+
async with session.post(
119+
check_url,
120+
data=json.dumps(input),
121+
) as response:
122+
if response.status != 200:
123+
error_json: dict = await response.json()
124+
logger.error(
125+
"error in permit.check({}):\n{}\n{}".format(
126+
(
127+
[
128+
[
129+
check.get("user"),
130+
check.get("action"),
131+
check.get("resource"),
132+
]
133+
for check in input
134+
]
135+
),
136+
f"status code: {response.status}",
137+
repr(error_json),
138+
)
139+
)
140+
raise PermitConnectionError
141+
content: dict = await response.json()
142+
logger.debug(
143+
f"permit.check() response:\ninput: {pformat(input, indent=2)}\nresponse status: {response.status}\nresponse data: {pformat(content, indent=2)}"
144+
)
145+
data = content.get(
146+
"allow", content.get("result", {}).get("allow", [])
147+
)
148+
decisions: list[bool] = [
149+
bool(item.get("allow", False)) for item in data
150+
]
151+
except aiohttp.ClientError as err:
152+
logger.error(
153+
"error in permit.check({}):\n{}".format(
154+
(
155+
[
156+
[
157+
check.get("user"),
158+
check.get("action"),
159+
check.get("resource"),
160+
]
161+
for check in input
162+
]
163+
),
164+
err,
165+
)
166+
)
167+
raise PermitConnectionError
168+
return decisions
169+
45170
async def check(
46171
self,
47172
user: User,

permit/permit.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from .api.api_client import PermitApiClient
77
from .api.elements import ElementsApi
88
from .config import PermitConfig
9-
from .enforcement.enforcer import Action, Enforcer, Resource, User
9+
from .enforcement.enforcer import Action, CheckQuery, Enforcer, Resource, User
1010
from .logger import configure_logger
1111
from .utils.context import Context
1212

@@ -64,6 +64,47 @@ def elements(self) -> ElementsApi:
6464
"""
6565
return self._elements
6666

67+
async def bulk_check(
68+
self,
69+
checks: list[CheckQuery],
70+
context: Context = {},
71+
) -> list[bool]:
72+
"""
73+
Checks if a user is authorized to perform an action on a list of resources within the specified context.
74+
75+
Args:
76+
checks: A list of check queries, each query contain user, action, and resource.
77+
context: The context object representing the context in which the action is performed. Defaults to None.
78+
79+
Returns:
80+
list[bool]: A list of booleans indicating whether the user is authorized for each resource.
81+
82+
Raises:
83+
PermitConnectionError: If an error occurs while sending the authorization request to the PDP.
84+
85+
Examples:
86+
87+
# Bulk query of multiple check conventions
88+
await permit.bulk_check([
89+
{
90+
"user": user,
91+
"action": "close",
92+
"resource": {type: "issue", key: "1234"},
93+
},
94+
{
95+
"user": {key: "user"},
96+
"action": "close",
97+
"resource": "issue:1235",
98+
},
99+
{
100+
"user": "user_a",
101+
"action": "close",
102+
"resource": "issue",
103+
},
104+
])
105+
"""
106+
return await self._enforcer.bulk_check(checks, context)
107+
67108
async def check(
68109
self,
69110
user: User,

permit/sync.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from .api.elements import SyncElementsApi
44
from .api.sync_api_client import SyncPermitApiClient
55
from .config import PermitConfig
6-
from .enforcement.enforcer import Action, Resource, SyncEnforcer, User
6+
from .enforcement.enforcer import Action, CheckQuery, Resource, SyncEnforcer, User
77
from .permit import Permit as AsyncPermit
88
from .utils.context import Context
99

@@ -39,6 +39,47 @@ def elements(self) -> SyncElementsApi:
3939
"""
4040
return self._elements
4141

42+
def bulk_check(
43+
self,
44+
checks: list[CheckQuery],
45+
context: Context = {},
46+
) -> list[bool]:
47+
"""
48+
Checks if a user is authorized to perform an action on a list of resources within the specified context.
49+
50+
Args:
51+
checks: A list of CheckQuery objects representing the authorization checks to be performed.
52+
context: The context object representing the context in which the action is performed. Defaults to None.
53+
54+
Returns:
55+
list[bool]: A list of booleans indicating whether the user is authorized for each resource.
56+
57+
Raises:
58+
PermitConnectionError: If an error occurs while sending the authorization request to the PDP.
59+
60+
Examples:
61+
62+
# Bulk query of multiple check conventions
63+
await permit.bulk_check([
64+
{
65+
"user": user,
66+
"action": "close",
67+
"resource": {type: "issue", key: "1234"},
68+
},
69+
{
70+
"user": {key: "user"},
71+
"action": "close",
72+
"resource": "issue:1235",
73+
},
74+
{
75+
"user": "user_a",
76+
"action": "close",
77+
"resource": "issue",
78+
},
79+
])
80+
"""
81+
return self._enforcer.bulk_check(checks, context)
82+
4283
def check(
4384
self,
4485
user: User,

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def get_requirements(env=""):
1010

1111
setup(
1212
name="permit",
13-
version="2.1.1",
13+
version="2.2.0",
1414
packages=find_packages(),
1515
author="Asaf Cohen",
1616
author_email="asaf@permit.io",

tests/test_abac_e2e.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,34 @@ async def test_abac_e2e(permit: Permit):
212212

213213
print_break()
214214

215+
logger.info("testing admin permissions in bulk")
216+
assert await permit.bulk_check(
217+
[
218+
{
219+
"user": USER_A.key,
220+
"action": "create",
221+
"resource": {"type": "document", "tenant": TESLA.key},
222+
},
223+
{
224+
"user": USER_B.key,
225+
"action": "create",
226+
"resource": {"type": "document", "tenant": TESLA.key},
227+
},
228+
{
229+
"user": USER_A.key,
230+
"action": "sign",
231+
"resource": {"type": "document", "tenant": TESLA.key},
232+
},
233+
{
234+
"user": USER_B.key,
235+
"action": "sign",
236+
"resource": {"type": "document", "tenant": TESLA.key},
237+
},
238+
]
239+
) == [True, True, False, False]
240+
241+
print_break()
242+
215243
logger.info("creating condition sets")
216244
for condition_set_data in CONDITION_SETS:
217245
condition_set = await permit.api.condition_sets.create(condition_set_data)

tests/test_rbac_e2e.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,36 @@ async def test_permission_check_e2e(permit: Permit):
189189

190190
print_break()
191191

192+
logger.info("testing bulk permission check")
193+
assert (
194+
await permit.bulk_check(
195+
[
196+
{
197+
"user": "auth0|elon",
198+
"action": "read",
199+
"resource": {
200+
"type": "document",
201+
"tenant": "tesla",
202+
"attributes": resource_attributes,
203+
},
204+
},
205+
{
206+
"user": user.dict(),
207+
"action": "read",
208+
"resource": {"type": document.key, "tenant": tenant.key},
209+
},
210+
{
211+
"user": user.key,
212+
"action": "create",
213+
"resource": {"type": document.key, "tenant": tenant.key},
214+
},
215+
],
216+
{},
217+
)
218+
) == [True, True, False]
219+
220+
print_break()
221+
192222
logger.info("changing the user roles")
193223

194224
# change the user role - assign admin role

tests/test_rbac_e2e_sync.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,31 @@ def test_permission_check_e2e(sync_permit: SyncPermit):
191191

192192
print_break()
193193

194+
logger.info("testing permissions in bulk")
195+
assert permit.bulk_check(
196+
[
197+
{
198+
"user": "auth0|elon",
199+
"action": "read",
200+
"resource": {
201+
"type": "document",
202+
"tenant": "tesla",
203+
"attributes": resource_attributes,
204+
},
205+
},
206+
{
207+
"user": user.dict(),
208+
"action": "read",
209+
"resource": {"type": document.key, "tenant": tenant.key},
210+
},
211+
{
212+
"user": user.key,
213+
"action": "create",
214+
"resource": {"type": document.key, "tenant": tenant.key},
215+
},
216+
]
217+
) == [True, True, False]
218+
194219
logger.info("changing the user roles")
195220

196221
# change the user role - assign admin role

0 commit comments

Comments
 (0)