From 9f7de8a749853e1e0e44d0c3ef0e8d5ac1430ae4 Mon Sep 17 00:00:00 2001 From: Attack825 <2707138687@qq.com> Date: Tue, 29 Jul 2025 23:42:50 +0800 Subject: [PATCH] feat: adapt async enforce/batch_enforce --- src/casdoor/async_main.py | 156 +++++++++++++++++++++------------- src/casdoor/main.py | 2 +- src/tests/test_async_oauth.py | 101 +++++----------------- 3 files changed, 119 insertions(+), 140 deletions(-) diff --git a/src/casdoor/async_main.py b/src/casdoor/async_main.py index 6853923..6732e1d 100644 --- a/src/casdoor/async_main.py +++ b/src/casdoor/async_main.py @@ -13,6 +13,7 @@ # limitations under the License. import base64 +import json from typing import Dict, List, Optional import aiohttp @@ -263,73 +264,114 @@ def parse_jwt_token(self, token: str, **kwargs) -> Dict: async def enforce( self, - permission_model_name: str, - sub: str, - obj: str, - act: str, - v3: Optional[str] = None, - v4: Optional[str] = None, - v5: Optional[str] = None, + permission_id: str, + model_id: str, + resource_id: str, + enforce_id: str, + owner: str, + casbin_request: Optional[List[str]] = None, ) -> bool: """ - Send data to Casdoor enforce API - # https://casdoor.org/docs/permission/exposed-casbin-apis#enforce - - :param permission_model_name: Name permission model - :param sub: sub from Casbin - :param obj: obj from Casbin - :param act: act from Casbin - :param v3: v3 from Casbin - :param v4: v4 from Casbin - :param v5: v5 from Casbin + Send data to Casdoor enforce API asynchronously + + :param permission_id: the permission id (i.e. organization name/permission name) + :param model_id: the model id + :param resource_id: the resource id + :param enforce_id: the enforce id + :param owner: the owner of the permission + :param casbin_request: a list containing the request data (i.e. sub, obj, act) + :return: a boolean value indicating whether the request is allowed """ - path = "/api/enforce" - params = { - "id": permission_model_name, - "v0": sub, - "v1": obj, - "v2": act, - "v3": v3, - "v4": v4, - "v5": v5, + url = "/api/enforce" + params: Dict[str, str] = { + "permissionId": permission_id, + "modelId": model_id, + "resourceId": resource_id, + "enforceId": enforce_id, + "owner": owner, } + async with self._session as session: - has_permission = await session.post(path, headers=self.headers, json=params) - if not isinstance(has_permission, bool): - raise ValueError(f"Casdoor response error: {has_permission}") - return has_permission + response = await session.post( + url, + params=params, + data=json.dumps(casbin_request), + auth=aiohttp.BasicAuth(self.client_id, self.client_secret), + headers={"Content-Type": "application/json"}, + ) + + if isinstance(response, dict): + data = response.get("data") + if isinstance(data, list) and len(data) > 0: + has_permission = data[0] + else: + has_permission = response + else: + has_permission = response - async def batch_enforce(self, permission_model_name: str, permission_rules: List[List[str]]) -> List[bool]: - """ - Send data to Casdoor enforce API - - :param permission_model_name: Name permission model - :param permission_rules: permission rules to enforce - [][0] -> sub: sub from Casbin - [][1] -> obj: obj from Casbin - [][2] -> act: act from Casbin - [][3] -> v3: v3 from Casbin (optional) - [][4] -> v4: v4 from Casbin (optional) - [][5] -> v5: v5 from Casbin (optional) - """ - path = "/api/batch-enforce" + if not isinstance(has_permission, bool): + error_str = f"Casdoor response error (invalid type {type(has_permission)}):\n{json.dumps(response)}" + raise ValueError(error_str) - def map_rule(rule: List[str], idx) -> Dict: - if len(rule) < 3: - raise ValueError(f"Invalid permission rule[{idx}]: {rule}") - result = {"id": permission_model_name} - for i in range(len(rule)): - result.update({f"v{i}": rule[i]}) - return result + return has_permission - params = [map_rule(permission_rules[i], i) for i in range(len(permission_rules))] + async def batch_enforce( + self, + permission_id: str, + model_id: str, + enforce_id: str, + owner: str, + casbin_request: Optional[List[List[str]]] = None, + ) -> List[bool]: + """ + Send data to Casdoor batch enforce API asynchronously + + :param permission_id: the permission id (i.e. organization name/permission name) + :param model_id: the model id + :param enforce_id: the enforce id + :param owner: the owner of the permission + :param casbin_request: a list of lists containing the request data + :return: a list of boolean values indicating whether each request is allowed + """ + url = "/api/batch-enforce" + params = { + "permissionId": permission_id, + "modelId": model_id, + "enforceId": enforce_id, + "owner": owner, + } async with self._session as session: - enforce_results = await session.post(path, headers=self.headers, json=params) - if not isinstance(enforce_results, bool): - raise ValueError(f"Casdoor response error:{enforce_results}") - - return enforce_results + response = await session.post( + url, + params=params, + data=json.dumps(casbin_request), + auth=aiohttp.BasicAuth(self.client_id, self.client_secret), + headers={"Content-Type": "application/json"}, + ) + + data = response.get("data") + if data is None: + error_str = "Casdoor response error: 'data' field is missing\n" + json.dumps(response) + raise ValueError(error_str) + + if not isinstance(data, list): + error_str = f"Casdoor 'data' is not a list (got {type(data)}):\n{json.dumps(response)}" + raise ValueError(error_str) + + enforce_results = data[0] if data else [] + + if ( + not isinstance(enforce_results, list) + or len(enforce_results) > 0 + and not isinstance(enforce_results[0], bool) + ): + error_str = ( + f"Casdoor response contains invalid results (got {type(enforce_results)}):\n{json.dumps(response)}" + ) + raise ValueError(error_str) + + return enforce_results async def get_users(self) -> Dict: """ diff --git a/src/casdoor/main.py b/src/casdoor/main.py index 62ca55f..c347a94 100644 --- a/src/casdoor/main.py +++ b/src/casdoor/main.py @@ -258,7 +258,7 @@ def parse_jwt_token(self, token: str, **kwargs) -> Dict: certificate = x509.load_pem_x509_certificate(self.certification, default_backend()) return_json = jwt.decode( - token.encode("utf-8"), + token, certificate.public_key(), algorithms=self.algorithms, audience=self.client_id, diff --git a/src/tests/test_async_oauth.py b/src/tests/test_async_oauth.py index e0be3cc..ec5f93d 100644 --- a/src/tests/test_async_oauth.py +++ b/src/tests/test_async_oauth.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from unittest import IsolatedAsyncioTestCase, mock +from unittest import IsolatedAsyncioTestCase import src.tests.test_util as test_util from src.casdoor.async_main import AsyncCasdoorSDK @@ -28,7 +28,7 @@ class TestOAuth(IsolatedAsyncioTestCase): """ # server returned authorization code - code = "6d038ac60d4e1f17e742" + code = "21dc0ac806c27e6d7962" # Casdoor user and password for auth with # Resource Owner Password Credentials Grant. @@ -135,92 +135,29 @@ async def test_parse_jwt_token(self): self.assertIsInstance(decoded_msg, dict) async def test_enforce(self): - sdk = self.get_sdk() - status = await sdk.enforce("built-in/permission-built-in", "admin", "a", "ac") - self.assertIsInstance(status, bool) - - def mocked_enforce_requests_post(*args, **kwargs): - class MockResponse: - def __init__( - self, - json_data, - status_code=200, - ): - self.json_data = json_data - self.status_code = status_code - - def json(self): - return self.json_data - - result = True - for i in range(0, 5): - if kwargs.get("json").get(f"v{i}") != f"v{i}": - result = False - - return MockResponse(result) - - @mock.patch("aiohttp.ClientSession.post", side_effect=mocked_enforce_requests_post) - async def test_enforce_parmas(self, mock_post): sdk = self.get_sdk() status = await sdk.enforce( - "built-in/permission-built-in", - "v0", - "v1", - "v2", - v3="v3", - v4="v4", - v5="v5", + permission_id="built-in/permission-built-in", + model_id="", + resource_id="", + enforce_id="", + owner="", + casbin_request=["alice", "data1", "read"], ) - self.assertEqual(status, True) - - def mocked_batch_enforce_requests_post(*args, **kwargs): - class MockResponse: - def __init__( - self, - json_data, - status_code=200, - ): - self.json_data = json_data - self.status_code = status_code - - def json(self): - return self.json_data - - json = kwargs.get("json") - result = [True for i in range(0, len(json))] - for k in range(0, len(json)): - for i in range(0, len(json[k]) - 1): - if json[k].get(f"v{i}") != f"v{i}": - result[k] = False - - return MockResponse(result) - - @mock.patch( - "aiohttp.ClientSession.post", - side_effect=mocked_batch_enforce_requests_post, - ) - def test_batch_enforce(self, mock_post): + self.assertIsInstance(status, bool) + + async def test_batch_enforce(self): sdk = self.get_sdk() - status = sdk.batch_enforce( - "built-in/permission-built-in", - [ - ["v0", "v1", "v2", "v3", "v4", "v5"], - ["v0", "v1", "v2", "v3", "v4", "v1"], - ], + status = await sdk.batch_enforce( + permission_id="built-in/permission-built-in", + model_id="", + enforce_id="", + owner="", + casbin_request=[["alice", "data1", "read"], ["bob", "data2", "write"]], ) self.assertEqual(len(status), 2) - self.assertEqual(status[0], True) - self.assertEqual(status[1], False) - - @mock.patch( - "aiohttp.ClientSession.post", - side_effect=mocked_batch_enforce_requests_post, - ) - def test_batch_enforce_raise(self, mock_post): - sdk = self.get_sdk() - with self.assertRaises(ValueError) as context: - sdk.batch_enforce("built-in/permission-built-in", [["v0", "v1"]]) - self.assertEqual("Invalid permission rule[0]: ['v0', 'v1']", str(context.exception)) + self.assertIsInstance(status[0], bool) + self.assertIsInstance(status[1], bool) async def test_get_users(self): sdk = self.get_sdk()