diff --git a/Makefile b/Makefile index 7bc56c6c..4ba5ab93 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # Main config -OPENFGA_DOCKER_TAG = v1.7.0 -OPEN_API_REF ?= 7c098f10acd22137c659c407818a4e0880044afe +OPENFGA_DOCKER_TAG = v1.8.1 +OPEN_API_REF ?= 0bb89b73d6550b627f79c53b4b97dec1ee3fe0ad OPEN_API_URL = https://raw.githubusercontent.com/openfga/api/${OPEN_API_REF}/docs/openapiv2/apidocs.swagger.json OPENAPI_GENERATOR_CLI_DOCKER_TAG = v6.4.0 NODE_DOCKER_TAG = 20-alpine diff --git a/config/clients/dotnet/CHANGELOG.md.mustache b/config/clients/dotnet/CHANGELOG.md.mustache index 0d65388b..4d731b49 100644 --- a/config/clients/dotnet/CHANGELOG.md.mustache +++ b/config/clients/dotnet/CHANGELOG.md.mustache @@ -2,6 +2,8 @@ ## [Unreleased](https://github.com/openfga/dotnet-sdk/compare/v{{packageVersion}}...HEAD) +- feat: add support for `start_time` parameter in `ReadChanges` endpoint + ## v0.5.1 ### [0.5.1](https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/compare/v0.5.0...v0.5.1) (2024-09-09) diff --git a/config/clients/dotnet/template/Client/Client.mustache b/config/clients/dotnet/template/Client/Client.mustache index be11e58a..46abf292 100644 --- a/config/clients/dotnet/template/Client/Client.mustache +++ b/config/clients/dotnet/template/Client/Client.mustache @@ -303,7 +303,7 @@ public class {{appShortName}}Client : IDisposable { /** * BatchCheck - Run a set of checks (evaluates) */ - public async Task BatchCheck(List body, + public async Task BatchCheck(List body, IClientBatchCheckOptions? options = default, CancellationToken cancellationToken = default) { var responses = new ConcurrentBag(); @@ -321,7 +321,7 @@ public class {{appShortName}}Client : IDisposable { } }); - return new BatchCheckResponse {Responses = responses.ToList()}; + return new ClientBatchCheckClientResponse {Responses = responses.ToList()}; } /** diff --git a/config/clients/dotnet/template/Client/Model/ClientBatchCheckResponse.mustache b/config/clients/dotnet/template/Client/Model/ClientBatchCheckResponse.mustache index 7c36a46e..5bf73a40 100644 --- a/config/clients/dotnet/template/Client/Model/ClientBatchCheckResponse.mustache +++ b/config/clients/dotnet/template/Client/Model/ClientBatchCheckResponse.mustache @@ -52,19 +52,19 @@ public class BatchCheckSingleResponse : IEquatable, IV /// /// CheckResponse /// -[DataContract(Name = "BatchCheckResponse")] -public class BatchCheckResponse : IEquatable, IValidatableObject { +[DataContract(Name = "ClientBatchCheckClientResponse")] +public class ClientBatchCheckClientResponse : IEquatable, IValidatableObject { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public BatchCheckResponse() { + public ClientBatchCheckClientResponse() { Responses = new List(); } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public BatchCheckResponse(List responses) { + public ClientBatchCheckClientResponse(List responses) { Responses = responses; } @@ -72,7 +72,7 @@ public class BatchCheckResponse : IEquatable, IValidatableOb [JsonPropertyName("responses")] public List Responses { get; set; } - public bool Equals(BatchCheckResponse? other) => throw new NotImplementedException(); + public bool Equals(ClientBatchCheckClientResponse? other) => throw new NotImplementedException(); public IEnumerable Validate(ValidationContext validationContext) => throw new NotImplementedException(); diff --git a/config/clients/dotnet/template/OpenFgaClientTests.mustache b/config/clients/dotnet/template/OpenFgaClientTests.mustache index 8d5733d0..e03cc73e 100644 --- a/config/clients/dotnet/template/OpenFgaClientTests.mustache +++ b/config/clients/dotnet/template/OpenFgaClientTests.mustache @@ -1183,7 +1183,7 @@ public class {{appShortName}}ClientTests { ItExpr.IsAny() ); - Assert.IsType(response); + Assert.IsType(response); var allowedResponses = response.Responses.FindAll(res => res.Allowed == true); Assert.Equal(2, allowedResponses.Count); diff --git a/config/clients/go/CHANGELOG.md.mustache b/config/clients/go/CHANGELOG.md.mustache index 82bd61e7..72dfc26e 100644 --- a/config/clients/go/CHANGELOG.md.mustache +++ b/config/clients/go/CHANGELOG.md.mustache @@ -2,6 +2,8 @@ ## [Unreleased](https://github.com/openfga/go-sdk/compare/v{{packageVersion}}...HEAD) +- feat: add support for `start_time` parameter in `ReadChanges` endpoint + ## v0.6.3 ### [0.6.3](https://github.com/openfga/go-sdk/compare/v0.6.2...v0.6.3) (2024-10-22) diff --git a/config/clients/java/CHANGELOG.md.mustache b/config/clients/java/CHANGELOG.md.mustache index ef89090e..402dc964 100644 --- a/config/clients/java/CHANGELOG.md.mustache +++ b/config/clients/java/CHANGELOG.md.mustache @@ -2,6 +2,8 @@ ## [Unreleased](https://github.com/openfga/java-sdk/compare/v{{packageVersion}}...HEAD) +- feat: add support for `start_time` parameter in `ReadChanges` endpoint + ## v0.7.1 ### [0.7.1](https://github.com/openfga/java-sdk/compare/v0.7.0...v0.7.1) (2024-09-23) diff --git a/config/clients/java/template/OpenFgaApiTest.java.mustache b/config/clients/java/template/OpenFgaApiTest.java.mustache index d0ad5f9f..f826f5fe 100644 --- a/config/clients/java/template/OpenFgaApiTest.java.mustache +++ b/config/clients/java/template/OpenFgaApiTest.java.mustache @@ -1424,7 +1424,7 @@ public class OpenFgaApiTest { // Given String postPath = "https://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/expand"; String expectedBody = String.format( - "{\"tuple_key\":{\"relation\":\"%s\",\"object\":\"%s\"},\"authorization_model_id\":\"%s\",\"consistency\":\"%s\"}", + "{\"tuple_key\":{\"relation\":\"%s\",\"object\":\"%s\"},\"authorization_model_id\":\"%s\",\"consistency\":\"%s\",\"contextual_tuples\":null}", DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID, ConsistencyPreference.HIGHER_CONSISTENCY); String responseBody = String.format( "{\"tree\":{\"root\":{\"union\":{\"nodes\":[{\"leaf\":{\"users\":{\"users\":[\"%s\"]}}}]}}}}", diff --git a/config/clients/java/template/client-OpenFgaClientTest.java.mustache b/config/clients/java/template/client-OpenFgaClientTest.java.mustache index d5a98b85..95692c1e 100644 --- a/config/clients/java/template/client-OpenFgaClientTest.java.mustache +++ b/config/clients/java/template/client-OpenFgaClientTest.java.mustache @@ -1829,8 +1829,7 @@ public class OpenFgaClientTest { // Given String postPath = "https://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/expand"; String expectedBody = String.format( - "{\"tuple_key\":{\"relation\":\"%s\",\"object\":\"%s\"},\"authorization_model_id\":\"%s\",\"consistency\":\"%s\"}", - DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID, ConsistencyPreference.HIGHER_CONSISTENCY); + "{\"tuple_key\":{\"relation\":\"%s\",\"object\":\"%s\"},\"authorization_model_id\":\"%s\",\"consistency\":\"%s\",\"contextual_tuples\":null}", DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID, ConsistencyPreference.HIGHER_CONSISTENCY); String responseBody = String.format( "{\"tree\":{\"root\":{\"union\":{\"nodes\":[{\"leaf\":{\"users\":{\"users\":[\"%s\"]}}}]}}}}", DEFAULT_USER); diff --git a/config/clients/js/CHANGELOG.md.mustache b/config/clients/js/CHANGELOG.md.mustache index 6b8c9084..285d77cb 100644 --- a/config/clients/js/CHANGELOG.md.mustache +++ b/config/clients/js/CHANGELOG.md.mustache @@ -3,7 +3,8 @@ ## [Unreleased](https://github.com/openfga/js-sdk/compare/v{{packageVersion}}...HEAD) -fix: error correctly if apiUrl is not provided (#161) +- fix: error correctly if apiUrl is not provided (#161) +- feat: add support for `start_time` parameter in `ReadChanges` endpoint ## v0.7.0 diff --git a/config/clients/python/CHANGELOG.md.mustache b/config/clients/python/CHANGELOG.md.mustache index 697fd575..4d13c355 100644 --- a/config/clients/python/CHANGELOG.md.mustache +++ b/config/clients/python/CHANGELOG.md.mustache @@ -2,6 +2,18 @@ ## [Unreleased](https://github.com/openfga/python-sdk/compare/v{{packageVersion}}...HEAD) +- feat: remove client-side validation - thanks @GMorris-professional (#155) +- feat: add support for `start_time` parameter in `ReadChanges` endpoint (#156) - Note, this feature requires v1.8.0 of OpenFGA or newer +- feat!: add support for `BatchCheck` API (#154) - Note, this feature requires v1.8.2 of OpenFGA or newer +- fix: change default max retry limit to 3 from 15 - thanks @ovindu-a (#155) + +BREAKING CHANGE: + +Usage of the existing batch_check should now use client_batch_check instead, additionally the existing +BatchCheckResponse has been renamed to ClientBatchCheckClientResponse. + +Please see (#154)(https://github.com/openfga/python-sdk/pull/154) for more details on this change. + ## v0.8.1 ### [0.8.1](https://github.com/openfga/python-sdk/compare/v0.8.0...v0.8.1) (2024-11-26) diff --git a/config/clients/python/config.overrides.json b/config/clients/python/config.overrides.json index 9ab5c86c..64e03eff 100644 --- a/config/clients/python/config.overrides.json +++ b/config/clients/python/config.overrides.json @@ -79,10 +79,26 @@ "destinationFilename": "openfga_sdk/client/models/assertion.py", "templateType": "SupportingFiles" }, + "src/client/models/batch_check_item.py.mustache": { + "destinationFilename": "openfga_sdk/client/models/batch_check_item.py", + "templateType": "SupportingFiles" + }, + "src/client/models/batch_check_request.py.mustache": { + "destinationFilename": "openfga_sdk/client/models/batch_check_request.py", + "templateType": "SupportingFiles" + }, "src/client/models/batch_check_response.py.mustache": { "destinationFilename": "openfga_sdk/client/models/batch_check_response.py", "templateType": "SupportingFiles" }, + "src/client/models/batch_check_single_response.py.mustache": { + "destinationFilename": "openfga_sdk/client/models/batch_check_single_response.py", + "templateType": "SupportingFiles" + }, + "src/client/models/client_batch_check_response.py.mustache": { + "destinationFilename": "openfga_sdk/client/models/client_batch_check_response.py", + "templateType": "SupportingFiles" + }, "src/client/models/check_request.py.mustache": { "destinationFilename": "openfga_sdk/client/models/check_request.py", "templateType": "SupportingFiles" diff --git a/config/clients/python/template/README_calling_api.mustache b/config/clients/python/template/README_calling_api.mustache index 9a1e98d5..59c9bd8e 100644 --- a/config/clients/python/template/README_calling_api.mustache +++ b/config/clients/python/template/README_calling_api.mustache @@ -492,9 +492,11 @@ If 429s or 5xxs are encountered, the underlying check will retry up to {{default ```python # from {{packageName}} import OpenFgaClient -# from {{packageName}}.client import ClientCheckRequest -# from {{packageName}}.client.models import ClientTuple - +# from {{packageName}}.client.models import ( +# ClientTuple, +# ClientBatchCheckItem, +# ClientBatchCheckRequest, +# ) # Initialize the fga_client # fga_client = OpenFgaClient(configuration) @@ -502,7 +504,7 @@ options = { # You can rely on the model id set in the configuration or override it for this specific request "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1" } -body = [ClientCheckRequest( +checks = [ClientBatchCheckItem( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="viewer", object="document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", @@ -516,7 +518,7 @@ body = [ClientCheckRequest( context=dict( ViewCount=100 ) -), ClientCheckRequest( +), ClientBatchCheckItem( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="admin", object="document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", @@ -527,20 +529,21 @@ body = [ClientCheckRequest( object="document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", ), ] -), ClientCheckRequest( +), ClientBatchCheckItem( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="creator", object="document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", -), ClientCheckRequest( +), ClientBatchCheckItem( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="deleter", object="document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", )] -response = await fga_client.batch_check(body, options) -# response.responses = [{ +response = await fga_client.batch_check(ClientBatchCheckRequest(checks=checks), options) +# response.result = [{ # allowed: false, -# request: { +# correlation_id: "de3630c2-f9be-4ee5-9441-cb1fbd82ce75", +# tuple: { # user: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", # relation: "viewer", # object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", @@ -555,7 +558,8 @@ response = await fga_client.batch_check(body, options) # } # }, { # allowed: false, -# request: { +# correlation_id: "6d7c7129-9607-480e-bfd0-17c16e46b9ec", +# tuple: { # user: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", # relation: "admin", # object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", @@ -567,14 +571,19 @@ response = await fga_client.batch_check(body, options) # } # }, { # allowed: false, -# request: { +# correlation_id: "210899b9-6bc3-4491-bdd1-d3d79780aa31", +# tuple: { # user: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", # relation: "creator", # object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", # }, -# error: +# error: { +# input_error: "validation_error", +# message: "relation 'document#creator' not found" +# } # }, { # allowed: true, +# correlation_id: "55cc1946-9fc3-4710-bd40-8fe2687ed8da", # request: { # user: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", # relation: "deleter", diff --git a/config/clients/python/template/docs/opentelemetry.md.mustache b/config/clients/python/template/docs/opentelemetry.md.mustache index 7d25b5a1..5ba12d8f 100644 --- a/config/clients/python/template/docs/opentelemetry.md.mustache +++ b/config/clients/python/template/docs/opentelemetry.md.mustache @@ -30,23 +30,24 @@ If you configure the OpenTelemetry SDK, these metrics will be exported and sent ### Supported Attributes -| Attribute Name | Type | Enabled by Default | Description | -| ------------------------------ | ------ | ------------------ | --------------------------------------------------------------------------------- | -| `fga-client.request.client_id` | string | Yes | Client ID associated with the request, if any | -| `fga-client.request.method` | string | Yes | FGA method/action that was performed (e.g., Check, ListObjects) in TitleCase | -| `fga-client.request.model_id` | string | Yes | Authorization model ID that was sent as part of the request, if any | -| `fga-client.request.store_id` | string | Yes | Store ID that was sent as part of the request | -| `fga-client.response.model_id` | string | Yes | Authorization model ID that the FGA server used | -| `fga-client.user` | string | No | User associated with the action of the request for check and list users | -| `http.client.request.duration` | int | No | Duration for the SDK to complete the request, in milliseconds | -| `http.host` | string | Yes | Host identifier of the origin the request was sent to | -| `http.request.method` | string | Yes | HTTP method for the request | -| `http.request.resend_count` | int | Yes | Number of retries attempted, if any | -| `http.response.status_code` | int | Yes | Status code of the response (e.g., `200` for success) | -| `http.server.request.duration` | int | No | Time taken by the FGA server to process and evaluate the request, in milliseconds | -| `url.scheme` | string | Yes | HTTP scheme of the request (`http`/`https`) | -| `url.full` | string | Yes | Full URL of the request | -| `user_agent.original` | string | Yes | User Agent used in the query | +| Attribute Name | Type | Enabled by Default | Description | +| ------------------------------------- | ------ | ------------------ | --------------------------------------------------------------------------------- | +| `fga-client.request.batch_check_size` | int | No | The total size of the `check` list in a `BatchCheck` call | +| `fga-client.request.client_id` | string | Yes | Client ID associated with the request, if any | +| `fga-client.request.method` | string | Yes | FGA method/action that was performed (e.g., Check, ListObjects) in TitleCase | +| `fga-client.request.model_id` | string | Yes | Authorization model ID that was sent as part of the request, if any | +| `fga-client.request.store_id` | string | Yes | Store ID that was sent as part of the request | +| `fga-client.response.model_id` | string | Yes | Authorization model ID that the FGA server used | +| `fga-client.user` | string | No | User associated with the action of the request for check and list users | +| `http.client.request.duration` | int | No | Duration for the SDK to complete the request, in milliseconds | +| `http.host` | string | Yes | Host identifier of the origin the request was sent to | +| `http.request.method` | string | Yes | HTTP method for the request | +| `http.request.resend_count` | int | Yes | Number of retries attempted, if any | +| `http.response.status_code` | int | Yes | Status code of the response (e.g., `200` for success) | +| `http.server.request.duration` | int | No | Time taken by the FGA server to process and evaluate the request, in milliseconds | +| `url.scheme` | string | Yes | HTTP scheme of the request (`http`/`https`) | +| `url.full` | string | Yes | Full URL of the request | +| `user_agent.original` | string | Yes | User Agent used in the query | ## Customizing Reporting diff --git a/config/clients/python/template/example/example1/example1.py.mustache b/config/clients/python/template/example/example1/example1.py.mustache index 6c42ab41..66e931fa 100644 --- a/config/clients/python/template/example/example1/example1.py.mustache +++ b/config/clients/python/template/example/example1/example1.py.mustache @@ -1,5 +1,6 @@ import asyncio import os +import uuid from {{packageName}} import ( ClientConfiguration, @@ -21,6 +22,8 @@ from {{packageName}} import ( ) from {{packageName}}.client.models import ( ClientAssertion, + ClientBatchCheckItem, + ClientBatchCheckRequest, ClientCheckRequest, ClientListObjectsRequest, ClientListRelationsRequest, @@ -266,6 +269,36 @@ async def main(): ) print(f"Allowed: {response.allowed}") + # Performing a BatchCheck + print("Checking for access via BatchCheck") + + anne_cor_id = str(uuid.uuid4()) + response = await fga_client.batch_check( + ClientBatchCheckRequest( + checks=[ + ClientBatchCheckItem( + user="user:anne", + relation="viewer", + object="document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + context=dict(ViewCount=100), + correlation_id=anne_cor_id, # correlation_id is an optional parameter, the SDK will insert a value if not provided. + ), + ClientBatchCheckItem( + user="user:bob", + relation="viewer", + object="document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + context=dict(ViewCount=100), + ), + ] + ) + ) + + for result in response.result: + if result.correlation_id == anne_cor_id: + print(f"Anne allowed: {result.allowed}") + else: + print(f"{result.request.user} allowed: {result.allowed}") + # List objects with context print("Listing objects for access with context") diff --git a/config/clients/python/template/src/api.py.mustache b/config/clients/python/template/src/api.py.mustache index aafc8d39..ceac6233 100644 --- a/config/clients/python/template/src/api.py.mustache +++ b/config/clients/python/template/src/api.py.mustache @@ -298,6 +298,11 @@ class {{classname}}: ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return {{#asyncio}}await ({{/asyncio}}self.api_client.call_api( '{{{path}}}'.replace('{store_id}', store_id), '{{httpMethod}}', path_params, diff --git a/config/clients/python/template/src/client/client.py.mustache b/config/clients/python/template/src/client/client.py.mustache index b79f9f11..9b569f5d 100644 --- a/config/clients/python/template/src/client/client.py.mustache +++ b/config/clients/python/template/src/client/client.py.mustache @@ -12,6 +12,11 @@ from {{packageName}}.api_client import ApiClient from {{packageName}}.api.open_fga_api import OpenFgaApi from {{packageName}}.client.configuration import ClientConfiguration from {{packageName}}.client.models.assertion import ClientAssertion +from {{packageName}}.client.models.batch_check_item import ClientBatchCheckItem, construct_batch_item +from {{packageName}}.client.models.batch_check_request import ClientBatchCheckRequest +from {{packageName}}.client.models.batch_check_response import BatchCheckResponse, ClientBatchCheckResponse +from {{packageName}}.client.models.batch_check_single_response import ClientBatchCheckSingleResponse +from {{packageName}}.client.models.client_batch_check_response import ClientBatchCheckClientResponse from {{packageName}}.client.models.check_request import ClientCheckRequest, construct_check_request from {{packageName}}.client.models.batch_check_response import BatchCheckResponse from {{packageName}}.client.models.tuple import ClientTuple, convert_tuple_keys @@ -30,6 +35,7 @@ from {{packageName}}.exceptions import ( UnauthorizedException, ) from {{packageName}}.models.assertion import Assertion +from {{packageName}}.models.batch_check_request import BatchCheckRequest from {{packageName}}.models.check_request import CheckRequest from {{packageName}}.models.contextual_tuple_keys import ContextualTupleKeys from {{packageName}}.models.create_store_request import CreateStoreRequest @@ -95,7 +101,7 @@ def options_to_transaction_info(options: dict[str, int|str] = None): return options["transaction"] return WriteTransactionOpts() -def _check_allowed(response:BatchCheckResponse): +def _check_allowed(response: ClientBatchCheckClientResponse): """ Helper function to return whether the response is check is allowed """ @@ -521,7 +527,7 @@ class OpenFgaClient: ) return api_response - {{#asyncio}}async {{/asyncio}}def _single_batch_check(self, body: ClientCheckRequest, semaphore: asyncio.Semaphore, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def _single_client_batch_check(self, body: ClientCheckRequest, semaphore: asyncio.Semaphore, options: dict[str, str] = None): """ Run a single batch request and return body in a SingleBatchCheckResponse :param body - ClientCheckRequest defining check request @@ -530,17 +536,17 @@ class OpenFgaClient: {{#asyncio}}await semaphore.acquire(){{/asyncio}} try: api_response = {{#asyncio}}await {{/asyncio}}self.check(body, options) - return BatchCheckResponse(allowed=api_response.allowed, request=body, response=api_response, error=None) + return ClientBatchCheckClientResponse(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) + return ClientBatchCheckClientResponse(allowed=False, request=body, response=None, error=err) {{#asyncio}} finally: semaphore.release() {{/asyncio}} - {{#asyncio}}async {{/asyncio}}def batch_check(self, body: list[ClientCheckRequest], options: dict[str, str | int] = None): + {{#asyncio}}async {{/asyncio}}def client_batch_check(self, body: list[ClientCheckRequest], options: dict[str, str | int] = None): """ Run a set of checks :param body - list of ClientCheckRequest defining check request @@ -566,7 +572,7 @@ class OpenFgaClient: {{#asyncio}} sem = asyncio.Semaphore(max_parallel_requests) - batch_check_coros = [self._single_batch_check(request, sem, options) for request in body] + batch_check_coros = [self._single_client_batch_check(request, sem, options) for request in body] batch_check_response = await asyncio.gather(*batch_check_coros) {{/asyncio}} {{^asyncio}} @@ -574,12 +580,117 @@ class OpenFgaClient: request_batches = _chuck_array(body, max_parallel_requests) batch_check_response = [] for request_batch in request_batches: - response = [self._single_batch_check(i, options) for i in request_batch] + response = [self._single_client_batch_check(i, options) for i in request_batch] batch_check_response.extend(response) {{/asyncio}} return batch_check_response + async def _single_batch_check( + self, + body: BatchCheckRequest, + semaphore: asyncio.Semaphore, + options: dict[str, str] = None, + ): + """ + Run a single BatchCheck request + :param body - list[ClientCheckRequest] defining check request + :param authorization_model_id(options) - Overrides the authorization model id in the configuration + """ + await semaphore.acquire() + try: + kwargs = options_to_kwargs(options) + api_response = await self._api.batch_check(body, **kwargs) + return api_response + except Exception as err: + raise err + finally: + semaphore.release() + + async def batch_check(self, body: ClientBatchCheckRequest, options = None): + """ + Run a batchcheck request + :param body - BatchCheck request + :param authorization_model_id(options) - Overrides the authorization model id in the configuration + :param max_parallel_requests(options) - Max number of requests to issue in parallel. Defaults to 10 + :param max_batch_size(options) - Max number of checks to include in a request. Defaults to 50 + :param header(options) - Custom headers to send alongside the request + :param retryParams(options) - Override the retry parameters for this request + :param retryParams.maxRetry(options) - Override the max number of retries on each API request + :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated + """ + options = set_heading_if_not_set( + options, CLIENT_BULK_REQUEST_ID_HEADER, str(uuid.uuid4()) + ) + + max_parallel_requests = 10 + if options is not None and "max_parallel_requests" in options: + if ( + isinstance(options["max_parallel_requests"], str) + and options["max_parallel_requests"].isdigit() + ): + max_parallel_requests = int(options["max_parallel_requests"]) + elif isinstance(options["max_parallel_requests"], int): + max_parallel_requests = options["max_parallel_requests"] + + max_batch_size = 50 + if options is not None and "max_batch_size" in options: + if ( + isinstance(options["max_batch_size"], str) + and options["max_batch_size"].isdigit() + ): + max_batch_size = int(options["max_batch_size"]) + elif isinstance(options["max_batch_size"], int): + max_batch_size = options["max_batch_size"] + + id_to_check: dict[str, ClientBatchCheckItem]= {} + def track_and_transform(checks): + transformed = [] + for check in checks: + if check.correlation_id is None: + check.correlation_id = str(uuid.uuid4()) + + if check.correlation_id in id_to_check: + raise FgaValidationException(f"Duplicate correlation_id ({check.correlation_id}) provided") + + id_to_check[check.correlation_id] = check + + transformed.append(construct_batch_item(check)) + return transformed + + checks = [ + track_and_transform(body.checks[i * max_batch_size : (i + 1) * max_batch_size]) + for i in range((len(body.checks) + max_batch_size - 1) // max_batch_size) + ] + + result = [] + sem = asyncio.Semaphore(max_parallel_requests) + def map_response(id, result): + check = id_to_check[id] + return ClientBatchCheckSingleResponse( + allowed=result.allowed, + request=check, + correlation_id=id, + error=result.error + ) + + async def coro(checks): + res = await self._single_batch_check(BatchCheckRequest( + checks=checks, + authorization_model_id=self._get_authorization_model_id(options), + consistency=self._get_consistency(options), + ), sem, options) + + result.extend([ + map_response(c_id, c_result) + for c_id, c_result in res.result.items() + ]) + + batch_check_coros = [coro(request) for request in checks] + await asyncio.gather(*batch_check_coros) + + return ClientBatchCheckResponse(result) + {{#asyncio}}async {{/asyncio}}def expand(self, body: ClientExpandRequest, options: dict[str, str] = None): """ Run expand request @@ -653,7 +764,7 @@ class OpenFgaClient: options = set_heading_if_not_set(options, CLIENT_BULK_REQUEST_ID_HEADER, str(uuid.uuid4())) request_body = [construct_check_request(user=body.user, relation=i, object=body.object, contextual_tuples=body.contextual_tuples, context=body.context) for i in body.relations] - result = {{#asyncio}}await {{/asyncio}}self.batch_check(request_body, options) + result = {{#asyncio}}await {{/asyncio}}self.client_batch_check(request_body, options) # need to filter with the allowed response result_iterator = filter(_check_allowed, result) result_list = list(result_iterator) diff --git a/config/clients/python/template/src/client/models/__init__.py.mustache b/config/clients/python/template/src/client/models/__init__.py.mustache index 6033fcc7..b07e3194 100644 --- a/config/clients/python/template/src/client/models/__init__.py.mustache +++ b/config/clients/python/template/src/client/models/__init__.py.mustache @@ -1,6 +1,10 @@ {{>partial_header}} from {{packageName}}.client.models.assertion import ClientAssertion -from {{packageName}}.client.models.batch_check_response import BatchCheckResponse +from openfga_sdk.client.models.batch_check_item import ClientBatchCheckItem +from openfga_sdk.client.models.batch_check_request import ClientBatchCheckRequest +from openfga_sdk.client.models.batch_check_response import ClientBatchCheckResponse +from openfga_sdk.client.models.batch_check_single_response import ClientBatchCheckSingleResponse +from openfga_sdk.client.models.client_batch_check_response import ClientBatchCheckClientResponse from {{packageName}}.client.models.check_request import ClientCheckRequest from {{packageName}}.client.models.expand_request import ClientExpandRequest from {{packageName}}.client.models.list_objects_request import ClientListObjectsRequest diff --git a/config/clients/python/template/src/client/models/batch_check_item.py.mustache b/config/clients/python/template/src/client/models/batch_check_item.py.mustache new file mode 100644 index 00000000..70f2ea65 --- /dev/null +++ b/config/clients/python/template/src/client/models/batch_check_item.py.mustache @@ -0,0 +1,128 @@ +{{>partial_header}} + +from openfga_sdk.client.models.tuple import ClientTuple, convert_tuple_keys +from openfga_sdk.models.batch_check_item import BatchCheckItem +from openfga_sdk.models.check_request_tuple_key import CheckRequestTupleKey +from openfga_sdk.models.contextual_tuple_keys import ContextualTupleKeys + +def construct_batch_item(check): + batch_item = BatchCheckItem( + tuple_key=CheckRequestTupleKey( + user=check.user, + relation=check.relation, + object=check.object, + ), + context=check.context, + correlation_id=check.correlation_id, + ) + + if check.contextual_tuples: + batch_item.contextual_tuples = ContextualTupleKeys( + tuple_keys=convert_tuple_keys(check.contextual_tuples) + ) + + return batch_item + +class ClientBatchCheckItem: + def __init__( + self, + user: str, + relation: str, + object: str, + correlation_id: str = None, + contextual_tuples: list[ClientTuple] = None, + context: object = None, + ): + self._user = user + self._relation = relation + self._object = object + self._correlation_id = correlation_id + self._contextual_tuples = None + if contextual_tuples: + self._contextual_tuples = contextual_tuples + self._context = context + + + @property + def user(self): + """ + Return user + """ + return self._user + + @property + def relation(self): + """ + Return relation + """ + return self._relation + + @property + def object(self): + """ + Return object + """ + return self._object + + + @property + def contextual_tuples(self): + """ + Return contextual tuples + """ + return self._contextual_tuples + + @property + def context(self): + """ + Return context + """ + return self._context + + @property + def correlation_id(self): + """ + """ + return self._correlation_id + + + @user.setter + def user(self, value): + """ + Set user + """ + self._user = value + + @relation.setter + def relation(self, value): + """ + Set relation + """ + self._relation = value + + @object.setter + def object(self, value): + """ + Set object + """ + self._object = value + + @contextual_tuples.setter + def contextual_tuples(self, value): + """ + Set contextual tuples + """ + self._contextual_tuples = value + + @context.setter + def context(self, value): + """ + Set context + """ + self._context = value + + @correlation_id.setter + def correlation_id(self, value): + """ + """ + self._correlation_id = value \ No newline at end of file diff --git a/config/clients/python/template/src/client/models/batch_check_request.py.mustache b/config/clients/python/template/src/client/models/batch_check_request.py.mustache new file mode 100644 index 00000000..3a7c7059 --- /dev/null +++ b/config/clients/python/template/src/client/models/batch_check_request.py.mustache @@ -0,0 +1,24 @@ +{{>partial_header}} + +from openfga_sdk.client.models.batch_check_item import ClientBatchCheckItem + +class ClientBatchCheckRequest: + """ + ClientBatchCheckRequest encapsulates the parameters for a BatchCheck request + """ + def __init__(self, checks: list[ClientBatchCheckItem]): + self._checks = checks + + @property + def checks(self): + """ + Return checks + """ + return self._checks + + @checks.setter + def checks(self, checks): + """ + Set checks + """ + self._checks = checks \ No newline at end of file diff --git a/config/clients/python/template/src/client/models/batch_check_response.py.mustache b/config/clients/python/template/src/client/models/batch_check_response.py.mustache index 981560f8..4c957757 100644 --- a/config/clients/python/template/src/client/models/batch_check_response.py.mustache +++ b/config/clients/python/template/src/client/models/batch_check_response.py.mustache @@ -1,49 +1,21 @@ {{>partial_header}} -from {{packageName}}.client.models.check_request import ClientCheckRequest -from {{packageName}}.models.check_response import CheckResponse +from openfga_sdk.client.models.batch_check_single_response import ClientBatchCheckSingleResponse -class BatchCheckResponse: - """ - BatchCheckResponse encapsulates the response for a single batch check - """ - - def __init__(self, allowed: bool, request: ClientCheckRequest, response: CheckResponse, error: Exception=None): - self._allowed = allowed - self._request = request - self._response = response - self._error = error - - @property - def allowed(self): - """ - Return whether request is allowed - """ - return self._allowed - - @property - def request(self): - """ - Return original request - """ - return self._request +class ClientBatchCheckResponse: + def __init__(self, result: list[ClientBatchCheckSingleResponse]): + self._result = result @property - def response(self): + def result(self): """ - Return original request + Return result """ - return self._response - - @property - def error(self): - """ - Return error associated with batch request (if any) - """ - return self._error - - def __str__(self): + return self._result + + @result.setter + def result(self, result): """ - Return the class string + Set result """ - return f"allowed {self._allowed} request {self._request} error {self._error}" + self._result = result \ No newline at end of file diff --git a/config/clients/python/template/src/client/models/batch_check_single_response.py.mustache b/config/clients/python/template/src/client/models/batch_check_single_response.py.mustache new file mode 100644 index 00000000..8a45ec24 --- /dev/null +++ b/config/clients/python/template/src/client/models/batch_check_single_response.py.mustache @@ -0,0 +1,78 @@ +{{>partial_header}} + +from openfga_sdk.client.models.tuple import ClientTuple +from openfga_sdk.models.batch_check_single_result import BatchCheckSingleResult +from openfga_sdk.models.check_error import CheckError + +class ClientBatchCheckSingleResponse: + def __init__( + self, + allowed: bool, + request: ClientTuple, + correlation_id: str, + error: CheckError = None, + ): + self._allowed = allowed + self._request = request + self._correlation_id = correlation_id + self._error = error + # Set "false" if there was an error and allowed isn't set + if error is not None and allowed is None: + self._allowed = False + + + @property + def allowed(self): + """ + Return allowed + """ + return self._allowed + + @property + def request(self): + """ + Return request + """ + return self._request + + @property + def correlation_id(self): + """ + Return correlation_id + """ + return self._correlation_id + + @property + def error(self): + """ + Return error + """ + return self._error + + @allowed.setter + def allowed(self, allowed): + """ + Set allowed + """ + self._allowed = allowed + + @request.setter + def request(self, request): + """ + Set request + """ + self._request = request + + @correlation_id.setter + def correlation_id(self, correlation_id): + """ + Set correlation_id + """ + self._correlation_id = correlation_id + + @error.setter + def error(self, error): + """ + Set error + """ + self._error = error diff --git a/config/clients/python/template/src/client/models/client_batch_check_response.py.mustache b/config/clients/python/template/src/client/models/client_batch_check_response.py.mustache new file mode 100644 index 00000000..52c9765c --- /dev/null +++ b/config/clients/python/template/src/client/models/client_batch_check_response.py.mustache @@ -0,0 +1,49 @@ +{{>partial_header}} + +from {{packageName}}.client.models.check_request import ClientCheckRequest +from {{packageName}}.models.check_response import CheckResponse + +class ClientBatchCheckClientResponse: + """ + ClientBatchCheckClientResponse encapsulates the response for a single batch check + """ + + def __init__(self, allowed: bool, request: ClientCheckRequest, response: CheckResponse, error: Exception=None): + self._allowed = allowed + self._request = request + self._response = response + self._error = error + + @property + def allowed(self): + """ + Return whether request is allowed + """ + return self._allowed + + @property + def request(self): + """ + Return original request + """ + return self._request + + @property + def response(self): + """ + Return original request + """ + return self._response + + @property + def error(self): + """ + Return error associated with batch request (if any) + """ + return self._error + + def __str__(self): + """ + Return the class string + """ + return f"allowed {self._allowed} request {self._request} error {self._error}" diff --git a/config/clients/python/template/src/sync/api.py.mustache b/config/clients/python/template/src/sync/api.py.mustache index 9e232cf5..3da0e7cc 100644 --- a/config/clients/python/template/src/sync/api.py.mustache +++ b/config/clients/python/template/src/sync/api.py.mustache @@ -286,6 +286,11 @@ class {{classname}}: ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( '{{{path}}}'.replace('{store_id}', store_id), '{{httpMethod}}', path_params, diff --git a/config/clients/python/template/src/sync/client/client.py.mustache b/config/clients/python/template/src/sync/client/client.py.mustache index 13df11be..a96a321e 100644 --- a/config/clients/python/template/src/sync/client/client.py.mustache +++ b/config/clients/python/template/src/sync/client/client.py.mustache @@ -4,8 +4,17 @@ from {{packageName}}.sync.api_client import ApiClient from {{packageName}}.sync.open_fga_api import OpenFgaApi from {{packageName}}.client.configuration import ClientConfiguration from {{packageName}}.client.models.assertion import ClientAssertion -from {{packageName}}.client.models.check_request import ClientCheckRequest, construct_check_request -from {{packageName}}.client.models.batch_check_response import BatchCheckResponse +from {{packageName}}.client.models.batch_check_item import ClientBatchCheckItem, construct_batch_item +from {{packageName}}.client.models.batch_check_request import ClientBatchCheckRequest +from {{packageName}}.client.models.batch_check_response import ClientBatchCheckResponse +from {{packageName}}.client.models.batch_check_single_response import ClientBatchCheckSingleResponse +from {{packageName}}.client.models.check_request import ( + ClientCheckRequest, + construct_check_request, +) +from {{packageName}}.client.models.client_batch_check_response import ( + ClientBatchCheckClientResponse, +) from {{packageName}}.client.models.tuple import ClientTuple, convert_tuple_keys from {{packageName}}.client.models.write_request import ClientWriteRequest from {{packageName}}.client.models.write_response import ClientWriteResponse @@ -22,6 +31,7 @@ from {{packageName}}.exceptions import ( UnauthorizedException, ) from {{packageName}}.models.assertion import Assertion +from {{packageName}}.models.batch_check_request import BatchCheckRequest from {{packageName}}.models.check_request import CheckRequest from {{packageName}}.models.contextual_tuple_keys import ContextualTupleKeys from {{packageName}}.models.create_store_request import CreateStoreRequest @@ -90,7 +100,7 @@ def options_to_transaction_info(options: dict[str, int|str] = None): return options["transaction"] return WriteTransactionOpts() -def _check_allowed(response:BatchCheckResponse): +def _check_allowed(response: ClientBatchCheckClientResponse): """ Helper function to return whether the response is check is allowed """ @@ -501,7 +511,7 @@ class OpenFgaClient: ) return api_response - def _single_batch_check(self, body: ClientCheckRequest, options: dict[str, str] = None): + def _single_client_batch_check(self, body: ClientCheckRequest, options: dict[str, str] = None): """ Run a single batch request and return body in a SingleBatchCheckResponse :param body - ClientCheckRequest defining check request @@ -509,13 +519,13 @@ class OpenFgaClient: """ try: api_response = self.check(body, options) - return BatchCheckResponse(allowed=api_response.allowed, request=body, response=api_response, error=None) + return ClientBatchCheckClientResponse(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) + return ClientBatchCheckClientResponse(allowed=False, request=body, response=None, error=err) - def batch_check(self, body: list[ClientCheckRequest], options: dict[str, str | int] = None): + def client_batch_check(self, body: list[ClientCheckRequest], options: dict[str, str | int] = None): """ Run a set of checks :param body - list of ClientCheckRequest defining check request @@ -543,7 +553,7 @@ class OpenFgaClient: batch_check_response = [] def single_batch_check(request): - return self._single_batch_check(request, options) + return self._single_client_batch_check(request, options) with ThreadPoolExecutor(max_workers=max_parallel_requests) as executor: for response in executor.map(single_batch_check, body): @@ -551,6 +561,114 @@ class OpenFgaClient: return batch_check_response + def _single_batch_check( + self, + body: BatchCheckRequest, + options: dict[str, str] = None, + ): + """ + Run a single BatchCheck request + :param body - list[ClientCheckRequest] defining check request + :param authorization_model_id(options) - Overrides the authorization model id in the configuration + """ + try: + kwargs = options_to_kwargs(options) + api_response = self._api.batch_check(body, **kwargs) + return api_response + # Does this cover all error cases? If one fails with a 4xx/5xx then all should? + except Exception as err: + raise err + + def batch_check(self, body: ClientBatchCheckRequest, options=None): + """ + Run a batchcheck request + :param body - BatchCheck request + :param authorization_model_id(options) - Overrides the authorization model id in the configuration + :param max_parallel_requests(options) - Max number of requests to issue in parallel. Defaults to 10 + :param max_batch_size(options) - Max number of checks to include in a request. Defaults to 50 + :param header(options) - Custom headers to send alongside the request + :param retryParams(options) - Override the retry parameters for this request + :param retryParams.maxRetry(options) - Override the max number of retries on each API request + :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated + """ + options = set_heading_if_not_set( + options, CLIENT_BULK_REQUEST_ID_HEADER, str(uuid.uuid4()) + ) + + max_parallel_requests = 10 + if options is not None and "max_parallel_requests" in options: + if ( + isinstance(options["max_parallel_requests"], str) + and options["max_parallel_requests"].isdigit() + ): + max_parallel_requests = int(options["max_parallel_requests"]) + elif isinstance(options["max_parallel_requests"], int): + max_parallel_requests = options["max_parallel_requests"] + + max_batch_size = 50 + if options is not None and "max_batch_size" in options: + if ( + isinstance(options["max_batch_size"], str) + and options["max_batch_size"].isdigit() + ): + max_batch_size = int(options["max_batch_size"]) + elif isinstance(options["max_batch_size"], int): + max_batch_size = options["max_batch_size"] + + id_to_check: dict[str, ClientBatchCheckItem] = {} + + def track_and_transform(checks): + transformed = [] + for check in checks: + if check.correlation_id is None: + check.correlation_id = str(uuid.uuid4()) + + if check.correlation_id in id_to_check: + raise FgaValidationException(f"Duplicate correlation_id ({check.correlation_id}) provided") + + id_to_check[check.correlation_id] = check + + transformed.append(construct_batch_item(check)) + return transformed + + checks = [ + track_and_transform( + body.checks[i * max_batch_size : (i + 1) * max_batch_size] + ) + for i in range((len(body.checks) + max_batch_size - 1) // max_batch_size) + ] + + def map_response(id, result): + check = id_to_check[id] + return ClientBatchCheckSingleResponse( + allowed=result.allowed, + request=check, + correlation_id=id, + error=result.error, + ) + + def single_batch_check(checks): + res = self._single_batch_check( + BatchCheckRequest( + checks=checks, + authorization_model_id=self._get_authorization_model_id(options), + consistency=self._get_consistency(options), + ), + options, + ) + + return res + + result = [] + + with ThreadPoolExecutor(max_workers=max_parallel_requests) as executor: + for response in executor.map(single_batch_check, checks): + result.extend([ + map_response(c_id, c_result) for c_id, c_result in response.result.items() + ]) + + return ClientBatchCheckResponse(result) + def expand(self, body: ClientExpandRequest, options: dict[str, str] = None): """ Run expand request @@ -624,7 +742,7 @@ class OpenFgaClient: options = set_heading_if_not_set(options, CLIENT_BULK_REQUEST_ID_HEADER, str(uuid.uuid4())) request_body = [construct_check_request(user=body.user, relation=i, object=body.object, contextual_tuples=body.contextual_tuples, context=body.context) for i in body.relations] - result = self.batch_check(request_body, options) + result = self.client_batch_check(request_body, options) # need to filter with the allowed response result_iterator = filter(_check_allowed, result) result_list = list(result_iterator) diff --git a/config/clients/python/template/src/telemetry/attributes.py.mustache b/config/clients/python/template/src/telemetry/attributes.py.mustache index 6c1cbbc5..2b31af84 100644 --- a/config/clients/python/template/src/telemetry/attributes.py.mustache +++ b/config/clients/python/template/src/telemetry/attributes.py.mustache @@ -1,6 +1,6 @@ import time import urllib -from typing import NamedTuple +from typing import Any, NamedTuple from aiohttp import ClientResponse from urllib3 import HTTPResponse @@ -18,6 +18,10 @@ class TelemetryAttribute(NamedTuple): class TelemetryAttributes: + fga_client_request_batch_check_size: TelemetryAttribute = TelemetryAttribute( + name="fga-client.request.batch_check_size", + format="int" + ) fga_client_request_client_id: TelemetryAttribute = TelemetryAttribute( name="fga-client.request.client_id", ) @@ -69,6 +73,7 @@ class TelemetryAttributes: ) _attributes: list[TelemetryAttribute] = [ + fga_client_request_batch_check_size, fga_client_request_client_id, fga_client_request_method, fga_client_request_model_id, @@ -164,6 +169,23 @@ class TelemetryAttributes: return response + @staticmethod + def fromBody(body: Any, attributes: dict[TelemetryAttribute, str | int] = None): + from openfga_sdk.models.batch_check_request import BatchCheckRequest + + if attributes is None: + attributes = {} + + if ( + TelemetryAttributes.fga_client_request_batch_check_size not in attributes + and isinstance(body, BatchCheckRequest) + ): + attributes[TelemetryAttributes.fga_client_request_batch_check_size] = len( + body.checks + ) + + return attributes + @staticmethod def fromRequest( user_agent: str = None, diff --git a/config/clients/python/template/src/telemetry/configuration.py.mustache b/config/clients/python/template/src/telemetry/configuration.py.mustache index 90269147..9ba43cdf 100644 --- a/config/clients/python/template/src/telemetry/configuration.py.mustache +++ b/config/clients/python/template/src/telemetry/configuration.py.mustache @@ -31,6 +31,7 @@ class TelemetryMetricConfiguration: url_scheme: bool | None = None, url_full: bool | None = None, user_agent_original: bool | None = None, + fga_client_request_batch_check_size: bool | None = None, ): """ Initialize a new instance of the `TelemetryMetricConfiguration` class. @@ -51,6 +52,7 @@ class TelemetryMetricConfiguration: :param url_scheme: The `url.scheme` attribute includes the scheme of the request URL. :param url_full: The `url.full` attribute includes the full URL of the request. :param user_agent_original: The `user_agent.original` attribute includes the original user agent string of the request. + :param fga_client_request_batch_check_size: The `fga-client.request.batch_check_size` attribute includes the size of the `checks` list in a `BatchCheck` request. """ self.configure( @@ -58,6 +60,11 @@ class TelemetryMetricConfiguration: clear=True, ) + if fga_client_request_batch_check_size is not None: + self._state[TelemetryAttributes.fga_client_request_batch_check_size] = ( + fga_client_request_batch_check_size + ) + if fga_client_request_client_id is not None: self._state[TelemetryAttributes.fga_client_request_client_id] = ( fga_client_request_client_id @@ -123,6 +130,25 @@ class TelemetryMetricConfiguration: self._valid = None # Reset the validation state + @property + def fga_client_request_batch_check_size(self) -> bool: + """ + Get the configuration for the `fga_client_request_batch_check_size` attribute. + + :return: The configuration for the `fga_client_request_batch_check_size` attribute. + """ + return self._state[TelemetryAttributes.fga_client_request_batch_check_size] + + @fga_client_request_batch_check_size.setter + def fga_client_request_batch_check_size(self, value: bool): + """ + Set the configuration for the `fga_client_request_batch_check_size` attribute. + + :param value: The configuration for the `fga_client_request_batch_check_size` attribute. + """ + self._valid = None # Reset the validation state + self._state[TelemetryAttributes.fga_client_request_batch_check_size] = value + @property def fga_client_request_client_id(self) -> bool: """ @@ -445,6 +471,7 @@ class TelemetryMetricConfiguration: # Reset the configuration to the default state self._state = { + TelemetryAttributes.fga_client_request_batch_check_size: False, TelemetryAttributes.fga_client_request_client_id: False, TelemetryAttributes.fga_client_request_method: False, TelemetryAttributes.fga_client_request_model_id: False, @@ -577,6 +604,7 @@ class TelemetryMetricConfiguration: :return: The default SDK configuration for the telemetry metric. """ return { + TelemetryAttributes.fga_client_request_batch_check_size: False, TelemetryAttributes.fga_client_request_client_id: True, TelemetryAttributes.fga_client_request_method: True, TelemetryAttributes.fga_client_request_model_id: True, diff --git a/config/clients/python/template/test/client/client_test.py.mustache b/config/clients/python/template/test/client/client_test.py.mustache index c6633b64..06c8d56f 100644 --- a/config/clients/python/template/test/client/client_test.py.mustache +++ b/config/clients/python/template/test/client/client_test.py.mustache @@ -6,11 +6,14 @@ from unittest.mock import patch from datetime import datetime import urllib3 +import uuid from {{packageName}} import rest from {{packageName}}.client import ClientConfiguration from {{packageName}}.client.client import OpenFgaClient from {{packageName}}.client.models.assertion import ClientAssertion +from {{packageName}}.client.models.batch_check_item import ClientBatchCheckItem +from {{packageName}}.client.models.batch_check_request import ClientBatchCheckRequest from {{packageName}}.client.models.check_request import ClientCheckRequest from {{packageName}}.client.models.expand_request import ClientExpandRequest from {{packageName}}.client.models.list_objects_request import ClientListObjectsRequest @@ -1593,7 +1596,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): {{#asyncio}}await {{/asyncio}}api_client.close() @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_batch_check_single_request(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_client_batch_check_single_request(self, mock_request): """Test case for check with single request Check whether a user is authorized to access an object @@ -1612,7 +1615,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: - api_response = {{#asyncio}}await {{/asyncio}}api_client.batch_check( + api_response = {{#asyncio}}await {{/asyncio}}api_client.client_batch_check( body=[body], options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"} ) @@ -1636,7 +1639,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): {{#asyncio}}await {{/asyncio}}api_client.close() @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_batch_check_multiple_request(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_client_batch_check_multiple_request(self, mock_request): """Test case for check with multiple request Check whether a user is authorized to access an object @@ -1666,7 +1669,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: - api_response = {{#asyncio}}await {{/asyncio}}api_client.batch_check( + api_response = {{#asyncio}}await {{/asyncio}}api_client.client_batch_check( body=[body1, body2, body3], options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", "max_parallel_requests": 2} ) @@ -1719,7 +1722,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_batch_check_multiple_request_fail(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_client_batch_check_multiple_request_fail(self, mock_request): """Test case for check with multiple request with one request failed Check whether a user is authorized to access an object @@ -1755,7 +1758,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: - api_response = {{#asyncio}}await {{/asyncio}}api_client.batch_check( + api_response = {{#asyncio}}await {{/asyncio}}api_client.client_batch_check( body=[body1, body2, body3], options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", "max_parallel_requests": 2} ) @@ -1806,7 +1809,368 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): _request_timeout=None ) {{#asyncio}}await {{/asyncio}}api_client.close() + @patch.object(rest.RESTClientObject, "request") + async def test_batch_check_single_request(self, mock_request): + """Test case for check with single request + + Check whether a user is authorized to access an object + """ + + # First, mock the response + response_body = """ + { + "result": { + "1": { + "allowed": true + } + } + } + """ + mock_request.side_effect = [ + mock_response(response_body, 200), + ] + + body = ClientBatchCheckRequest( + checks=[ + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + correlation_id='1' + ), + ] + ) + configuration = self.configuration + configuration.store_id = store_id + async with OpenFgaClient(configuration) as api_client: + api_response = await api_client.batch_check( + body=body, + options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + ) + self.assertEqual(len(api_response.result), 1) + self.assertEqual(api_response.result[0].error, None) + self.assertTrue(api_response.result[0].allowed) + self.assertEqual(api_response.result[0].correlation_id, '1') + self.assertEqual(api_response.result[0].request, body.checks[0]) + # Make sure the API was called with the right data + mock_request.assert_any_call( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/batch-check", + headers=ANY, + query_params=[], + post_params=[], + body={ + "checks": [ + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + }, + "correlation_id": "1", + } + ], + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, + _preload_content=ANY, + _request_timeout=None, + ) + await api_client.close() + + @patch.object(uuid, "uuid4") + @patch.object(rest.RESTClientObject, "request") + async def test_batch_check_multiple_request(self, mock_request, mock_uuid): + """Test case for check with multiple request + + Check whether a user is authorized to access an object + """ + first_response_body = """ + { + "result": { + "1": { + "allowed": true + }, + "2": { + "allowed": false + } + } + } +""" + + second_response_body = """ +{ + "result": { + "fake-uuid": { + "error": { + "input_error": "validation_error", + "message": "type 'doc' not found" + } + } + } +}""" + + + # First, mock the response + mock_request.side_effect = [ + mock_response(first_response_body, 200), + mock_response(second_response_body, 200), + ] + + def mock_v4(val: str): + return val + mock_uuid.side_effect = [ + mock_v4("batch-id-header"), + mock_v4("fake-uuid") + ] + + body = ClientBatchCheckRequest( + checks=[ + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + correlation_id='1' + ), + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", + correlation_id='2' + ), + ClientBatchCheckItem( + object="doc:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31d" + ) + ] + ) + + configuration = self.configuration + configuration.store_id = store_id + async with OpenFgaClient(configuration) as api_client: + api_response = await api_client.batch_check( + body=body, + options={ + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + "max_parallel_requests": 1, + "max_batch_size": 2, + }, + ) + self.assertEqual(len(api_response.result), 3) + self.assertEqual(api_response.result[0].error, None) + self.assertTrue(api_response.result[0].allowed) + self.assertEqual(api_response.result[1].error, None) + self.assertFalse(api_response.result[1].allowed) + self.assertEqual( + api_response.result[2].error.message, "type 'doc' not found" + ) + self.assertFalse(api_response.result[2].allowed) + # value generated from the uuid mock + self.assertEqual(api_response.result[2].correlation_id, 'fake-uuid') + # Make sure the API was called with the right data + mock_request.assert_any_call( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/batch-check", + headers=ANY, + query_params=[], + post_params=[], + body={ + "checks": [ + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + }, + "correlation_id": "1", + }, + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31c", + }, + "correlation_id": "2", + }, + ], + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, + _preload_content=ANY, + _request_timeout=None, + ) + mock_request.assert_any_call( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/batch-check", + headers=ANY, + query_params=[], + post_params=[], + body={ + "checks": [ + { + "tuple_key": { + "object": "doc:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d", + }, + "correlation_id": "fake-uuid", + } + ], + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, + _preload_content=ANY, + _request_timeout=None, + ) + await api_client.close() + + async def test_batch_check_errors_dupe_cor_id(self): + """Test case for duplicate correlation_id being provided to batch_check + """ + + body = ClientBatchCheckRequest( + checks=[ + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + correlation_id='1' + ), + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + correlation_id='1' + ), + ] + ) + configuration = self.configuration + configuration.store_id = store_id + async with OpenFgaClient(configuration) as api_client: + with self.assertRaises(FgaValidationException) as error: + await api_client.batch_check( + body=body, + options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + ) + self.assertEqual( + "Duplicate correlation_id (1) provided", + str(error.exception) + ) + await api_client.close() + + @patch.object(rest.RESTClientObject, "request") + async def test_batch_check_errors_unauthorized(self, mock_request): + """Test case for BatchCheck with a 401""" + first_response_body = """ + { + "result": { + "1": { + "allowed": true + }, + "2": { + "allowed": false + } + } + } +""" + + # First, mock the response + mock_request.side_effect = [ + mock_response(first_response_body, 200), + UnauthorizedException( + http_resp=http_mock_response("{}", 401) + ), + ] + + body = ClientBatchCheckRequest( + checks=[ + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + correlation_id="1", + ), + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", + correlation_id="2", + ), + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", + correlation_id="3" + ), + ] + ) + + configuration = self.configuration + configuration.store_id = store_id + async with OpenFgaClient(configuration) as api_client: + with self.assertRaises(UnauthorizedException): + await api_client.batch_check( + body=body, + options={ + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + "max_parallel_requests": 1, + "max_batch_size": 2, + }, + ) + + # Make sure the API was called with the right data + mock_request.assert_any_call( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/batch-check", + headers=ANY, + query_params=[], + post_params=[], + body={ + "checks": [ + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + }, + "correlation_id": "1", + }, + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31c", + }, + "correlation_id": "2", + }, + ], + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, + _preload_content=ANY, + _request_timeout=None, + ) + mock_request.assert_any_call( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/batch-check", + headers=ANY, + query_params=[], + post_params=[], + body={ + "checks": [ + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d", + }, + "correlation_id": "3", + } + ], + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, + _preload_content=ANY, + _request_timeout=None, + ) + await api_client.close() @patch.object(rest.RESTClientObject, 'request') {{#asyncio}}async {{/asyncio}}def test_expand(self, mock_request): diff --git a/config/clients/python/template/test/sync/client/client_test.py.mustache b/config/clients/python/template/test/sync/client/client_test.py.mustache index a8aa2f3e..9d6e934a 100644 --- a/config/clients/python/template/test/sync/client/client_test.py.mustache +++ b/config/clients/python/template/test/sync/client/client_test.py.mustache @@ -6,9 +6,12 @@ from unittest.mock import patch from datetime import datetime import urllib3 +import uuid from {{packageName}}.client import ClientConfiguration from {{packageName}}.client.models.assertion import ClientAssertion +from {{packageName}}.client.models.batch_check_item import ClientBatchCheckItem +from {{packageName}}.client.models.batch_check_request import ClientBatchCheckRequest from {{packageName}}.client.models.check_request import ClientCheckRequest from {{packageName}}.client.models.expand_request import ClientExpandRequest from {{packageName}}.client.models.list_objects_request import ClientListObjectsRequest @@ -1590,8 +1593,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) api_client.close() - @patch.object(rest.RESTClientObject, 'request') - def test_batch_check_single_request(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + def test_client_batch_check_single_request(self, mock_request): """Test case for check with single request Check whether a user is authorized to access an object @@ -1603,19 +1606,19 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): mock_response(response_body, 200), ] body = ClientCheckRequest( - object="document:2021-budget", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - ) + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) configuration = self.configuration configuration.store_id = store_id with OpenFgaClient(configuration) as api_client: - api_response = api_client.batch_check( + api_response = api_client.client_batch_check( body=[body], options={ "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", "consistency": ConsistencyPreference.MINIMIZE_LATENCY, - } + }, ) self.assertIsInstance(api_response, list) self.assertEqual(len(api_response), 1) @@ -1624,8 +1627,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): self.assertEqual(api_response[0].request, body) # Make sure the API was called with the right data mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check", headers=ANY, query_params=[], post_params=[], @@ -1633,18 +1636,18 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "tuple_key": { "object": "document:2021-budget", "relation": "reader", - "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b" + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", }, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", "consistency": "MINIMIZE_LATENCY", }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) api_client.close() - @patch.object(rest.RESTClientObject, 'request') - def test_batch_check_multiple_request(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + def test_client_batch_check_multiple_request(self, mock_request): """Test case for check with multiple request Check whether a user is authorized to access an object @@ -1657,26 +1660,29 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): mock_response('{"allowed": true, "resolution": "1234"}', 200), ] body1 = ClientCheckRequest( - object="document:2021-budget", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - ) + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) body2 = ClientCheckRequest( - object="document:2021-budget", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", - ) + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", + ) body3 = ClientCheckRequest( - object="document:2021-budget", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", - ) + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", + ) configuration = self.configuration configuration.store_id = store_id with OpenFgaClient(configuration) as api_client: - api_response = api_client.batch_check( + api_response = api_client.client_batch_check( body=[body1, body2, body3], - options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", "max_parallel_requests": 2} + options={ + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + "max_parallel_requests": 2, + }, ) self.assertIsInstance(api_response, list) self.assertEqual(len(api_response), 3) @@ -1691,53 +1697,70 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): self.assertEqual(api_response[2].request, body3) # Make sure the API was called with the right data mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check", headers=ANY, query_params=[], post_params=[], - body={"tuple_key": {"object": "document:2021-budget", - "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b"}, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + body={ + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + }, + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check", headers=ANY, query_params=[], post_params=[], - body={"tuple_key": {"object": "document:2021-budget", - "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31c"}, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + body={ + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31c", + }, + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check", headers=ANY, query_params=[], post_params=[], - body={"tuple_key": {"object": "document:2021-budget", - "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d"}, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + body={ + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d", + }, + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) api_client.close() - - @patch.object(rest.RESTClientObject, 'request') - def test_batch_check_multiple_request_fail(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + def test_client_batch_check_multiple_request_fail(self, mock_request): """Test case for check with multiple request with one request failed Check whether a user is authorized to access an object """ - response_body = ''' + response_body = """ { "code": "validation_error", "message": "Generic validation error" } - ''' + """ # First, mock the response mock_request.side_effect = [ @@ -1746,26 +1769,29 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): mock_response('{"allowed": false, "resolution": "1234"}', 200), ] body1 = ClientCheckRequest( - object="document:2021-budget", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - ) + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) body2 = ClientCheckRequest( - object="document:2021-budget", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", - ) + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", + ) body3 = ClientCheckRequest( - object="document:2021-budget", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", - ) + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", + ) configuration = self.configuration configuration.store_id = store_id with OpenFgaClient(configuration) as api_client: - api_response = api_client.batch_check( + api_response = api_client.client_batch_check( body=[body1, body2, body3], - options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", "max_parallel_requests": 2} + options={ + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + "max_parallel_requests": 2, + }, ) self.assertIsInstance(api_response, list) self.assertEqual(len(api_response), 3) @@ -1775,46 +1801,422 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): self.assertFalse(api_response[1].allowed) self.assertEqual(api_response[1].request, body2) self.assertIsInstance(api_response[1].error, ValidationException) - self.assertIsInstance(api_response[1].error.parsed_exception, ValidationErrorMessageResponse) + self.assertIsInstance( + api_response[1].error.parsed_exception, ValidationErrorMessageResponse + ) self.assertEqual(api_response[2].error, None) 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( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check", headers=ANY, query_params=[], post_params=[], - body={"tuple_key": {"object": "document:2021-budget", - "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b"}, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + body={ + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + }, + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check", headers=ANY, query_params=[], post_params=[], - body={"tuple_key": {"object": "document:2021-budget", - "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31c"}, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + body={ + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31c", + }, + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check", headers=ANY, query_params=[], post_params=[], - body={"tuple_key": {"object": "document:2021-budget", - "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d"}, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + body={ + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d", + }, + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, + ) + api_client.close() + + @patch.object(rest.RESTClientObject, "request") + def test_batch_check_single_request(self, mock_request): + """Test case for check with single request + + Check whether a user is authorized to access an object + """ + + # First, mock the response + response_body = """ + { + "result": { + "1": { + "allowed": true + } + } + } + """ + mock_request.side_effect = [ + mock_response(response_body, 200), + ] + + body = ClientBatchCheckRequest( + checks=[ + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + correlation_id="1", + ), + ] + ) + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + api_response = api_client.batch_check( + body=body, + options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + ) + self.assertEqual(len(api_response.result), 1) + self.assertEqual(api_response.result[0].error, None) + self.assertTrue(api_response.result[0].allowed) + self.assertEqual(api_response.result[0].correlation_id, "1") + self.assertEqual(api_response.result[0].request, body.checks[0]) + # Make sure the API was called with the right data + mock_request.assert_any_call( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/batch-check", + headers=ANY, + query_params=[], + post_params=[], + body={ + "checks": [ + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + }, + "correlation_id": "1", + } + ], + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, + _preload_content=ANY, + _request_timeout=None, + ) + api_client.close() + + @patch.object(uuid, "uuid4") + @patch.object(rest.RESTClientObject, "request") + def test_batch_check_multiple_request(self, mock_request, mock_uuid): + """Test case for check with multiple request + + Check whether a user is authorized to access an object + """ + first_response_body = """ + { + "result": { + "1": { + "allowed": true + }, + "2": { + "allowed": false + } + } + } +""" + + second_response_body = """ +{ + "result": { + "fake-uuid": { + "error": { + "input_error": "validation_error", + "message": "type 'doc' not found" + } + } + } +}""" + # First, mock the response + mock_request.side_effect = [ + mock_response(first_response_body, 200), + mock_response(second_response_body, 200), + ] + + def mock_v4(val: str): + return val + + mock_uuid.side_effect = [mock_v4("batch-id-header"), mock_v4("fake-uuid")] + + body = ClientBatchCheckRequest( + checks=[ + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + correlation_id="1", + ), + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", + correlation_id="2", + ), + ClientBatchCheckItem( + object="doc:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", + ), + ] + ) + + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + api_response = api_client.batch_check( + body=body, + options={ + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + "max_parallel_requests": 1, + "max_batch_size": 2, + }, + ) + self.assertEqual(len(api_response.result), 3) + self.assertEqual(api_response.result[0].error, None) + self.assertTrue(api_response.result[0].allowed) + self.assertEqual(api_response.result[1].error, None) + self.assertFalse(api_response.result[1].allowed) + self.assertEqual( + api_response.result[2].error.message, "type 'doc' not found" + ) + self.assertFalse(api_response.result[2].allowed) + # value generated from the uuid mock + self.assertEqual(api_response.result[2].correlation_id, "fake-uuid") + # Make sure the API was called with the right data + mock_request.assert_any_call( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/batch-check", + headers=ANY, + query_params=[], + post_params=[], + body={ + "checks": [ + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + }, + "correlation_id": "1", + }, + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31c", + }, + "correlation_id": "2", + }, + ], + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, + _preload_content=ANY, + _request_timeout=None, + ) + mock_request.assert_any_call( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/batch-check", + headers=ANY, + query_params=[], + post_params=[], + body={ + "checks": [ + { + "tuple_key": { + "object": "doc:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d", + }, + "correlation_id": "fake-uuid", + } + ], + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, + _preload_content=ANY, + _request_timeout=None, + ) + api_client.close() + + def test_batch_check_errors_dupe_cor_id(self): + """Test case for duplicate correlation_id being provided to batch_check""" + + body = ClientBatchCheckRequest( + checks=[ + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + correlation_id="1", + ), + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + correlation_id="1", + ), + ] + ) + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + with self.assertRaises(FgaValidationException) as error: + api_client.batch_check( + body=body, + options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + ) + self.assertEqual( + "Duplicate correlation_id (1) provided", + str(error.exception) ) api_client.close() + @patch.object(rest.RESTClientObject, "request") + def test_batch_check_errors_unauthorized(self, mock_request): + """Test case for BatchCheck with a 401""" + first_response_body = """ + { + "result": { + "1": { + "allowed": true + }, + "2": { + "allowed": false + } + } + } +""" + + # First, mock the response + mock_request.side_effect = [ + mock_response(first_response_body, 200), + UnauthorizedException( + http_resp=http_mock_response("{}", 401) + ), + ] + + body = ClientBatchCheckRequest( + checks=[ + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + correlation_id="1", + ), + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", + correlation_id="2", + ), + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", + correlation_id="3" + ), + ] + ) + + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + with self.assertRaises(UnauthorizedException): + api_client.batch_check( + body=body, + options={ + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + "max_parallel_requests": 1, + "max_batch_size": 2, + }, + ) + + # Make sure the API was called with the right data + mock_request.assert_any_call( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/batch-check", + headers=ANY, + query_params=[], + post_params=[], + body={ + "checks": [ + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + }, + "correlation_id": "1", + }, + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31c", + }, + "correlation_id": "2", + }, + ], + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, + _preload_content=ANY, + _request_timeout=None, + ) + mock_request.assert_any_call( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/batch-check", + headers=ANY, + query_params=[], + post_params=[], + body={ + "checks": [ + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d", + }, + "correlation_id": "3", + } + ], + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, + _preload_content=ANY, + _request_timeout=None, + ) + api_client.close() @patch.object(rest.RESTClientObject, 'request') def test_expand(self, mock_request): diff --git a/config/clients/python/template/test/telemetry/attributes_test.py.mustache b/config/clients/python/template/test/telemetry/attributes_test.py.mustache index 95a429d1..28086afa 100644 --- a/config/clients/python/template/test/telemetry/attributes_test.py.mustache +++ b/config/clients/python/template/test/telemetry/attributes_test.py.mustache @@ -5,6 +5,9 @@ import pytest from urllib3 import HTTPResponse from {{packageName}}.credentials import CredentialConfiguration, Credentials +from {{packageName}}.models.batch_check_request import BatchCheckRequest +from {{packageName}}.models.check_request import CheckRequest +from {{packageName}}.rest import RESTResponse from {{packageName}}.rest import RESTResponse from {{packageName}}.telemetry.attributes import ( TelemetryAttributes, @@ -20,14 +23,19 @@ def test_prepare_with_valid_attributes(telemetry_attributes): attributes = { telemetry_attributes.fga_client_request_client_id: "client_123", telemetry_attributes.http_request_method: "GET", + telemetry_attributes.fga_client_request_batch_check_size: 3, } - filter_attributes = [telemetry_attributes.fga_client_request_client_id] + filter_attributes = [ + telemetry_attributes.fga_client_request_client_id, + telemetry_attributes.fga_client_request_batch_check_size, + ] prepared = telemetry_attributes.prepare(attributes, filter=filter_attributes) # Assert that only filtered attributes are returned assert prepared == { "fga-client.request.client_id": "client_123", + "fga-client.request.batch_check_size": 3, } @@ -144,4 +152,20 @@ def test_from_response_with_rest_response(telemetry_attributes): assert attributes[TelemetryAttributes.http_response_status_code] == 404 assert attributes[TelemetryAttributes.fga_client_response_model_id] == "model_404" assert attributes[TelemetryAttributes.http_server_request_duration] == "100" - assert attributes[TelemetryAttributes.fga_client_request_client_id] == "client_456" \ No newline at end of file + assert attributes[TelemetryAttributes.fga_client_request_client_id] == "client_456" + +def test_from_body_with_batch_check(telemetry_attributes): + body = MagicMock(spec=BatchCheckRequest) + body.checks = ["1", "2", "3"] + + attributes = telemetry_attributes.fromBody(body=body) + + assert attributes[TelemetryAttributes.fga_client_request_batch_check_size] == 3 + + +def test_from_body_with_other_body(telemetry_attributes): + body = MagicMock(spec=CheckRequest) + + attributes = telemetry_attributes.fromBody(body=body) + + assert attributes == {} \ No newline at end of file diff --git a/config/clients/python/template/test/telemetry/configuration_test.py.mustache b/config/clients/python/template/test/telemetry/configuration_test.py.mustache index 412a221a..2607c433 100644 --- a/config/clients/python/template/test/telemetry/configuration_test.py.mustache +++ b/config/clients/python/template/test/telemetry/configuration_test.py.mustache @@ -12,6 +12,7 @@ from {{packageName}}.telemetry.histograms import TelemetryHistograms def test_telemetry_metric_configuration_default_initialization(): config = TelemetryMetricConfiguration() + assert config.fga_client_request_batch_check_size is False assert config.fga_client_request_client_id is False assert config.fga_client_request_method is False assert config.fga_client_request_model_id is False @@ -292,8 +293,11 @@ def test_default_telemetry_metric_configuration(): metric_config = TelemetryMetricConfiguration.getSdkDefaults() assert isinstance(metric_config, dict) - assert len(metric_config) == 15 + assert len(metric_config) == 16 + assert ( + metric_config[TelemetryAttributes.fga_client_request_batch_check_size] is False + ) assert metric_config[TelemetryAttributes.fga_client_request_client_id] is True assert metric_config[TelemetryAttributes.fga_client_request_method] is True assert metric_config[TelemetryAttributes.fga_client_request_model_id] is True