diff --git a/Makefile b/Makefile index a5242a58..b5de6c7e 100644 --- a/Makefile +++ b/Makefile @@ -115,7 +115,7 @@ build-client-python: make run-in-docker sdk_language=python image=busybox:${BUSYBOX_DOCKER_TAG} command="/bin/sh -c 'patch -p1 /module/openfga_sdk/api/open_fga_api.py /config/clients/python/patches/open_fga_api.py.patch'" make run-in-docker sdk_language=python image=busybox:${BUSYBOX_DOCKER_TAG} command="/bin/sh -c 'patch -p1 /module/openfga_sdk/sync/open_fga_api.py /config/clients/python/patches/open_fga_api_sync.py.patch'" make run-in-docker sdk_language=python image=busybox:${BUSYBOX_DOCKER_TAG} command="/bin/sh -c 'patch -p1 /module/docs/OpenFgaApi.md /config/clients/python/patches/OpenFgaApi.md.patch'" - make run-in-docker sdk_language=python image=python:${PYTHON_DOCKER_TAG} command="/bin/sh -c 'python -m pip install pyupgrade==3.15.1 isort==5.13.2 black==24.2.0 autoflake==2.3.0; pyupgrade \`find . -name *.py -type f\` --py310-plus --keep-runtime-typing; isort . --profile black; black .; autoflake --exclude=__init__.py --in-place --remove-unused-variables --remove-all-unused-imports -r .'" + make run-in-docker sdk_language=python image=python:${PYTHON_DOCKER_TAG} command="/bin/sh -c 'python -m pip install pyupgrade==3.15.2 isort==5.13.2 black==24.4.2 autoflake==2.3.1; pyupgrade \`find . -name *.py -type f\` --py310-plus --keep-runtime-typing; isort . --profile black; autoflake --exclude=__init__.py --in-place --remove-unused-variables --remove-all-unused-imports -r .; black .'" make run-in-docker sdk_language=python image=python:${PYTHON_DOCKER_TAG} command="/bin/sh -c 'pip install setuptools wheel && python setup.py sdist bdist_wheel'" .PHONY: test-client-python diff --git a/config/clients/python/CHANGELOG.md.mustache b/config/clients/python/CHANGELOG.md.mustache index fb55a641..924464cd 100644 --- a/config/clients/python/CHANGELOG.md.mustache +++ b/config/clients/python/CHANGELOG.md.mustache @@ -1,5 +1,14 @@ # Changelog +## v0.5.0 + +### [0.5.0](https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/compare/v0.4.2...v0.5.0) (2024-06-17) + +- fix: ClientTuple condition property type +- fix: list_users should accept FgaObject type +- fix: remove ReadAuthorizationModel calls from BatchCheck and writes +- chore!: remove excluded users from ListUsers response + ## v0.4.3 ### [0.4.3](https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/compare/v0.4.2...v0.4.3) (2024-06-07) diff --git a/config/clients/python/config.overrides.json b/config/clients/python/config.overrides.json index 23705d05..c6e1e777 100644 --- a/config/clients/python/config.overrides.json +++ b/config/clients/python/config.overrides.json @@ -2,7 +2,7 @@ "sdkId": "python", "gitRepoId": "python-sdk", "packageName": "openfga_sdk", - "packageVersion": "0.4.3", + "packageVersion": "0.5.0", "packageDescription": "Python SDK for OpenFGA", "packageDetailedDescription": "This is an autogenerated python SDK for OpenFGA. It provides a wrapper around the [OpenFGA API definition](https://openfga.dev/api).", "fossaComplianceNoticeId": "2f8a8629-b46c-435e-b8cd-1174a674fb4b", diff --git a/config/clients/python/template/.github/dependabot.yaml b/config/clients/python/template/.github/dependabot.yaml index 959bdad8..5c2a0d6a 100644 --- a/config/clients/python/template/.github/dependabot.yaml +++ b/config/clients/python/template/.github/dependabot.yaml @@ -10,6 +10,14 @@ updates: - "*" ignore: - dependency-name: "urllib3" + - package-ecosystem: "pip" + directory: "/example/example1" + schedule: + interval: "monthly" + groups: + dependencies: + patterns: + - "*" - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/config/clients/python/template/.github/workflows/main.yaml.mustache b/config/clients/python/template/.github/workflows/main.yaml.mustache index b2baeacd..7a936518 100644 --- a/config/clients/python/template/.github/workflows/main.yaml.mustache +++ b/config/clients/python/template/.github/workflows/main.yaml.mustache @@ -22,7 +22,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 with: fetch-depth: 0 @@ -60,7 +60,7 @@ jobs: - if: matrix.python-version == '3.10' name: Upload coverage to Codecov - uses: codecov/codecov-action@84508663e988701840491b86de86b666e8a86bed # v4.3.0 + uses: codecov/codecov-action@125fc84a9a348dbcf27191600683ec096ec9021c # v4.4.1 continue-on-error: true with: token: ${{ secrets.CODECOV_TOKEN }} @@ -75,7 +75,7 @@ jobs: id-token: write # Required for PyPI trusted publishing steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 with: fetch-depth: 0 @@ -106,7 +106,7 @@ jobs: needs: [publish] steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 with: fetch-depth: 0 diff --git a/config/clients/python/template/README_calling_api.mustache b/config/clients/python/template/README_calling_api.mustache index 13a1c32b..854c382b 100644 --- a/config/clients/python/template/README_calling_api.mustache +++ b/config/clients/python/template/README_calling_api.mustache @@ -684,6 +684,7 @@ List the users who have a certain relation to a particular type. ```python from openfga_sdk import OpenFgaClient +from openfga_sdk.models.fga_object import FgaObject from openfga_sdk.client.models import ClientListUsersRequest, ClientTuple configuration = ClientConfiguration( @@ -697,7 +698,7 @@ async with OpenFgaClient(configuration) as api_client: } request = ClientListUsersRequest( - object="document:2021-budget", + object=FgaObject(type="document", id="2021-budget"), relation="can_read", user_filters=[ UserTypeFilter(type="user"), diff --git a/config/clients/python/template/client/client.mustache b/config/clients/python/template/client/client.mustache index 6a1c8b66..ba775418 100644 --- a/config/clients/python/template/client/client.mustache +++ b/config/clients/python/template/client/client.mustache @@ -24,7 +24,11 @@ from {{packageName}}.client.models.list_users_request import ClientListUsersRequ from {{packageName}}.client.models.write_single_response import construct_write_single_response from {{packageName}}.client.models.write_transaction_opts import WriteTransactionOpts from {{packageName}}.client.models.read_changes_request import ClientReadChangesRequest -from {{packageName}}.exceptions import FgaValidationException +from {{packageName}}.exceptions import ( + AuthenticationError, + FgaValidationException, + UnauthorizedException, +) from {{packageName}}.models.assertion import Assertion from {{packageName}}.models.check_request import CheckRequest from {{packageName}}.models.contextual_tuple_keys import ContextualTupleKeys @@ -165,16 +169,6 @@ class OpenFgaClient: """ return self._client_configuration.authorization_model_id - {{#asyncio}}async {{/asyncio}}def _check_valid_api_connection(self, options: dict[str, int | str]): - """ - Checks that a connection with the given configuration can be established - """ - authorization_model_id = self._get_authorization_model_id(options) - if authorization_model_id is not None and authorization_model_id != "": - {{#asyncio}}await {{/asyncio}}self.read_authorization_model(options) - else: - {{#asyncio}}await {{/asyncio}}self.read_latest_authorization_model(options) - ################# # Stores ################# @@ -380,6 +374,8 @@ class OpenFgaClient: delete_batch = batch {{#asyncio}}await {{/asyncio}}self._write_with_transaction(ClientWriteRequest(writes=write_batch, deletes=delete_batch), options) return [construct_write_single_response(i, True, None) for i in batch] + except (AuthenticationError, UnauthorizedException) as err: + raise err except Exception as err: return [construct_write_single_response(i, False, err) for i in batch] @@ -449,8 +445,6 @@ class OpenFgaClient: return results options = set_heading_if_not_set(options, CLIENT_BULK_REQUEST_ID_HEADER, str(uuid.uuid4())) - # TODO: this should be run in parallel - {{#asyncio}}await {{/asyncio}}self._check_valid_api_connection(options) # otherwise, it is not a transaction and it is a batch write requests writes_response = None @@ -533,6 +527,8 @@ class OpenFgaClient: try: api_response = {{#asyncio}}await {{/asyncio}}self.check(body, options) return BatchCheckResponse(allowed=api_response.allowed, request=body, response=api_response, error=None) + except (AuthenticationError, UnauthorizedException) as err: + raise err except Exception as err: return BatchCheckResponse(allowed=False, request=body, response=None, error=err) {{#asyncio}} @@ -554,9 +550,6 @@ class OpenFgaClient: options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "BatchCheck") options = set_heading_if_not_set(options, CLIENT_BULK_REQUEST_ID_HEADER, str(uuid.uuid4())) - # TODO: this should be run in parallel - {{#asyncio}}await {{/asyncio}}self._check_valid_api_connection(options) - max_parallel_requests = {{ clientMaxMethodParallelRequests }} if options is not None and "max_parallel_requests" in options: max_parallel_requests = options["max_parallel_requests"] diff --git a/config/clients/python/template/client/client_sync.mustache b/config/clients/python/template/client/client_sync.mustache index 25c7c5b3..19a33774 100644 --- a/config/clients/python/template/client/client_sync.mustache +++ b/config/clients/python/template/client/client_sync.mustache @@ -16,7 +16,11 @@ from {{packageName}}.client.models.list_users_request import ClientListUsersRequ from {{packageName}}.client.models.write_single_response import construct_write_single_response from {{packageName}}.client.models.write_transaction_opts import WriteTransactionOpts from {{packageName}}.client.models.read_changes_request import ClientReadChangesRequest -from {{packageName}}.exceptions import FgaValidationException +from {{packageName}}.exceptions import ( + AuthenticationError, + FgaValidationException, + UnauthorizedException, +) from {{packageName}}.models.assertion import Assertion from {{packageName}}.models.check_request import CheckRequest from {{packageName}}.models.contextual_tuple_keys import ContextualTupleKeys @@ -151,16 +155,6 @@ class OpenFgaClient: """ return self._client_configuration.authorization_model_id - def _check_valid_api_connection(self, options: dict[str, int | str]): - """ - Checks that a connection with the given configuration can be established - """ - authorization_model_id = self._get_authorization_model_id(options) - if authorization_model_id is not None and authorization_model_id != "": - self.read_authorization_model(options) - else: - self.read_latest_authorization_model(options) - ################# # Stores ################# @@ -366,6 +360,8 @@ class OpenFgaClient: delete_batch = batch self._write_with_transaction(ClientWriteRequest(writes=write_batch, deletes=delete_batch), options) return [construct_write_single_response(i, True, None) for i in batch] + except (AuthenticationError, UnauthorizedException) as err: + raise err except Exception as err: return [construct_write_single_response(i, False, err) for i in batch] @@ -429,8 +425,6 @@ class OpenFgaClient: return results options = set_heading_if_not_set(options, CLIENT_BULK_REQUEST_ID_HEADER, str(uuid.uuid4())) - # TODO: this should be run in parallel - self._check_valid_api_connection(options) # otherwise, it is not a transaction and it is a batch write requests writes_response = None @@ -512,6 +506,8 @@ class OpenFgaClient: try: api_response = self.check(body, options) return BatchCheckResponse(allowed=api_response.allowed, request=body, response=api_response, error=None) + except (AuthenticationError, UnauthorizedException) as err: + raise err except Exception as err: return BatchCheckResponse(allowed=False, request=body, response=None, error=err) @@ -529,9 +525,6 @@ class OpenFgaClient: options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "BatchCheck") options = set_heading_if_not_set(options, CLIENT_BULK_REQUEST_ID_HEADER, str(uuid.uuid4())) - # TODO: this should be run in parallel - self._check_valid_api_connection(options) - max_parallel_requests = {{ clientMaxMethodParallelRequests }} if options is not None and "max_parallel_requests" in options: max_parallel_requests = options["max_parallel_requests"] diff --git a/config/clients/python/template/client/models/list_users_request.mustache b/config/clients/python/template/client/models/list_users_request.mustache index 1fa114ed..3c83e3e1 100644 --- a/config/clients/python/template/client/models/list_users_request.mustache +++ b/config/clients/python/template/client/models/list_users_request.mustache @@ -1,6 +1,7 @@ {{>partial_header}} from {{packageName}}.client.models.tuple import ClientTuple +from {{packageName}}.models.fga_object import FgaObject from {{packageName}}.models.user_type_filter import UserTypeFilter @@ -11,7 +12,7 @@ class ClientListUsersRequest: def __init__( self, - object: str = None, + object: FgaObject = None, relation: str = None, user_filters: list[UserTypeFilter] = None, contextual_tuples: list[ClientTuple] = None, @@ -24,7 +25,7 @@ class ClientListUsersRequest: self._context = context @property - def object(self): + def object(self) -> FgaObject: """Gets the object of this ClientListUsersRequest. @@ -34,7 +35,7 @@ class ClientListUsersRequest: return self._object @object.setter - def object(self, object): + def object(self, object: FgaObject): """Sets the object of this ClientListUsersRequest. @@ -45,7 +46,7 @@ class ClientListUsersRequest: self._object = object @property - def relation(self): + def relation(self) -> str: """Gets the relation of this ClientListUsersRequest. @@ -55,7 +56,7 @@ class ClientListUsersRequest: return self._relation @relation.setter - def relation(self, relation): + def relation(self, relation: str): """Sets the relation of this ClientListUsersRequest. @@ -66,7 +67,7 @@ class ClientListUsersRequest: self._relation = relation @property - def user_filters(self): + def user_filters(self) -> list[UserTypeFilter]: """Gets the user_filters of this ClientListUsersRequest. @@ -76,7 +77,7 @@ class ClientListUsersRequest: return self._user_filters @user_filters.setter - def user_filters(self, user_filters): + def user_filters(self, user_filters: list[UserTypeFilter]): """Sets the user_filters of this ClientListUsersRequest. @@ -87,7 +88,7 @@ class ClientListUsersRequest: self._user_filters = user_filters @property - def contextual_tuples(self): + def contextual_tuples(self) -> list[ClientTuple]: """Gets the contextual_tuples of this ClientListUsersRequest. @@ -97,7 +98,7 @@ class ClientListUsersRequest: return self._contextual_tuples @contextual_tuples.setter - def contextual_tuples(self, contextual_tuples): + def contextual_tuples(self, contextual_tuples: list[ClientTuple]): """Sets the contextual_tuples of this ClientListUsersRequest. @@ -108,7 +109,7 @@ class ClientListUsersRequest: self._contextual_tuples = contextual_tuples @property - def context(self): + def context(self) -> object: """Gets the context of this ClientListUsersRequest. Additional request context that will be used to evaluate any ABAC conditions encountered in the query evaluation. @@ -119,7 +120,7 @@ class ClientListUsersRequest: return self._context @context.setter - def context(self, context): + def context(self, context: object): """Sets the context of this ClientListUsersRequest. Additional request context that will be used to evaluate any ABAC conditions encountered in the query evaluation. diff --git a/config/clients/python/template/client/models/tuple.mustache b/config/clients/python/template/client/models/tuple.mustache index 77720075..d5e75def 100644 --- a/config/clients/python/template/client/models/tuple.mustache +++ b/config/clients/python/template/client/models/tuple.mustache @@ -8,7 +8,13 @@ class ClientTuple: ClientTuple encapsulates the client tuple """ - def __init__(self, user: str, relation: str, object: str, condition: RelationshipCondition = None): + def __init__( + self, + user: str, + relation: str, + object: str, + condition: RelationshipCondition | None = None, + ): self._user = user self._relation = relation self._object = object diff --git a/config/clients/python/template/client/test_client.mustache b/config/clients/python/template/client/test_client.mustache index e18321dc..3e4bce32 100644 --- a/config/clients/python/template/client/test_client.mustache +++ b/config/clients/python/template/client/test_client.mustache @@ -28,6 +28,7 @@ from {{packageName}}.models.check_response import CheckResponse from {{packageName}}.models.create_store_request import CreateStoreRequest from {{packageName}}.models.create_store_response import CreateStoreResponse from {{packageName}}.models.expand_response import ExpandResponse +from {{packageName}}.models.fga_object import FgaObject from {{packageName}}.models.get_store_response import GetStoreResponse from {{packageName}}.models.leaf import Leaf from {{packageName}}.models.list_objects_response import ListObjectsResponse @@ -864,7 +865,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): mock_response('{}', 200), mock_response('{}', 200), mock_response('{}', 200), - mock_response('{}', 200), ] configuration = self.configuration configuration.store_id = store_id @@ -925,15 +925,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): error=None) ] ) - self.assertEqual(mock_request.call_count, 4) - mock_request.assert_any_call( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J', - headers=ANY, - query_params=[], - _preload_content=ANY, - _request_timeout=None - ) + self.assertEqual(mock_request.call_count, 3) mock_request.assert_any_call( 'POST', 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', @@ -975,7 +967,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): mock_response('{}', 200), mock_response('{}', 200), mock_response('{}', 200), - mock_response('{}', 200), ] configuration = self.configuration configuration.store_id = store_id @@ -1036,15 +1027,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): error=None) ] ) - self.assertEqual(mock_request.call_count, 4) - mock_request.assert_any_call( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J', - headers=ANY, - query_params=[], - _preload_content=ANY, - _request_timeout=None - ) + self.assertEqual(mock_request.call_count, 3) mock_request.assert_any_call( 'POST', 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', @@ -1086,7 +1069,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): mock_request.side_effect = [ mock_response('{}', 200), mock_response('{}', 200), - mock_response('{}', 200), ] configuration = self.configuration configuration.store_id = store_id @@ -1147,15 +1129,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): error=None) ] ) - self.assertEqual(mock_request.call_count, 3) - mock_request.assert_any_call( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J', - headers=ANY, - query_params=[], - _preload_content=ANY, - _request_timeout=None - ) + self.assertEqual(mock_request.call_count, 2) mock_request.assert_any_call( 'POST', 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', @@ -1191,7 +1165,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ''' mock_request.side_effect = [ - mock_response('{}', 200), mock_response('{}', 200), ValidationException(http_resp=http_mock_response(response_body, 400)), mock_response('{}', 200), @@ -1255,15 +1228,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ), success=True, error=None)) - self.assertEqual(mock_request.call_count, 4) - mock_request.assert_any_call( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J', - headers=ANY, - query_params=[], - _preload_content=ANY, - _request_timeout=None - ) + self.assertEqual(mock_request.call_count, 3) mock_request.assert_any_call( 'POST', 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', @@ -1303,7 +1268,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): """ mock_request.side_effect = [ mock_response('{}', 200), - mock_response('{}', 200), ] configuration = self.configuration configuration.store_id = store_id @@ -1325,14 +1289,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): body, options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "transaction": transaction} ) - mock_request.assert_any_call( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J', - headers=ANY, - query_params=[], - _preload_content=ANY, - _request_timeout=None - ) mock_request.assert_any_call( 'POST', 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', @@ -1441,7 +1397,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: - with self.assertRaises(UnauthorizedException) as api_exception: + with self.assertRaises(UnauthorizedException): body = ClientWriteRequest( writes=[ ClientTuple( @@ -1459,17 +1415,29 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "transaction": transaction} ) - self.assertIsInstance(api_exception.exception, UnauthorizedException) mock_request.assert_called() self.assertEqual(mock_request.call_count, 1) mock_request.assert_called_once_with( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J', + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write", headers=ANY, query_params=[], + post_params=[], + body={ + "writes": { + "tuple_keys": [ + { + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + "relation": "reader", + "object": "document:2021-budget", + } + ] + }, + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + }, _preload_content=ANY, - _request_timeout=None + _request_timeout=ANY, ) {{#asyncio}}await {{/asyncio}}api_client.close() @@ -1568,7 +1536,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): # First, mock the response response_body = '{"allowed": true, "resolution": "1234"}' mock_request.side_effect = [ - mock_response('{}', 200), mock_response(response_body, 200), ] body = ClientCheckRequest( @@ -1589,14 +1556,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): self.assertTrue(api_response[0].allowed) self.assertEqual(api_response[0].request, body) # Make sure the API was called with the right data - mock_request.assert_any_call( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01GXSA8YR785C4FYS3C0RTG7B1', - headers=ANY, - query_params=[], - _preload_content=ANY, - _request_timeout=None - ) mock_request.assert_any_call( 'POST', 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', @@ -1619,7 +1578,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): # First, mock the response mock_request.side_effect = [ - mock_response('{}', 200), mock_response('{"allowed": true, "resolution": "1234"}', 200), mock_response('{"allowed": false, "resolution": "1234"}', 200), mock_response('{"allowed": true, "resolution": "1234"}', 200), @@ -1658,14 +1616,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): self.assertTrue(api_response[2].allowed) self.assertEqual(api_response[2].request, body3) # Make sure the API was called with the right data - mock_request.assert_any_call( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01GXSA8YR785C4FYS3C0RTG7B1', - headers=ANY, - query_params=[], - _preload_content=ANY, - _request_timeout=None - ) mock_request.assert_any_call( 'POST', 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', @@ -1717,7 +1667,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): # First, mock the response mock_request.side_effect = [ - mock_response('{}', 200), mock_response('{"allowed": true, "resolution": "1234"}', 200), ValidationException(http_resp=http_mock_response(response_body, 400)), mock_response('{"allowed": false, "resolution": "1234"}', 200), @@ -1757,14 +1706,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): self.assertFalse(api_response[2].allowed) self.assertEqual(api_response[2].request, body3) # Make sure the API was called with the right data - mock_request.assert_any_call( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01GXSA8YR785C4FYS3C0RTG7B1', - headers=ANY, - query_params=[], - _preload_content=ANY, - _request_timeout=None - ) mock_request.assert_any_call( 'POST', 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', @@ -1938,7 +1879,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): # First, mock the response mock_request.side_effect = [ - mock_response('{}', 200), mock_response('{"allowed": true, "resolution": "1234"}', 200), mock_response('{"allowed": false, "resolution": "1234"}', 200), mock_response('{"allowed": true, "resolution": "1234"}', 200), @@ -1955,14 +1895,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): self.assertEqual(api_response, ["reader", "viewer"]) # Make sure the API was called with the right data - mock_request.assert_any_call( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01GXSA8YR785C4FYS3C0RTG7B1', - headers=ANY, - query_params=[], - _preload_content=ANY, - _request_timeout=None - ) mock_request.assert_any_call( 'POST', 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', @@ -2009,7 +1941,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: - with self.assertRaises(UnauthorizedException) as api_exception: + with self.assertRaises(UnauthorizedException): {{#asyncio}}await {{/asyncio}}api_client.list_relations( body=ClientListRelationsRequest(user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relations=["reader", "owner", "viewer"], @@ -2017,18 +1949,9 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"} ) - self.assertIsInstance(api_exception.exception, UnauthorizedException) mock_request.assert_called() - self.assertEqual(mock_request.call_count, 1) + self.assertEqual(mock_request.call_count, 3) - mock_request.assert_called_once_with( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01GXSA8YR785C4FYS3C0RTG7B1', - headers=ANY, - query_params=[], - _preload_content=ANY, - _request_timeout=None - ) {{#asyncio}}await {{/asyncio}}api_client.close() @@ -2068,7 +1991,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: body = ClientListUsersRequest( - object="document:2021-budget", + object=FgaObject(type="document", id="2021-budget"), relation="can_read", user_filters=[ UserTypeFilter(type="user"), @@ -2125,7 +2048,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): post_params=[], body={ "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", - "object": "document:2021-budget", + "object": {"id": "2021-budget", "type": "document"}, "relation": "can_read", "user_filters": [ {"type": "user"}, diff --git a/config/clients/python/template/client/test_client_sync.mustache b/config/clients/python/template/client/test_client_sync.mustache index f64a16fe..87b81da0 100644 --- a/config/clients/python/template/client/test_client_sync.mustache +++ b/config/clients/python/template/client/test_client_sync.mustache @@ -26,6 +26,7 @@ from {{packageName}}.models.check_response import CheckResponse from {{packageName}}.models.create_store_request import CreateStoreRequest from {{packageName}}.models.create_store_response import CreateStoreResponse from {{packageName}}.models.expand_response import ExpandResponse +from {{packageName}}.models.fga_object import FgaObject from {{packageName}}.models.get_store_response import GetStoreResponse from {{packageName}}.models.leaf import Leaf from {{packageName}}.models.list_objects_response import ListObjectsResponse @@ -864,7 +865,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): mock_response('{}', 200), mock_response('{}', 200), mock_response('{}', 200), - mock_response('{}', 200), ] configuration = self.configuration configuration.store_id = store_id @@ -925,15 +925,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): error=None) ] ) - self.assertEqual(mock_request.call_count, 4) - mock_request.assert_any_call( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J', - headers=ANY, - query_params=[], - _preload_content=ANY, - _request_timeout=None - ) + self.assertEqual(mock_request.call_count, 3) mock_request.assert_any_call( 'POST', 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', @@ -975,7 +967,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): mock_response('{}', 200), mock_response('{}', 200), mock_response('{}', 200), - mock_response('{}', 200), ] configuration = self.configuration configuration.store_id = store_id @@ -1036,15 +1027,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): error=None) ] ) - self.assertEqual(mock_request.call_count, 4) - mock_request.assert_any_call( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J', - headers=ANY, - query_params=[], - _preload_content=ANY, - _request_timeout=None - ) + self.assertEqual(mock_request.call_count, 3) mock_request.assert_any_call( 'POST', 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', @@ -1147,15 +1130,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): error=None) ] ) - self.assertEqual(mock_request.call_count, 3) - mock_request.assert_any_call( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J', - headers=ANY, - query_params=[], - _preload_content=ANY, - _request_timeout=None - ) + self.assertEqual(mock_request.call_count, 2) mock_request.assert_any_call( 'POST', 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', @@ -1191,7 +1166,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ''' mock_request.side_effect = [ - mock_response('{}', 200), mock_response('{}', 200), ValidationException(http_resp=http_mock_response(response_body, 400)), mock_response('{}', 200), @@ -1255,15 +1229,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ), success=True, error=None)) - self.assertEqual(mock_request.call_count, 4) - mock_request.assert_any_call( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J', - headers=ANY, - query_params=[], - _preload_content=ANY, - _request_timeout=None - ) + self.assertEqual(mock_request.call_count, 3) mock_request.assert_any_call( 'POST', 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', @@ -1303,7 +1269,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): """ mock_request.side_effect = [ mock_response('{}', 200), - mock_response('{}', 200), ] configuration = self.configuration configuration.store_id = store_id @@ -1325,14 +1290,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): body, options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "transaction": transaction} ) - mock_request.assert_any_call( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J', - headers=ANY, - query_params=[], - _preload_content=ANY, - _request_timeout=None - ) mock_request.assert_any_call( 'POST', 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', @@ -1441,7 +1398,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id with OpenFgaClient(configuration) as api_client: - with self.assertRaises(UnauthorizedException) as api_exception: + with self.assertRaises(UnauthorizedException): body = ClientWriteRequest( writes=[ ClientTuple( @@ -1459,19 +1416,31 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "transaction": transaction} ) - self.assertIsInstance(api_exception.exception, UnauthorizedException) - mock_request.assert_called() - self.assertEqual(mock_request.call_count, 1) - - mock_request.assert_called_once_with( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J', - headers=ANY, - query_params=[], - _preload_content=ANY, - _request_timeout=None - ) - api_client.close() + mock_request.assert_called() + self.assertEqual(mock_request.call_count, 1) + + mock_request.assert_called_once_with( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write", + headers=ANY, + query_params=[], + post_params=[], + body={ + "writes": { + "tuple_keys": [ + { + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + "relation": "reader", + "object": "document:2021-budget", + } + ] + }, + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + }, + _preload_content=ANY, + _request_timeout=ANY, + ) + api_client.close() @patch.object(rest.RESTClientObject, 'request') def test_check(self, mock_request): @@ -1568,7 +1537,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): # First, mock the response response_body = '{"allowed": true, "resolution": "1234"}' mock_request.side_effect = [ - mock_response('{}', 200), mock_response(response_body, 200), ] body = ClientCheckRequest( @@ -1589,14 +1557,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): self.assertTrue(api_response[0].allowed) self.assertEqual(api_response[0].request, body) # Make sure the API was called with the right data - mock_request.assert_any_call( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01GXSA8YR785C4FYS3C0RTG7B1', - headers=ANY, - query_params=[], - _preload_content=ANY, - _request_timeout=None - ) mock_request.assert_any_call( 'POST', 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', @@ -1619,7 +1579,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): # First, mock the response mock_request.side_effect = [ - mock_response('{}', 200), mock_response('{"allowed": true, "resolution": "1234"}', 200), mock_response('{"allowed": false, "resolution": "1234"}', 200), mock_response('{"allowed": true, "resolution": "1234"}', 200), @@ -1658,14 +1617,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): self.assertTrue(api_response[2].allowed) self.assertEqual(api_response[2].request, body3) # Make sure the API was called with the right data - mock_request.assert_any_call( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01GXSA8YR785C4FYS3C0RTG7B1', - headers=ANY, - query_params=[], - _preload_content=ANY, - _request_timeout=None - ) mock_request.assert_any_call( 'POST', 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', @@ -1717,7 +1668,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): # First, mock the response mock_request.side_effect = [ - mock_response('{}', 200), mock_response('{"allowed": true, "resolution": "1234"}', 200), ValidationException(http_resp=http_mock_response(response_body, 400)), mock_response('{"allowed": false, "resolution": "1234"}', 200), @@ -1757,14 +1707,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): self.assertFalse(api_response[2].allowed) self.assertEqual(api_response[2].request, body3) # Make sure the API was called with the right data - mock_request.assert_any_call( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01GXSA8YR785C4FYS3C0RTG7B1', - headers=ANY, - query_params=[], - _preload_content=ANY, - _request_timeout=None - ) mock_request.assert_any_call( 'POST', 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', @@ -1938,7 +1880,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): # First, mock the response mock_request.side_effect = [ - mock_response('{}', 200), mock_response('{"allowed": true, "resolution": "1234"}', 200), mock_response('{"allowed": false, "resolution": "1234"}', 200), mock_response('{"allowed": true, "resolution": "1234"}', 200), @@ -1955,14 +1896,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): self.assertEqual(api_response, ["reader", "viewer"]) # Make sure the API was called with the right data - mock_request.assert_any_call( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01GXSA8YR785C4FYS3C0RTG7B1', - headers=ANY, - query_params=[], - _preload_content=ANY, - _request_timeout=None - ) mock_request.assert_any_call( 'POST', 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', @@ -2019,16 +1952,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): self.assertIsInstance(api_exception.exception, UnauthorizedException) mock_request.assert_called() - self.assertEqual(mock_request.call_count, 1) - - mock_request.assert_called_once_with( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01GXSA8YR785C4FYS3C0RTG7B1', - headers=ANY, - query_params=[], - _preload_content=ANY, - _request_timeout=None - ) + self.assertEqual(mock_request.call_count, 3) api_client.close() @@ -2068,7 +1992,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): with OpenFgaClient(configuration) as api_client: body = ClientListUsersRequest() - body.object = "document:2021-budget" + body.object = FgaObject(type="document", id="2021-budget") body.relation = "can_read" body.user_filters = [ UserTypeFilter(type="user"), @@ -2124,7 +2048,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): post_params=[], body={ "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", - "object": "document:2021-budget", + "object": {"id": "2021-budget", "type": "document"}, "relation": "can_read", "user_filters": [ {"type": "user"}, diff --git a/config/clients/python/template/example/example1/example1.py b/config/clients/python/template/example/example1/example1.py index 92d79f54..3ae7f880 100644 --- a/config/clients/python/template/example/example1/example1.py +++ b/config/clients/python/template/example/example1/example1.py @@ -28,7 +28,9 @@ ClientWriteRequest, WriteTransactionOpts, ) +from openfga_sdk.client.models.list_users_request import ClientListUsersRequest from openfga_sdk.credentials import CredentialConfiguration, Credentials +from openfga_sdk.models.fga_object import FgaObject async def main(): @@ -264,7 +266,7 @@ async def main(): print(f"Allowed: {response.allowed}") # List objects with context - print("Listing objects for access with context") + print("Listing objects for access with context") response = await fga_client.list_objects( ClientListObjectsRequest( @@ -301,6 +303,20 @@ async def main(): ) print(f"Relations: {response}") + # ListUsers + print("Listing user who have access to object") + + response = await fga_client.list_objects( + ClientListUsersRequest( + relation="viewer", + object=FgaObject(type="document", id="roadmap"), + user_filters=[ + FgaObject(type="user"), + ], + ) + ) + print(f"Users: {response.objects}") + # WriteAssertions await fga_client.write_assertions( [ diff --git a/config/clients/python/template/gitignore_custom.mustache b/config/clients/python/template/gitignore_custom.mustache index 914543f6..ae5ff0ce 100644 --- a/config/clients/python/template/gitignore_custom.mustache +++ b/config/clients/python/template/gitignore_custom.mustache @@ -67,3 +67,6 @@ target/ #Ipython Notebook .ipynb_checkpoints + +# Python tooling +setup.local.cfg