diff --git a/README.md b/README.md index 97cbaba..64ecb92 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,10 @@ Initialization requires 5 parameters, which are all str type: | Name (in order) | Must | Description | | ---------------- | ---- | --------------------------------------------------- | | endpoint | Yes | Casdoor Server Url, such as `http://localhost:8000` | -| client_id | Yes | Application.client_id | -| client_secret | Yes | Application.client_secret | -| certificate | Yes | Same as Casdoor certificate | -| org_name | Yes |Organization name +| client_id | Yes | Application.client_id | +| client_secret | Yes | Application.client_secret | +| certificate | Yes | Same as Casdoor certificate | +| org_name | Yes | Organization name | ```python from casdoor import CasdoorSDK @@ -42,6 +42,27 @@ sdk = CasdoorSDK( org_name, ) ``` + +OR use async version + +```python +from casdoor import AsyncCasdoorSDK + +certificate = b'''-----BEGIN CERTIFICATE----- +MIIE+TCCAuGgAwIBAgIDAeJAMA0GCSqGSIb3DQEBCwUAMDYxHTAbBgNVBAoTFENh +... +-----END CERTIFICATE-----''' + +sdk = AsyncCasdoorSDK( + endpoint, + client_id, + client_secret, + certificate, + org_name, +) +``` + + ## Step2. Authorize with the Casdoor server At this point, we should use some ways to verify with the Casdoor server. diff --git a/setup.cfg b/setup.cfg index 1738c18..e129666 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,6 +31,7 @@ install_requires = requests pyjwt cryptography + aiohttp python_requires = >=3.6 test_suite = tests diff --git a/src/casdoor/__init__.py b/src/casdoor/__init__.py index a8e567a..ade2e8b 100644 --- a/src/casdoor/__init__.py +++ b/src/casdoor/__init__.py @@ -1,2 +1,3 @@ from .main import CasdoorSDK +from .async_main import AsyncCasdoorSDK from .user import User diff --git a/src/casdoor/async_main.py b/src/casdoor/async_main.py new file mode 100644 index 0000000..05bbfe6 --- /dev/null +++ b/src/casdoor/async_main.py @@ -0,0 +1,199 @@ +# Copyright 2021 The Casbin Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import aiohttp +import jwt +import json +from .user import User +from typing import List +from cryptography import x509 +from cryptography.hazmat.backends import default_backend + + +class AsyncCasdoorSDK: + def __init__( + self, + endpoint: str, + client_id: str, + client_secret: str, + certificate: str, + org_name: str, + application_name: str, + front_endpoint: str = None + ): + self.endpoint = endpoint + if front_endpoint: + self.front_endpoint = front_endpoint + else: + self.front_endpoint = endpoint.replace(":8000", ":7001") + self.client_id = client_id + self.client_secret = client_secret + self.certificate = certificate + self.org_name = org_name + self.application_name = application_name + self.grant_type = "authorization_code" + + self.algorithms = ["RS256"] + self._session = aiohttp.ClientSession() + + def __del__(self): + loop = asyncio.get_running_loop() + loop.create_task(self._session.close()) + + @property + def certification(self) -> bytes: + if type(self.certificate) is not str: + raise TypeError('certificate field must be str type') + return self.certificate.encode('utf-8') + + async def get_auth_link(self, redirect_uri: str, response_type: str = "code", scope: str = "read"): + url = self.front_endpoint + "/login/oauth/authorize" + params = { + "client_id": self.client_id, + "response_type": response_type, + "redirect_uri": redirect_uri, + "scope": scope, + "state": self.application_name, + } + async with self._session.request("", url, params=params) as request: + return request.url + + async def get_oauth_token(self, code: str) -> str: + """ + Request the Casdoor server to get access_token. + :param code: the code that sent from Casdoor using redirect url back to your server. + :return: access_token + """ + r = await self.oauth_token_request(code) + access_token = r.get("access_token") + return access_token + + async def oauth_token_request(self, code: str) -> dict: + """ + Request the Casdoor server to get access_token. + :param code: the code that sent from Casdoor using redirect url back to your server. + :return: Response from Casdoor + """ + url = self.endpoint + "/api/login/oauth/access_token" + params = { + "grant_type": self.grant_type, + "client_id": self.client_id, + "client_secret": self.client_secret, + "code": code, + } + async with self._session.post(url, data=params) as response: + return await response.json() + + async def refresh_token_request(self, refresh_token: str, scope: str = "") -> dict: + """ + Request the Casdoor server to get access_token. + :param refresh_token: refresh_token for send to Casdoor + :param scope: OAuth scope + :return: Response from Casdoor + """ + url = self.endpoint + "/api/login/oauth/refresh_token" + params = { + "grant_type": self.grant_type, + "client_id": self.client_id, + "client_secret": self.client_secret, + "scope": scope, + "refresh_token": refresh_token, + } + async with self._session.post(url, data=params) as request: + return await request.json() + + async def refresh_oauth_token(self, refresh_token: str, scope: str = "") -> str: + """ + Request the Casdoor server to get access_token. + :param refresh_token: refresh_token for send to Casdoor + :param scope: OAuth scope + :return: Response from Casdoor + """ + r = await self.refresh_token_request(refresh_token, scope) + access_token = r.get("access_token") + + return access_token + + def parse_jwt_token(self, token: str) -> dict: + """ + Converts the returned access_token to real data using jwt (JSON Web Token) algorithms. + :param token: access_token + :return: the data in dict format + """ + certificate = x509.load_pem_x509_certificate(self.certification, default_backend()) + + return_json = jwt.decode( + token, + certificate.public_key(), + algorithms=self.algorithms, + audience=self.client_id, + ) + return return_json + + async def get_users(self) -> List[dict]: + """ + Get the users from Casdoor. + :return: a list of dicts containing user info + """ + url = self.endpoint + "/api/get-users" + params = { + "owner": self.org_name, + "clientId": self.client_id, + "clientSecret": self.client_secret, + } + async with self._session.get(url, params=params) as request: + users = await request.json() + return users + + async def get_user(self, user_id: str) -> dict: + """ + Get the user from Casdoor providing the user_id. + :param user_id: the id of the user + :return: a dict that contains user's info + """ + url = self.endpoint + "/api/get-user" + params = { + "id": f"{self.org_name}/{user_id}", + "clientId": self.client_id, + "clientSecret": self.client_secret, + } + async with self._session.get(url, params=params) as request: + user = await request.json() + return user + + async def modify_user(self, method: str, user: User) -> dict: + url = self.endpoint + f"/api/{method}" + user.owner = self.org_name + params = { + "id": f"{user.owner}/{user.name}", + "clientId": self.client_id, + "clientSecret": self.client_secret, + } + user_info = json.dumps(user.to_dict()) + async with self._session.post(url, params=params, data=user_info) as request: + response = await request.json() + return response + + async def add_user(self, user: User) -> dict: + response = await self.modify_user("add-user", user) + return response + + async def update_user(self, user: User) -> dict: + response = await self.modify_user("update-user", user) + return response + + async def delete_user(self, user: User) -> dict: + response = await self.modify_user("delete-user", user) + return response diff --git a/src/tests/test_async_oauth.py b/src/tests/test_async_oauth.py new file mode 100644 index 0000000..6b775cf --- /dev/null +++ b/src/tests/test_async_oauth.py @@ -0,0 +1,107 @@ +# Copyright 2021 The Casbin Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from src.casdoor.async_main import AsyncCasdoorSDK, User +from unittest import IsolatedAsyncioTestCase + + +class TestOAuth(IsolatedAsyncioTestCase): + """ + You should replace the code content below and + the get_sdk() method's content with your own Casdoor + instance and such if you need to. And running these tests successfully + proves that your connection to Casdoor is good-and-working! + """ + + # server returned authorization code + code = "6d038ac60d4e1f17e742" + + @staticmethod + def get_sdk(): + + sdk = AsyncCasdoorSDK( + endpoint="http://test.casbin.com:8000", + client_id="3267f876b11e7d1cb217", + client_secret="3f0d1f06d28d65309c8f38b505cb9dcfa487754d", + certificate="CasdoorSecret", + org_name="built-in", + application_name="app-built-in" + ) + return sdk + + async def test_get_oauth_token(self): + sdk = self.get_sdk() + access_token = await sdk.get_oauth_token(self.code) + self.assertIsInstance(access_token, str) + + async def test_oauth_token_request(self): + sdk = self.get_sdk() + response = await sdk.oauth_token_request(self.code) + self.assertIsInstance(response, dict) + + async def test_refresh_token_request(self): + sdk = self.get_sdk() + response = await sdk.oauth_token_request(self.code) + refresh_token = response.get("refresh_token") + sdk.grant_type = "refresh_token" + response = await sdk.refresh_token_request(refresh_token) + sdk.grant_type = "authorization_code" + self.assertIsInstance(response, dict) + + async def test_get_oauth_refreshed_token(self): + sdk = self.get_sdk() + response = await sdk.oauth_token_request(self.code) + refresh_token = response.get("refresh_token") + sdk.grant_type = "refresh_token" + response = await sdk.refresh_oauth_token(refresh_token) + sdk.grant_type = "authorization_code" + self.assertIsInstance(response, str) + + async def test_parse_jwt_token(self): + sdk = self.get_sdk() + access_token = await sdk.get_oauth_token(self.code) + decoded_msg = sdk.parse_jwt_token(access_token) + self.assertIsInstance(decoded_msg, dict) + + async def test_get_users(self): + sdk = self.get_sdk() + users = await sdk.get_users() + self.assertIsInstance(users, list) + + async def test_get_user(self): + sdk = self.get_sdk() + user = await sdk.get_user("admin") + self.assertIsInstance(user, dict) + + async def test_modify_user(self): + sdk = self.get_sdk() + user = User() + user.name = "test_ffyuanda" + await sdk.delete_user(user) + + response = await sdk.add_user(user) + self.assertEqual(response["data"], "Affected") + + response = await sdk.delete_user(user) + self.assertEqual(response["data"], "Affected") + + response = await sdk.add_user(user) + self.assertEqual(response["data"], "Affected") + + user.phone = "phone" + response = await sdk.update_user(user) + self.assertEqual(response["data"], "Affected") + + self.assertIn("status", response) + self.assertIsInstance(response, dict) diff --git a/src/tests/test_oauth.py b/src/tests/test_oauth.py index 1f3d515..edf98aa 100644 --- a/src/tests/test_oauth.py +++ b/src/tests/test_oauth.py @@ -37,7 +37,7 @@ def get_sdk(): client_secret="3f0d1f06d28d65309c8f38b505cb9dcfa487754d", certificate="CasdoorSecret", org_name="built-in", - application_name= "app-built-in" + application_name="app-built-in" ) return sdk @@ -62,7 +62,9 @@ def test_get_oauth_refreshed_token(self): sdk = self.get_sdk() response = sdk.oauth_token_request(self.code) refresh_token = response.json().get("refresh_token") + sdk.grant_type = "refresh_token" response = sdk.refresh_oauth_token(refresh_token) + sdk.grant_type = "authorization_code" self.assertIsInstance(response, str) def test_parse_jwt_token(self):