From 9bebf2fc877eac070103c8d56b18ae2c3040895d Mon Sep 17 00:00:00 2001 From: Ryan Quinn Date: Thu, 6 Feb 2025 10:26:51 -0600 Subject: [PATCH 1/8] chore: update for PR 398 from sdk-generator --- CHANGELOG.md | 4 +- README.md | 62 ++++---- docs/Assertion.md | 2 + docs/AuthErrorCode.md | 9 ++ docs/ConsistencyPreference.md | 2 +- docs/ForbiddenResponse.md | 11 ++ docs/OpenFgaApi.md | 26 +++- example/Example1/Example1.cs | 8 +- src/OpenFga.Sdk.Test/Api/OpenFgaApiTests.cs | 62 ++++---- .../Client/OpenFgaClientTests.cs | 78 +++++----- src/OpenFga.Sdk.Test/Models/ModelTests.cs | 6 +- src/OpenFga.Sdk.Test/OpenFga.Sdk.Test.csproj | 2 +- src/OpenFga.Sdk/Api/OpenFgaApi.cs | 10 +- src/OpenFga.Sdk/Model/Assertion.cs | 40 ++++- src/OpenFga.Sdk/Model/AuthErrorCode.cs | 79 ++++++++++ .../Model/ConsistencyPreference.cs | 4 +- src/OpenFga.Sdk/Model/ErrorCode.cs | 8 +- src/OpenFga.Sdk/Model/ForbiddenResponse.cs | 143 ++++++++++++++++++ src/OpenFga.Sdk/Model/InternalErrorCode.cs | 22 +-- src/OpenFga.Sdk/Model/ReadRequest.cs | 10 ++ src/OpenFga.Sdk/Model/TypeName.cs | 126 +++++++-------- src/OpenFga.Sdk/OpenFga.Sdk.csproj | 2 +- 22 files changed, 518 insertions(+), 198 deletions(-) create mode 100644 docs/AuthErrorCode.md create mode 100644 docs/ForbiddenResponse.md create mode 100644 src/OpenFga.Sdk/Model/AuthErrorCode.cs create mode 100644 src/OpenFga.Sdk/Model/ForbiddenResponse.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index f1528c3..c1b2d55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased](https://github.com/openfga/dotnet-sdk/compare/v0.5.1...HEAD) + ## v0.5.1 ### [0.5.1](https://github.com/openfga/dotnet-sdk/compare/v0.5.0...v0.5.1) (2024-09-09) @@ -136,7 +138,7 @@ Updated to include support for [OpenFGA 0.3.0](https://github.com/openfga/openfg Changes: - [BREAKING] feat(list-objects)!: response has been changed to include the object type - e.g. response that was `{"object_ids":["roadmap"]}`, will now be `{"objects":["document:roadmap"]}` + e.g. response that was `{"object_ids":["roadmap"]}`, will now be `{"objects":["document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"]}` Fixes: - fix(models): update interfaces that had incorrectly optional fields to make them required diff --git a/README.md b/README.md index a376d7a..983e8ef 100644 --- a/README.md +++ b/README.md @@ -405,13 +405,13 @@ Reads the relationship tuples stored in the database. It does not evaluate nor e var body = new ClientReadRequest() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", }; // Find all relationship tuples where a certain user has a relationship as any relation to a certain document var body = new ClientReadRequest() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", }; // Find all relationship tuples where a certain user is a viewer of any document @@ -424,7 +424,7 @@ var body = new ClientReadRequest() { // Find all relationship tuples where any user has a relationship as any relation with a particular document var body = new ClientReadRequest() { - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", }; // Read all stored relationship tuples @@ -457,19 +457,19 @@ var body = new ClientWriteRequest() { new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", }, new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:budget", + Object = "document:0192ab2d-d36e-7cb3-a4a8-5d1d67a300c5", } }, Deletes = new List { new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "writer", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", } }, }; @@ -492,19 +492,19 @@ var body = new ClientWriteRequest() { new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", }, new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:budget", + Object = "document:0192ab2d-d36e-7cb3-a4a8-5d1d67a300c5", } }, Deletes = new List { new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "writer", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", } }, }; @@ -536,7 +536,7 @@ var options = new ClientCheckOptions { var body = new ClientCheckRequest { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "writer", - Object = "document:roadmap" + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" }; var response = await fgaClient.Check(body, options); // response.Allowed = true @@ -557,36 +557,36 @@ var body = new List(){ new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", ContextualTuples = new List() { new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "editor", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", } }, }, new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "admin", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", ContextualTuples = new List() { new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "editor", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", } }, }, new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "creator", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", }, new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "deleter", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", } }; @@ -598,11 +598,11 @@ response.Responses = [{ Request: { User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation: "viewer", - Object: "document:roadmap", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", ContextualTuples: [{ User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation: "editor", - Object: "document:roadmap" + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" }] } }, { @@ -610,11 +610,11 @@ response.Responses = [{ Request: { User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation: "admin", - Object: "document:roadmap", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", ContextualTuples: [{ User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation: "editor", - Object: "document:roadmap" + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" }] } }, { @@ -622,7 +622,7 @@ response.Responses = [{ Request: { User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation: "creator", - Object: "document:roadmap", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", }, Error: }, { @@ -630,7 +630,7 @@ response.Responses = [{ Request: { User: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation: "deleter", - Object: "document:roadmap", + Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", }}, ] */ @@ -649,11 +649,11 @@ var options = new ClientCheckOptions { }; var body = new ClientExpandRequest { Relation = "viewer", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", }; var response = await fgaClient.Expand(body, options); -// response.Tree.Root = {"name":"document:roadmap#viewer","leaf":{"users":{"users":["user:81684243-9356-4421-8fbf-a4f8d36aa31b","user:f52a4f7a-054d-47ff-bb6e-3ac81269988f"]}}} +// response.Tree.Root = {"name":"document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a#viewer","leaf":{"users":{"users":["user:81684243-9356-4421-8fbf-a4f8d36aa31b","user:f52a4f7a-054d-47ff-bb6e-3ac81269988f"]}}} ``` ##### List Objects @@ -675,13 +675,13 @@ var body = new ClientListObjectsRequest { new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "writer", - Object = "document:budget", + Object = "document:0192ab2d-d36e-7cb3-a4a8-5d1d67a300c5", }, }, }; var response = await fgaClient.ListObjects(body, options); -// response.Objects = ["document:roadmap"] +// response.Objects = ["document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"] ``` ##### List Relations @@ -692,13 +692,13 @@ List the relations a user has on an object. ListRelationsRequest body = new ListRelationsRequest() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", Relations = new List {"can_view", "can_edit", "can_delete", "can_rename"}, ContextualTuples = new List() { new ClientTupleKey { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "editor", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", } } }; @@ -742,7 +742,7 @@ const response = await fgaClient.listUsers({ }, { user: "folder:product", relation: "parent", - object: "document:roadmap" + object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" }] }, options); @@ -779,7 +779,7 @@ var options = new ClientWriteAssertionsOptions { var body = new List() {new ClientAssertion() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", Expectation = true, }}; await fgaClient.WriteAssertions(body, options); @@ -851,6 +851,7 @@ namespace Example { - [Model.Any](docs/Any.md) - [Model.Assertion](docs/Assertion.md) - [Model.AssertionTupleKey](docs/AssertionTupleKey.md) + - [Model.AuthErrorCode](docs/AuthErrorCode.md) - [Model.AuthorizationModel](docs/AuthorizationModel.md) - [Model.CheckRequest](docs/CheckRequest.md) - [Model.CheckRequestTupleKey](docs/CheckRequestTupleKey.md) @@ -869,6 +870,7 @@ namespace Example { - [Model.ExpandRequestTupleKey](docs/ExpandRequestTupleKey.md) - [Model.ExpandResponse](docs/ExpandResponse.md) - [Model.FgaObject](docs/FgaObject.md) + - [Model.ForbiddenResponse](docs/ForbiddenResponse.md) - [Model.GetStoreResponse](docs/GetStoreResponse.md) - [Model.InternalErrorCode](docs/InternalErrorCode.md) - [Model.InternalErrorMessageResponse](docs/InternalErrorMessageResponse.md) diff --git a/docs/Assertion.md b/docs/Assertion.md index 2a6031b..a2b5459 100644 --- a/docs/Assertion.md +++ b/docs/Assertion.md @@ -6,6 +6,8 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **TupleKey** | [**AssertionTupleKey**](AssertionTupleKey.md) | | **Expectation** | **bool** | | +**ContextualTuples** | [**List<TupleKey>**](TupleKey.md) | | [optional] +**Context** | **Object** | Additional request context that will be used to evaluate any ABAC conditions encountered in the query evaluation. | [optional] [[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) diff --git a/docs/AuthErrorCode.md b/docs/AuthErrorCode.md new file mode 100644 index 0000000..13d0f01 --- /dev/null +++ b/docs/AuthErrorCode.md @@ -0,0 +1,9 @@ +# OpenFga.Sdk.Model.AuthErrorCode + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- + +[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) + diff --git a/docs/ConsistencyPreference.md b/docs/ConsistencyPreference.md index 4c95048..c6049be 100644 --- a/docs/ConsistencyPreference.md +++ b/docs/ConsistencyPreference.md @@ -1,5 +1,5 @@ # OpenFga.Sdk.Model.ConsistencyPreference -- UNSPECIFIED: Default if not set. Behavior will be the same as MINIMIZE_LATENCY - MINIMIZE_LATENCY: Minimize latency at the potential expense of lower consistency. - HIGHER_CONSISTENCY: Prefer higher consistency, at the potential expense of increased latency. +Controls the consistency preferences when calling the query APIs. - UNSPECIFIED: Default if not set. Behavior will be the same as MINIMIZE_LATENCY. - MINIMIZE_LATENCY: Minimize latency at the potential expense of lower consistency. - HIGHER_CONSISTENCY: Prefer higher consistency, at the potential expense of increased latency. ## Properties diff --git a/docs/ForbiddenResponse.md b/docs/ForbiddenResponse.md new file mode 100644 index 0000000..1d51214 --- /dev/null +++ b/docs/ForbiddenResponse.md @@ -0,0 +1,11 @@ +# OpenFga.Sdk.Model.ForbiddenResponse + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Code** | **AuthErrorCode** | | [optional] +**Message** | **string** | | [optional] + +[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) + diff --git a/docs/OpenFgaApi.md b/docs/OpenFgaApi.md index 0e34e98..c935b38 100644 --- a/docs/OpenFgaApi.md +++ b/docs/OpenFgaApi.md @@ -28,7 +28,7 @@ Method | HTTP request | Description Check whether a user is authorized to access an object -The Check API returns whether a given user has a relationship with a given object in a given store. The `user` field of the request can be a specific target, such as `user:anne`, or a userset (set of users) such as `group:marketing#member` or a type-bound public access `user:*`. To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). A `contextual_tuples` object may also be included in the body of the request. This object contains one field `tuple_keys`, which is an array of tuple keys. Each of these tuples may have an associated `condition`. You may also provide an `authorization_model_id` in the body. This will be used to assert that the input `tuple_key` is valid for the model specified. If not specified, the assertion will be made against the latest authorization model ID. It is strongly recommended to specify authorization model id for better performance. You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. The response will return whether the relationship exists in the field `allowed`. Some exceptions apply, but in general, if a Check API responds with `{allowed: true}`, then you can expect the equivalent ListObjects query to return the object, and viceversa. For example, if `Check(user:anne, reader, document:2021-budget)` responds with `{allowed: true}`, then `ListObjects(user:anne, reader, document)` may include `document:2021-budget` in the response. ## Examples ### Querying with contextual tuples In order to check if user `user:anne` of type `user` has a `reader` relationship with object `document:2021-budget` given the following contextual tuple ```json { \"user\": \"user:anne\", \"relation\": \"member\", \"object\": \"time_slot:office_hours\" } ``` the Check API can be used with the following request body: ```json { \"tuple_key\": { \"user\": \"user:anne\", \"relation\": \"reader\", \"object\": \"document:2021-budget\" }, \"contextual_tuples\": { \"tuple_keys\": [ { \"user\": \"user:anne\", \"relation\": \"member\", \"object\": \"time_slot:office_hours\" } ] }, \"authorization_model_id\": \"01G50QVV17PECNVAHX1GG4Y5NC\" } ``` ### Querying usersets Some Checks will always return `true`, even without any tuples. For example, for the following authorization model ```python model schema 1.1 type user type document relations define reader: [user] ``` the following query ```json { \"tuple_key\": { \"user\": \"document:2021-budget#reader\", \"relation\": \"reader\", \"object\": \"document:2021-budget\" } } ``` will always return `{ \"allowed\": true }`. This is because usersets are self-defining: the userset `document:2021-budget#reader` will always have the `reader` relation with `document:2021-budget`. ### Querying usersets with difference in the model A Check for a userset can yield results that must be treated carefully if the model involves difference. For example, for the following authorization model ```python model schema 1.1 type user type group relations define member: [user] type document relations define blocked: [user] define reader: [group#member] but not blocked ``` the following query ```json { \"tuple_key\": { \"user\": \"group:finance#member\", \"relation\": \"reader\", \"object\": \"document:2021-budget\" }, \"contextual_tuples\": { \"tuple_keys\": [ { \"user\": \"user:anne\", \"relation\": \"member\", \"object\": \"group:finance\" }, { \"user\": \"group:finance#member\", \"relation\": \"reader\", \"object\": \"document:2021-budget\" }, { \"user\": \"user:anne\", \"relation\": \"blocked\", \"object\": \"document:2021-budget\" } ] }, } ``` will return `{ \"allowed\": true }`, even though a specific user of the userset `group:finance#member` does not have the `reader` relationship with the given object. +The Check API returns whether a given user has a relationship with a given object in a given store. The `user` field of the request can be a specific target, such as `user:anne`, or a userset (set of users) such as `group:marketing#member` or a type-bound public access `user:*`. To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). A `contextual_tuples` object may also be included in the body of the request. This object contains one field `tuple_keys`, which is an array of tuple keys. Each of these tuples may have an associated `condition`. You may also provide an `authorization_model_id` in the body. This will be used to assert that the input `tuple_key` is valid for the model specified. If not specified, the assertion will be made against the latest authorization model ID. It is strongly recommended to specify authorization model id for better performance. You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. By default, the Check API caches results for a short time to optimize performance. You may specify a value of `HIGHER_CONSISTENCY` for the optional `consistency` parameter in the body to inform the server that higher conisistency is preferred at the expense of increased latency. Consideration should be given to the increased latency if requesting higher consistency. The response will return whether the relationship exists in the field `allowed`. Some exceptions apply, but in general, if a Check API responds with `{allowed: true}`, then you can expect the equivalent ListObjects query to return the object, and viceversa. For example, if `Check(user:anne, reader, document:2021-budget)` responds with `{allowed: true}`, then `ListObjects(user:anne, reader, document)` may include `document:2021-budget` in the response. ## Examples ### Querying with contextual tuples In order to check if user `user:anne` of type `user` has a `reader` relationship with object `document:2021-budget` given the following contextual tuple ```json { \"user\": \"user:anne\", \"relation\": \"member\", \"object\": \"time_slot:office_hours\" } ``` the Check API can be used with the following request body: ```json { \"tuple_key\": { \"user\": \"user:anne\", \"relation\": \"reader\", \"object\": \"document:2021-budget\" }, \"contextual_tuples\": { \"tuple_keys\": [ { \"user\": \"user:anne\", \"relation\": \"member\", \"object\": \"time_slot:office_hours\" } ] }, \"authorization_model_id\": \"01G50QVV17PECNVAHX1GG4Y5NC\" } ``` ### Querying usersets Some Checks will always return `true`, even without any tuples. For example, for the following authorization model ```python model schema 1.1 type user type document relations define reader: [user] ``` the following query ```json { \"tuple_key\": { \"user\": \"document:2021-budget#reader\", \"relation\": \"reader\", \"object\": \"document:2021-budget\" } } ``` will always return `{ \"allowed\": true }`. This is because usersets are self-defining: the userset `document:2021-budget#reader` will always have the `reader` relation with `document:2021-budget`. ### Querying usersets with difference in the model A Check for a userset can yield results that must be treated carefully if the model involves difference. For example, for the following authorization model ```python model schema 1.1 type user type group relations define member: [user] type document relations define blocked: [user] define reader: [group#member] but not blocked ``` the following query ```json { \"tuple_key\": { \"user\": \"group:finance#member\", \"relation\": \"reader\", \"object\": \"document:2021-budget\" }, \"contextual_tuples\": { \"tuple_keys\": [ { \"user\": \"user:anne\", \"relation\": \"member\", \"object\": \"group:finance\" }, { \"user\": \"group:finance#member\", \"relation\": \"reader\", \"object\": \"document:2021-budget\" }, { \"user\": \"user:anne\", \"relation\": \"blocked\", \"object\": \"document:2021-budget\" } ] }, } ``` will return `{ \"allowed\": true }`, even though a specific user of the userset `group:finance#member` does not have the `reader` relationship with the given object. ### Requesting higher consistency By default, the Check API caches results for a short time to optimize performance. You may request higher consistency to inform the server that higher consistency should be preferred at the expense of increased latency. Care should be taken when requesting higher consistency due to the increased latency. ```json { \"tuple_key\": { \"user\": \"group:finance#member\", \"relation\": \"reader\", \"object\": \"document:2021-budget\" }, \"consistency\": \"HIGHER_CONSISTENCY\" } ``` ### Example ```csharp @@ -95,6 +95,7 @@ Name | Type | Description | Notes | **200** | A successful response. | - | | **400** | Request failed due to invalid input. | - | | **401** | Not authenticated. | - | +| **403** | Forbidden. | - | | **404** | Request failed due to incorrect path. | - | | **409** | Request was aborted due a transaction conflict. | - | | **422** | Request timed out due to excessive request throttling. | - | @@ -173,6 +174,7 @@ Name | Type | Description | Notes | **201** | A successful response. | - | | **400** | Request failed due to invalid input. | - | | **401** | Not authenticated. | - | +| **403** | Forbidden. | - | | **404** | Request failed due to incorrect path. | - | | **409** | Request was aborted due a transaction conflict. | - | | **422** | Request timed out due to excessive request throttling. | - | @@ -250,6 +252,7 @@ void (empty response body) | **204** | A successful response. | - | | **400** | Request failed due to invalid input. | - | | **401** | Not authenticated. | - | +| **403** | Forbidden. | - | | **404** | Request failed due to incorrect path. | - | | **409** | Request was aborted due a transaction conflict. | - | | **422** | Request timed out due to excessive request throttling. | - | @@ -330,6 +333,7 @@ Name | Type | Description | Notes | **200** | A successful response. | - | | **400** | Request failed due to invalid input. | - | | **401** | Not authenticated. | - | +| **403** | Forbidden. | - | | **404** | Request failed due to incorrect path. | - | | **409** | Request was aborted due a transaction conflict. | - | | **422** | Request timed out due to excessive request throttling. | - | @@ -408,6 +412,7 @@ Name | Type | Description | Notes | **200** | A successful response. | - | | **400** | Request failed due to invalid input. | - | | **401** | Not authenticated. | - | +| **403** | Forbidden. | - | | **404** | Request failed due to incorrect path. | - | | **409** | Request was aborted due a transaction conflict. | - | | **422** | Request timed out due to excessive request throttling. | - | @@ -421,7 +426,7 @@ Name | Type | Description | Notes List all objects of the given type that the user has a relation with -The ListObjects API returns a list of all the objects of the given type that the user has a relation with. To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. You may also specify `contextual_tuples` that will be treated as regular tuples. Each of these tuples may have an associated `condition`. You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. The response will contain the related objects in an array in the \"objects\" field of the response and they will be strings in the object format `:` (e.g. \"document:roadmap\"). The number of objects in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_OBJECTS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_OBJECTS_MAX_RESULTS, whichever is hit first. The objects given will not be sorted, and therefore two identical calls can give a given different set of objects. +The ListObjects API returns a list of all the objects of the given type that the user has a relation with. To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. You may also specify `contextual_tuples` that will be treated as regular tuples. Each of these tuples may have an associated `condition`. You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. By default, the Check API caches results for a short time to optimize performance. You may specify a value of `HIGHER_CONSISTENCY` for the optional `consistency` parameter in the body to inform the server that higher conisistency is preferred at the expense of increased latency. Consideration should be given to the increased latency if requesting higher consistency. The response will contain the related objects in an array in the \"objects\" field of the response and they will be strings in the object format `:` (e.g. \"document:roadmap\"). The number of objects in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_OBJECTS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_OBJECTS_MAX_RESULTS, whichever is hit first. The objects given will not be sorted, and therefore two identical calls can give a given different set of objects. ### Example ```csharp @@ -488,6 +493,7 @@ Name | Type | Description | Notes | **200** | A successful response. | - | | **400** | Request failed due to invalid input. | - | | **401** | Not authenticated. | - | +| **403** | Forbidden. | - | | **404** | Request failed due to incorrect path. | - | | **409** | Request was aborted due a transaction conflict. | - | | **422** | Request timed out due to excessive request throttling. | - | @@ -568,6 +574,7 @@ Name | Type | Description | Notes | **200** | A successful response. | - | | **400** | Request failed due to invalid input. | - | | **401** | Not authenticated. | - | +| **403** | Forbidden. | - | | **404** | Request failed due to incorrect path. | - | | **409** | Request was aborted due a transaction conflict. | - | | **422** | Request timed out due to excessive request throttling. | - | @@ -581,7 +588,7 @@ Name | Type | Description | Notes List the users matching the provided filter who have a certain relation to a particular type. -The ListUsers API returns a list of all the users of a specific type that have a relation to a given object. To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. You may also specify `contextual_tuples` that will be treated as regular tuples. Each of these tuples may have an associated `condition`. You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. The response will contain the related users in an array in the \"users\" field of the response. These results may include specific objects, usersets or type-bound public access. Each of these types of results is encoded in its own type and not represented as a string.In cases where a type-bound public acces result is returned (e.g. `user:*`), it cannot be inferred that all subjects of that type have a relation to the object; it is possible that negations exist and checks should still be queried on individual subjects to ensure access to that document.The number of users in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_USERS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_USERS_MAX_RESULTS, whichever is hit first. The returned users will not be sorted, and therefore two identical calls may yield different sets of users. +The ListUsers API returns a list of all the users of a specific type that have a relation to a given object. To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. You may also specify `contextual_tuples` that will be treated as regular tuples. Each of these tuples may have an associated `condition`. You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. The response will contain the related users in an array in the \"users\" field of the response. These results may include specific objects, usersets or type-bound public access. Each of these types of results is encoded in its own type and not represented as a string.In cases where a type-bound public access result is returned (e.g. `user:*`), it cannot be inferred that all subjects of that type have a relation to the object; it is possible that negations exist and checks should still be queried on individual subjects to ensure access to that document.The number of users in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_USERS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_USERS_MAX_RESULTS, whichever is hit first. The returned users will not be sorted, and therefore two identical calls may yield different sets of users. ### Example ```csharp @@ -648,6 +655,7 @@ Name | Type | Description | Notes | **200** | A successful response. | - | | **400** | Request failed due to invalid input. | - | | **401** | Not authenticated. | - | +| **403** | Forbidden. | - | | **404** | Request failed due to incorrect path. | - | | **409** | Request was aborted due a transaction conflict. | - | | **422** | Request timed out due to excessive request throttling. | - | @@ -728,6 +736,7 @@ Name | Type | Description | Notes | **200** | A successful response. | - | | **400** | Request failed due to invalid input. | - | | **401** | Not authenticated. | - | +| **403** | Forbidden. | - | | **404** | Request failed due to incorrect path. | - | | **409** | Request was aborted due a transaction conflict. | - | | **422** | Request timed out due to excessive request throttling. | - | @@ -741,7 +750,7 @@ Name | Type | Description | Notes Read assertions for an authorization model ID -The ReadAssertions API will return, for a given authorization model id, all the assertions stored for it. An assertion is an object that contains a tuple key, and the expectation of whether a call to the Check API of that tuple key will return true or false. +The ReadAssertions API will return, for a given authorization model id, all the assertions stored for it. ### Example ```csharp @@ -808,6 +817,7 @@ Name | Type | Description | Notes | **200** | A successful response. | - | | **400** | Request failed due to invalid input. | - | | **401** | Not authenticated. | - | +| **403** | Forbidden. | - | | **404** | Request failed due to incorrect path. | - | | **409** | Request was aborted due a transaction conflict. | - | | **422** | Request timed out due to excessive request throttling. | - | @@ -888,6 +898,7 @@ Name | Type | Description | Notes | **200** | A successful response. | - | | **400** | Request failed due to invalid input. | - | | **401** | Not authenticated. | - | +| **403** | Forbidden. | - | | **404** | Request failed due to incorrect path. | - | | **409** | Request was aborted due a transaction conflict. | - | | **422** | Request timed out due to excessive request throttling. | - | @@ -970,6 +981,7 @@ Name | Type | Description | Notes | **200** | A successful response. | - | | **400** | Request failed due to invalid input. | - | | **401** | Not authenticated. | - | +| **403** | Forbidden. | - | | **404** | Request failed due to incorrect path. | - | | **409** | Request was aborted due a transaction conflict. | - | | **422** | Request timed out due to excessive request throttling. | - | @@ -1054,6 +1066,7 @@ Name | Type | Description | Notes | **200** | A successful response. | - | | **400** | Request failed due to invalid input. | - | | **401** | Not authenticated. | - | +| **403** | Forbidden. | - | | **404** | Request failed due to incorrect path. | - | | **409** | Request was aborted due a transaction conflict. | - | | **422** | Request timed out due to excessive request throttling. | - | @@ -1134,6 +1147,7 @@ Name | Type | Description | Notes | **200** | A successful response. | - | | **400** | Request failed due to invalid input. | - | | **401** | Not authenticated. | - | +| **403** | Forbidden. | - | | **404** | Request failed due to incorrect path. | - | | **409** | Request was aborted due a transaction conflict. | - | | **422** | Request timed out due to excessive request throttling. | - | @@ -1147,7 +1161,7 @@ Name | Type | Description | Notes Upsert assertions for an authorization model ID -The WriteAssertions API will upsert new assertions for an authorization model id, or overwrite the existing ones. An assertion is an object that contains a tuple key, and the expectation of whether a call to the Check API of that tuple key will return true or false. +The WriteAssertions API will upsert new assertions for an authorization model id, or overwrite the existing ones. An assertion is an object that contains a tuple key, the expectation of whether a call to the Check API of that tuple key will return true or false, and optionally a list of contextual tuples. ### Example ```csharp @@ -1215,6 +1229,7 @@ void (empty response body) | **204** | A successful response. | - | | **400** | Request failed due to invalid input. | - | | **401** | Not authenticated. | - | +| **403** | Forbidden. | - | | **404** | Request failed due to incorrect path. | - | | **409** | Request was aborted due a transaction conflict. | - | | **422** | Request timed out due to excessive request throttling. | - | @@ -1295,6 +1310,7 @@ Name | Type | Description | Notes | **201** | A successful response. | - | | **400** | Request failed due to invalid input. | - | | **401** | Not authenticated. | - | +| **403** | Forbidden. | - | | **404** | Request failed due to incorrect path. | - | | **409** | Request was aborted due a transaction conflict. | - | | **422** | Request timed out due to excessive request throttling. | - | diff --git a/example/Example1/Example1.cs b/example/Example1/Example1.cs index 26bfc54..3324677 100644 --- a/example/Example1/Example1.cs +++ b/example/Example1/Example1.cs @@ -160,7 +160,7 @@ await fgaClient.Write(new ClientWriteRequest { new() { User = "user:anne", Relation = "writer", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", Condition = new RelationshipCondition() { Name = "ViewCountLessThan200", Context = new { Name = "Roadmap", Type = "document" } @@ -188,7 +188,7 @@ await fgaClient.Write(new ClientWriteRequest { var failingCheckResponse = await fgaClient.Check(new ClientCheckRequest { User = "user:anne", Relation = "viewer", - Object = "document:roadmap" + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" }); Console.WriteLine("Allowed: " + failingCheckResponse.Allowed); } @@ -201,7 +201,7 @@ await fgaClient.Write(new ClientWriteRequest { var checkResponse = await fgaClient.Check(new ClientCheckRequest { User = "user:anne", Relation = "viewer", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", Context = new { ViewCount = 100 } }); Console.WriteLine("Allowed: " + checkResponse.Allowed); @@ -217,7 +217,7 @@ await fgaClient.WriteAssertions(new List() { new ClientAssertion() { User = "user:anne", Relation = "viewer", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", Expectation = false, } }); diff --git a/src/OpenFga.Sdk.Test/Api/OpenFgaApiTests.cs b/src/OpenFga.Sdk.Test/Api/OpenFgaApiTests.cs index 85892f6..b825fed 100644 --- a/src/OpenFga.Sdk.Test/Api/OpenFgaApiTests.cs +++ b/src/OpenFga.Sdk.Test/Api/OpenFgaApiTests.cs @@ -604,7 +604,7 @@ public async Task FgaApiValidationErrorTest() { TupleKey = new CheckRequestTupleKey() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap" + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" }, AuthorizationModelId = "01GXSA8YR785C4FYS3C0RTG7B1" }; @@ -658,7 +658,7 @@ public async Task FgaApiInternalErrorTest() { TupleKey = new CheckRequestTupleKey() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap" + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" }, AuthorizationModelId = "01GXSA8YR785C4FYS3C0RTG7B1" }; @@ -711,7 +711,7 @@ public async Task FgaApiInternalErrorRetrySuccessTest() { TupleKey = new CheckRequestTupleKey() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap" + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" } }; @@ -756,7 +756,7 @@ public async Task FgaNotFoundErrorTest() { TupleKey = new CheckRequestTupleKey() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap" + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" } }; @@ -803,7 +803,7 @@ public async Task FgaApiErrorTest() { TupleKey = new CheckRequestTupleKey() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap" + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" } }; @@ -857,7 +857,7 @@ public async Task FgaApiRateLimitExceededErrorTest() { TupleKey = new CheckRequestTupleKey() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap" + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" } }; @@ -906,7 +906,7 @@ public async Task FgaApiRateLimitExceededErrorRetrySuccessTest() { TupleKey = new CheckRequestTupleKey() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap" + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" } }; @@ -955,7 +955,7 @@ public async Task RetryOnRateLimitTest() { TupleKey = new CheckRequestTupleKey() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap" + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" } }; @@ -1181,7 +1181,7 @@ public async Task CheckTest() { TupleKey = new CheckRequestTupleKey() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap" + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" }, AuthorizationModelId = "01GXSA8YR785C4FYS3C0RTG7B1" }; @@ -1227,7 +1227,7 @@ public async Task WriteWriteTest() { Writes = new WriteRequestWrites(new List { new () { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap" + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" } }), AuthorizationModelId = "01GXSA8YR785C4FYS3C0RTG7B1" }; @@ -1269,7 +1269,7 @@ public async Task WriteDeleteTest() { Deletes = new WriteRequestDeletes(new List { new () { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap" + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" } }), AuthorizationModelId = "01GXSA8YR785C4FYS3C0RTG7B1" }; @@ -1311,11 +1311,11 @@ public async Task WriteMixedWithAuthorizationModelIdTest() { Writes = new WriteRequestWrites(new List { new () { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "writer", - Object = "document:roadmap" + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" }, new () { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap" + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" } }), AuthorizationModelId = "01GXSA8YR785C4FYS3C0RTG7B1" }; @@ -1339,7 +1339,7 @@ public async Task ExpandTest() { var mockHandler = new Mock(MockBehavior.Strict); var jsonResponse = - "{\"tree\":{\"root\":{\"name\":\"document:roadmap#owner\", \"union\":{\"nodes\":[{\"name\":\"document:roadmap#owner\", \"leaf\":{\"users\":{\"users\":[\"team:product#member\"]}}}, {\"name\":\"document:roadmap#owner\", \"leaf\":{\"tupleToUserset\":{\"tupleset\":\"document:roadmap#owner\", \"computed\":[{\"userset\":\"org:contoso#admin\"}]}}}]}}}}"; + "{\"tree\":{\"root\":{\"name\":\"document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a#owner\", \"union\":{\"nodes\":[{\"name\":\"document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a#owner\", \"leaf\":{\"users\":{\"users\":[\"team:product#member\"]}}}, {\"name\":\"document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a#owner\", \"leaf\":{\"tupleToUserset\":{\"tupleset\":\"document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a#owner\", \"computed\":[{\"userset\":\"org:contoso#admin\"}]}}}]}}}}"; mockHandler.Protected() .Setup>( "SendAsync", @@ -1356,7 +1356,7 @@ public async Task ExpandTest() { var httpClient = new HttpClient(mockHandler.Object); var openFgaApi = new OpenFgaApi(_config, httpClient); - var body = new ExpandRequest { TupleKey = new ExpandRequestTupleKey(_object: "document:roadmap", relation: "viewer"), AuthorizationModelId = "01GXSA8YR785C4FYS3C0RTG7B1" }; + var body = new ExpandRequest { TupleKey = new ExpandRequestTupleKey(_object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", relation: "viewer"), AuthorizationModelId = "01GXSA8YR785C4FYS3C0RTG7B1" }; var response = await openFgaApi.Expand(_storeId, body); mockHandler.Protected().Verify( @@ -1381,22 +1381,22 @@ public async Task ExpandComplexResponseTest() { var mockHandler = new Mock(MockBehavior.Strict); var mockResponse = new ExpandResponse( tree: new UsersetTree( - root: new Node(name: "document:roadmap1#owner", + root: new Node(name: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a1#owner", union: new Nodes( nodes: new List() { - new Node(name: "document:roadmap2#owner", leaf: new Leaf(users: new Users(users: new List(){"team:product#member"}))), - new Node(name: "document:roadmap3#owner", leaf: new Leaf(tupleToUserset: new UsersetTreeTupleToUserset(tupleset: "document:roadmap#owner", computed: new List(){ + new Node(name: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a2#owner", leaf: new Leaf(users: new Users(users: new List(){"team:product#member"}))), + new Node(name: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a3#owner", leaf: new Leaf(tupleToUserset: new UsersetTreeTupleToUserset(tupleset: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a#owner", computed: new List(){ new Computed(userset: "org:contoso#admin") }))), }), difference: new UsersetTreeDifference( - _base: new Node(name: "document:roadmap3#owner", leaf: new Leaf(users: new Users(users: new List() { "team:product#member" }))), - subtract: new Node(name: "document:roadmap4#owner", leaf: new Leaf(users: new Users(users: new List() { "team:product#member" }))) + _base: new Node(name: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a3#owner", leaf: new Leaf(users: new Users(users: new List() { "team:product#member" }))), + subtract: new Node(name: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a4#owner", leaf: new Leaf(users: new Users(users: new List() { "team:product#member" }))) ), intersection: new Nodes( nodes: new List() { - new Node(name: "document:roadmap5#owner", leaf: new Leaf(users: new Users(users: new List(){"team:product#commentor"}))), - new Node(name: "document:roadmap6#owner", leaf: new Leaf(tupleToUserset: new UsersetTreeTupleToUserset(tupleset: "document:roadmap#viewer", computed: new List(){ + new Node(name: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a5#owner", leaf: new Leaf(users: new Users(users: new List(){"team:product#commentor"}))), + new Node(name: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a6#owner", leaf: new Leaf(tupleToUserset: new UsersetTreeTupleToUserset(tupleset: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a#viewer", computed: new List(){ new Computed(userset: "org:contoso#owner") }))), })) @@ -1418,7 +1418,7 @@ public async Task ExpandComplexResponseTest() { var httpClient = new HttpClient(mockHandler.Object); var openFgaApi = new OpenFgaApi(_config, httpClient); - var body = new ExpandRequest(new ExpandRequestTupleKey(_object: "document:roadmap", relation: "viewer")); + var body = new ExpandRequest(new ExpandRequestTupleKey(_object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", relation: "viewer")); var response = await openFgaApi.Expand(_storeId, body); mockHandler.Protected().Verify( @@ -1440,7 +1440,7 @@ public async Task ExpandComplexResponseTest() { [Fact] public async Task ListObjectsTest() { var mockHandler = new Mock(MockBehavior.Strict); - var expectedResponse = new ListObjectsResponse { Objects = new List { "document:roadmap" } }; + var expectedResponse = new ListObjectsResponse { Objects = new List { "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" } }; mockHandler.Protected() .Setup>( "SendAsync", @@ -1465,7 +1465,7 @@ public async Task ListObjectsTest() { ContextualTuples = new ContextualTupleKeys() { TupleKeys = new List { new("folder:product", "editor", "user:81684243-9356-4421-8fbf-a4f8d36aa31b"), - new("document:roadmap", "parent", "folder:product") + new("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", "parent", "folder:product") } } }; @@ -1545,7 +1545,7 @@ public async Task ListUsersTest() { }, ContextualTuples = new List { new("folder:product", "editor", "user:81684243-9356-4421-8fbf-a4f8d36aa31b"), - new("document:roadmap", "parent", "folder:product") + new("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", "parent", "folder:product") }, Context = new { ViewCount = 100 } }; @@ -1576,7 +1576,7 @@ public async Task ReadTest() { new(new TupleKey { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap" + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" }, DateTime.Now) } }; @@ -1600,7 +1600,7 @@ public async Task ReadTest() { TupleKey = new ReadRequestTupleKey { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap" + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" }, }; var response = await openFgaApi.Read(_storeId, body); @@ -1630,7 +1630,7 @@ public async Task ReadEmptyTest() { new(new TupleKey { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap" + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" }, DateTime.Now) } }; @@ -1678,7 +1678,7 @@ public async Task ReadChangesTest() { new(new TupleKey { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap" + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" }, TupleOperation.WRITE, DateTime.Now), }, ContinuationToken = "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" @@ -1748,7 +1748,7 @@ public async Task WriteAssertionsTest() { new (new AssertionTupleKey { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap" + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" }) }, }; diff --git a/src/OpenFga.Sdk.Test/Client/OpenFgaClientTests.cs b/src/OpenFga.Sdk.Test/Client/OpenFgaClientTests.cs index 584d6cc..3d732c1 100644 --- a/src/OpenFga.Sdk.Test/Client/OpenFgaClientTests.cs +++ b/src/OpenFga.Sdk.Test/Client/OpenFgaClientTests.cs @@ -619,7 +619,7 @@ public async Task ReadChangesTest() { new(new TupleKey { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap" + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" }, TupleOperation.WRITE, DateTime.Now), }, @@ -678,7 +678,7 @@ public async Task ReadTest() { new(new TupleKey { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap" + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" }, DateTime.Now) } @@ -703,7 +703,7 @@ public async Task ReadTest() { var body = new ClientReadRequest() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", }; var options = new ClientReadOptions { }; var response = await fgaClient.Read(body, options); @@ -734,7 +734,7 @@ public async Task ReadEmptyTest() { new(new TupleKey { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap" + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" }, DateTime.Now) } @@ -802,7 +802,7 @@ public async Task WriteWriteTest() { new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", } }, Deletes = new List(), // should not get passed @@ -848,7 +848,7 @@ public async Task WriteDeleteTest() { new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", } }, }; @@ -893,14 +893,14 @@ public async Task WriteMixedWithAuthorizationModelIdTest() { new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", }, }, Deletes = new List { new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "writer", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", } }, }; @@ -953,7 +953,7 @@ public async Task WriteNonTransactionTest() { new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", }, new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", @@ -965,7 +965,7 @@ public async Task WriteNonTransactionTest() { new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "writer", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", } }, }; @@ -1018,12 +1018,12 @@ public async Task CheckTest() { var body = new ClientCheckRequest { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", ContextualTuples = new List() { new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "editor", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", Condition = new RelationshipCondition() { Name = "ViewCountLessThan200", Context = new { Name = "Roadmap", Type = "document" } @@ -1074,12 +1074,12 @@ public async Task CheckWithConsistencyTest() { var body = new ClientCheckRequest { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", ContextualTuples = new List() { new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "editor", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", Condition = new RelationshipCondition() { Name = "ViewCountLessThan200", Context = new { Name = "Roadmap", Type = "document" } @@ -1157,36 +1157,36 @@ public async Task BatchCheckTest() { new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", ContextualTuples = new List() { new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "editor", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", } }, }, new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "admin", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", ContextualTuples = new List() { new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "editor", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", } }, }, new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "creator", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", }, new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "deleter", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", } }; var options = new ClientBatchCheckOptions { @@ -1222,7 +1222,7 @@ public async Task ExpandTest() { var mockHandler = new Mock(MockBehavior.Strict); var jsonResponse = - "{\"tree\":{\"root\":{\"name\":\"document:roadmap#owner\", \"union\":{\"nodes\":[{\"name\":\"document:roadmap#owner\", \"leaf\":{\"users\":{\"users\":[\"team:product#member\"]}}}, {\"name\":\"document:roadmap#owner\", \"leaf\":{\"tupleToUserset\":{\"tupleset\":\"document:roadmap#owner\", \"computed\":[{\"userset\":\"org:contoso#admin\"}]}}}]}}}}"; + "{\"tree\":{\"root\":{\"name\":\"document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a#owner\", \"union\":{\"nodes\":[{\"name\":\"document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a#owner\", \"leaf\":{\"users\":{\"users\":[\"team:product#member\"]}}}, {\"name\":\"document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a#owner\", \"leaf\":{\"tupleToUserset\":{\"tupleset\":\"document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a#owner\", \"computed\":[{\"userset\":\"org:contoso#admin\"}]}}}]}}}}"; mockHandler.Protected() .Setup>( "SendAsync", @@ -1242,7 +1242,7 @@ public async Task ExpandTest() { var body = new ClientExpandRequest { Relation = "viewer", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", }; var response = await fgaClient.Expand(body, new ClientExpandOptions() { AuthorizationModelId = "01GXSA8YR785C4FYS3C0RTG7B1", @@ -1272,29 +1272,29 @@ public async Task ExpandComplexResponseTest() { var mockHandler = new Mock(MockBehavior.Strict); var mockResponse = new ExpandResponse( tree: new UsersetTree( - root: new Node(name: "document:roadmap1#owner", + root: new Node(name: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a1#owner", union: new Nodes( nodes: new List() { - new Node(name: "document:roadmap2#owner", + new Node(name: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a2#owner", leaf: new Leaf(users: new Users(users: new List() {"team:product#member"}))), - new Node(name: "document:roadmap3#owner", + new Node(name: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a3#owner", leaf: new Leaf(tupleToUserset: new UsersetTreeTupleToUserset( - tupleset: "document:roadmap#owner", + tupleset: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a#owner", computed: new List() {new Computed(userset: "org:contoso#admin")}))), }), difference: new UsersetTreeDifference( - _base: new Node(name: "document:roadmap3#owner", + _base: new Node(name: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a3#owner", leaf: new Leaf(users: new Users(users: new List() { "team:product#member" }))), - subtract: new Node(name: "document:roadmap4#owner", + subtract: new Node(name: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a4#owner", leaf: new Leaf(users: new Users(users: new List() { "team:product#member" }))) ), intersection: new Nodes( nodes: new List() { - new Node(name: "document:roadmap5#owner", + new Node(name: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a5#owner", leaf: new Leaf(users: new Users(users: new List() {"team:product#commentor"}))), - new Node(name: "document:roadmap6#owner", + new Node(name: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a6#owner", leaf: new Leaf(tupleToUserset: new UsersetTreeTupleToUserset( - tupleset: "document:roadmap#viewer", + tupleset: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a#viewer", computed: new List() {new Computed(userset: "org:contoso#owner")}))), })) )); @@ -1317,7 +1317,7 @@ public async Task ExpandComplexResponseTest() { var body = new ClientExpandRequest { Relation = "viewer", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", }; var response = await fgaClient.Expand(body, new ClientExpandOptions { AuthorizationModelId = "01GXSA8YR785C4FYS3C0RTG7B1", @@ -1342,7 +1342,7 @@ public async Task ExpandComplexResponseTest() { [Fact] public async Task ListObjectsTest() { var mockHandler = new Mock(MockBehavior.Strict); - var expectedResponse = new ListObjectsResponse { Objects = new List { "document:roadmap" } }; + var expectedResponse = new ListObjectsResponse { Objects = new List { "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a" } }; mockHandler.Protected() .Setup>( "SendAsync", @@ -1373,7 +1373,7 @@ public async Task ListObjectsTest() { new() { User = "folder:product", Relation = "parent", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", }, }, }; @@ -1435,13 +1435,13 @@ public async Task ListRelationsTest() { var body = new ClientListRelationsRequest() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", Relations = new List { "can_view", "can_edit", "can_delete", "can_rename" }, ContextualTuples = new List() { new() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "editor", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", } } }; @@ -1489,7 +1489,7 @@ public async Task ListRelationsNoRelationsProvidedTest() { var body = new ClientListRelationsRequest() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", Relations = new List { }, }; @@ -1577,7 +1577,7 @@ public async Task ListUsersTest() { new() { User = "folder:product", Relation = "parent", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", }, }, Context = new { ViewCount = 100 } @@ -1674,7 +1674,7 @@ public async Task WriteAssertionsTest() { var body = new List() {new ClientAssertion() { User = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", Relation = "viewer", - Object = "document:roadmap", + Object = "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", Expectation = true, }}; diff --git a/src/OpenFga.Sdk.Test/Models/ModelTests.cs b/src/OpenFga.Sdk.Test/Models/ModelTests.cs index 6d126b1..17b878d 100644 --- a/src/OpenFga.Sdk.Test/Models/ModelTests.cs +++ b/src/OpenFga.Sdk.Test/Models/ModelTests.cs @@ -72,7 +72,7 @@ public void DeserializeReadResponse() { [Fact] public void DeserializeReadChangesResponse() { var jsonResponse = - "{\"changes\":[{\"tuple_key\":{\"object\":\"document:planning\",\"relation\":\"viewer\",\"user\":\"user:jane\"},\"operation\":\"TUPLE_OPERATION_WRITE\",\"timestamp\":\"2022-01-01T00:00:00.000000000Z\"},{\"tuple_key\":{\"object\":\"document:roadmap\",\"relation\":\"owner\",\"user\":\"user:anna\"},\"operation\":\"TUPLE_OPERATION_DELETE\",\"timestamp\":\"2022-01-01T00:00:00.000000000Z\"}],\"continuation_token\":\"abcxyz==\"}"; + "{\"changes\":[{\"tuple_key\":{\"object\":\"document:planning\",\"relation\":\"viewer\",\"user\":\"user:jane\"},\"operation\":\"TUPLE_OPERATION_WRITE\",\"timestamp\":\"2022-01-01T00:00:00.000000000Z\"},{\"tuple_key\":{\"object\":\"document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a\",\"relation\":\"owner\",\"user\":\"user:anna\"},\"operation\":\"TUPLE_OPERATION_DELETE\",\"timestamp\":\"2022-01-01T00:00:00.000000000Z\"}],\"continuation_token\":\"abcxyz==\"}"; JsonSerializer.Deserialize(jsonResponse); } @@ -94,7 +94,7 @@ public void DeserializeCheckResponse() { [Fact] public void DeserializeReadAssertionsResponse() { var jsonResponse = - "{\"authorization_model_id\":\"01FQHMTEX3ASF7TAGZZ828KSQ2\",\"assertions\":[{\"tuple_key\":{\"object\":\"document:roadmap\",\"relation\":\"viewer\",\"user\":\"carlos\"},\"expectation\":true}]}"; + "{\"authorization_model_id\":\"01FQHMTEX3ASF7TAGZZ828KSQ2\",\"assertions\":[{\"tuple_key\":{\"object\":\"document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a\",\"relation\":\"viewer\",\"user\":\"carlos\"},\"expectation\":true}]}"; JsonSerializer.Deserialize(jsonResponse); } @@ -105,7 +105,7 @@ public void DeserializeReadAssertionsResponse() { [Fact] public void DeserializeExpandResponse() { var jsonResponse = - "{\"tree\":{\"root\":{\"name\":\"document:roadmap#owner\", \"union\":{\"nodes\":[{\"name\":\"document:roadmap#owner\", \"leaf\":{\"users\":{\"users\":[\"team:product#member\"]}}}, {\"name\":\"document:roadmap#owner\", \"leaf\":{\"tupleToUserset\":{\"tupleset\":\"document:roadmap#owner\", \"computed\":[{\"userset\":\"org:contoso#admin\"}]}}}]}}}}"; + "{\"tree\":{\"root\":{\"name\":\"document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a#owner\", \"union\":{\"nodes\":[{\"name\":\"document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a#owner\", \"leaf\":{\"users\":{\"users\":[\"team:product#member\"]}}}, {\"name\":\"document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a#owner\", \"leaf\":{\"tupleToUserset\":{\"tupleset\":\"document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a#owner\", \"computed\":[{\"userset\":\"org:contoso#admin\"}]}}}]}}}}"; JsonSerializer.Deserialize(jsonResponse); } diff --git a/src/OpenFga.Sdk.Test/OpenFga.Sdk.Test.csproj b/src/OpenFga.Sdk.Test/OpenFga.Sdk.Test.csproj index 3dff5f3..cc3e9df 100644 --- a/src/OpenFga.Sdk.Test/OpenFga.Sdk.Test.csproj +++ b/src/OpenFga.Sdk.Test/OpenFga.Sdk.Test.csproj @@ -9,7 +9,7 @@ - all + all all all diff --git a/src/OpenFga.Sdk/Api/OpenFgaApi.cs b/src/OpenFga.Sdk/Api/OpenFgaApi.cs index f4fed49..4d66dbb 100644 --- a/src/OpenFga.Sdk/Api/OpenFgaApi.cs +++ b/src/OpenFga.Sdk/Api/OpenFgaApi.cs @@ -36,7 +36,7 @@ public OpenFgaApi( } /// - /// Check whether a user is authorized to access an object The Check API returns whether a given user has a relationship with a given object in a given store. The `user` field of the request can be a specific target, such as `user:anne`, or a userset (set of users) such as `group:marketing#member` or a type-bound public access `user:*`. To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). A `contextual_tuples` object may also be included in the body of the request. This object contains one field `tuple_keys`, which is an array of tuple keys. Each of these tuples may have an associated `condition`. You may also provide an `authorization_model_id` in the body. This will be used to assert that the input `tuple_key` is valid for the model specified. If not specified, the assertion will be made against the latest authorization model ID. It is strongly recommended to specify authorization model id for better performance. You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. The response will return whether the relationship exists in the field `allowed`. Some exceptions apply, but in general, if a Check API responds with `{allowed: true}`, then you can expect the equivalent ListObjects query to return the object, and viceversa. For example, if `Check(user:anne, reader, document:2021-budget)` responds with `{allowed: true}`, then `ListObjects(user:anne, reader, document)` may include `document:2021-budget` in the response. ## Examples ### Querying with contextual tuples In order to check if user `user:anne` of type `user` has a `reader` relationship with object `document:2021-budget` given the following contextual tuple ```json { \"user\": \"user:anne\", \"relation\": \"member\", \"object\": \"time_slot:office_hours\" } ``` the Check API can be used with the following request body: ```json { \"tuple_key\": { \"user\": \"user:anne\", \"relation\": \"reader\", \"object\": \"document:2021-budget\" }, \"contextual_tuples\": { \"tuple_keys\": [ { \"user\": \"user:anne\", \"relation\": \"member\", \"object\": \"time_slot:office_hours\" } ] }, \"authorization_model_id\": \"01G50QVV17PECNVAHX1GG4Y5NC\" } ``` ### Querying usersets Some Checks will always return `true`, even without any tuples. For example, for the following authorization model ```python model schema 1.1 type user type document relations define reader: [user] ``` the following query ```json { \"tuple_key\": { \"user\": \"document:2021-budget#reader\", \"relation\": \"reader\", \"object\": \"document:2021-budget\" } } ``` will always return `{ \"allowed\": true }`. This is because usersets are self-defining: the userset `document:2021-budget#reader` will always have the `reader` relation with `document:2021-budget`. ### Querying usersets with difference in the model A Check for a userset can yield results that must be treated carefully if the model involves difference. For example, for the following authorization model ```python model schema 1.1 type user type group relations define member: [user] type document relations define blocked: [user] define reader: [group#member] but not blocked ``` the following query ```json { \"tuple_key\": { \"user\": \"group:finance#member\", \"relation\": \"reader\", \"object\": \"document:2021-budget\" }, \"contextual_tuples\": { \"tuple_keys\": [ { \"user\": \"user:anne\", \"relation\": \"member\", \"object\": \"group:finance\" }, { \"user\": \"group:finance#member\", \"relation\": \"reader\", \"object\": \"document:2021-budget\" }, { \"user\": \"user:anne\", \"relation\": \"blocked\", \"object\": \"document:2021-budget\" } ] }, } ``` will return `{ \"allowed\": true }`, even though a specific user of the userset `group:finance#member` does not have the `reader` relationship with the given object. + /// Check whether a user is authorized to access an object The Check API returns whether a given user has a relationship with a given object in a given store. The `user` field of the request can be a specific target, such as `user:anne`, or a userset (set of users) such as `group:marketing#member` or a type-bound public access `user:*`. To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). A `contextual_tuples` object may also be included in the body of the request. This object contains one field `tuple_keys`, which is an array of tuple keys. Each of these tuples may have an associated `condition`. You may also provide an `authorization_model_id` in the body. This will be used to assert that the input `tuple_key` is valid for the model specified. If not specified, the assertion will be made against the latest authorization model ID. It is strongly recommended to specify authorization model id for better performance. You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. By default, the Check API caches results for a short time to optimize performance. You may specify a value of `HIGHER_CONSISTENCY` for the optional `consistency` parameter in the body to inform the server that higher conisistency is preferred at the expense of increased latency. Consideration should be given to the increased latency if requesting higher consistency. The response will return whether the relationship exists in the field `allowed`. Some exceptions apply, but in general, if a Check API responds with `{allowed: true}`, then you can expect the equivalent ListObjects query to return the object, and viceversa. For example, if `Check(user:anne, reader, document:2021-budget)` responds with `{allowed: true}`, then `ListObjects(user:anne, reader, document)` may include `document:2021-budget` in the response. ## Examples ### Querying with contextual tuples In order to check if user `user:anne` of type `user` has a `reader` relationship with object `document:2021-budget` given the following contextual tuple ```json { \"user\": \"user:anne\", \"relation\": \"member\", \"object\": \"time_slot:office_hours\" } ``` the Check API can be used with the following request body: ```json { \"tuple_key\": { \"user\": \"user:anne\", \"relation\": \"reader\", \"object\": \"document:2021-budget\" }, \"contextual_tuples\": { \"tuple_keys\": [ { \"user\": \"user:anne\", \"relation\": \"member\", \"object\": \"time_slot:office_hours\" } ] }, \"authorization_model_id\": \"01G50QVV17PECNVAHX1GG4Y5NC\" } ``` ### Querying usersets Some Checks will always return `true`, even without any tuples. For example, for the following authorization model ```python model schema 1.1 type user type document relations define reader: [user] ``` the following query ```json { \"tuple_key\": { \"user\": \"document:2021-budget#reader\", \"relation\": \"reader\", \"object\": \"document:2021-budget\" } } ``` will always return `{ \"allowed\": true }`. This is because usersets are self-defining: the userset `document:2021-budget#reader` will always have the `reader` relation with `document:2021-budget`. ### Querying usersets with difference in the model A Check for a userset can yield results that must be treated carefully if the model involves difference. For example, for the following authorization model ```python model schema 1.1 type user type group relations define member: [user] type document relations define blocked: [user] define reader: [group#member] but not blocked ``` the following query ```json { \"tuple_key\": { \"user\": \"group:finance#member\", \"relation\": \"reader\", \"object\": \"document:2021-budget\" }, \"contextual_tuples\": { \"tuple_keys\": [ { \"user\": \"user:anne\", \"relation\": \"member\", \"object\": \"group:finance\" }, { \"user\": \"group:finance#member\", \"relation\": \"reader\", \"object\": \"document:2021-budget\" }, { \"user\": \"user:anne\", \"relation\": \"blocked\", \"object\": \"document:2021-budget\" } ] }, } ``` will return `{ \"allowed\": true }`, even though a specific user of the userset `group:finance#member` does not have the `reader` relationship with the given object. ### Requesting higher consistency By default, the Check API caches results for a short time to optimize performance. You may request higher consistency to inform the server that higher consistency should be preferred at the expense of increased latency. Care should be taken when requesting higher consistency due to the increased latency. ```json { \"tuple_key\": { \"user\": \"group:finance#member\", \"relation\": \"reader\", \"object\": \"document:2021-budget\" }, \"consistency\": \"HIGHER_CONSISTENCY\" } ``` /// /// Thrown when fails to make API call /// @@ -185,7 +185,7 @@ public async Task GetStore(string storeId, CancellationToken c } /// - /// List all objects of the given type that the user has a relation with The ListObjects API returns a list of all the objects of the given type that the user has a relation with. To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. You may also specify `contextual_tuples` that will be treated as regular tuples. Each of these tuples may have an associated `condition`. You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. The response will contain the related objects in an array in the \"objects\" field of the response and they will be strings in the object format `<type>:<id>` (e.g. \"document:roadmap\"). The number of objects in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_OBJECTS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_OBJECTS_MAX_RESULTS, whichever is hit first. The objects given will not be sorted, and therefore two identical calls can give a given different set of objects. + /// List all objects of the given type that the user has a relation with The ListObjects API returns a list of all the objects of the given type that the user has a relation with. To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. You may also specify `contextual_tuples` that will be treated as regular tuples. Each of these tuples may have an associated `condition`. You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. By default, the Check API caches results for a short time to optimize performance. You may specify a value of `HIGHER_CONSISTENCY` for the optional `consistency` parameter in the body to inform the server that higher conisistency is preferred at the expense of increased latency. Consideration should be given to the increased latency if requesting higher consistency. The response will contain the related objects in an array in the \"objects\" field of the response and they will be strings in the object format `<type>:<id>` (e.g. \"document:roadmap\"). The number of objects in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_OBJECTS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_OBJECTS_MAX_RESULTS, whichever is hit first. The objects given will not be sorted, and therefore two identical calls can give a given different set of objects. /// /// Thrown when fails to make API call /// @@ -248,7 +248,7 @@ public async Task ListObjects(string storeId, ListObjectsRe } /// - /// List the users matching the provided filter who have a certain relation to a particular type. The ListUsers API returns a list of all the users of a specific type that have a relation to a given object. To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. You may also specify `contextual_tuples` that will be treated as regular tuples. Each of these tuples may have an associated `condition`. You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. The response will contain the related users in an array in the \"users\" field of the response. These results may include specific objects, usersets or type-bound public access. Each of these types of results is encoded in its own type and not represented as a string.In cases where a type-bound public acces result is returned (e.g. `user:*`), it cannot be inferred that all subjects of that type have a relation to the object; it is possible that negations exist and checks should still be queried on individual subjects to ensure access to that document.The number of users in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_USERS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_USERS_MAX_RESULTS, whichever is hit first. The returned users will not be sorted, and therefore two identical calls may yield different sets of users. + /// List the users matching the provided filter who have a certain relation to a particular type. The ListUsers API returns a list of all the users of a specific type that have a relation to a given object. To arrive at a result, the API uses: an authorization model, explicit tuples written through the Write API, contextual tuples present in the request, and implicit tuples that exist by virtue of applying set theory (such as `document:2021-budget#viewer@document:2021-budget#viewer`; the set of users who are viewers of `document:2021-budget` are the set of users who are the viewers of `document:2021-budget`). An `authorization_model_id` may be specified in the body. If it is not specified, the latest authorization model ID will be used. It is strongly recommended to specify authorization model id for better performance. You may also specify `contextual_tuples` that will be treated as regular tuples. Each of these tuples may have an associated `condition`. You may also provide a `context` object that will be used to evaluate the conditioned tuples in the system. It is strongly recommended to provide a value for all the input parameters of all the conditions, to ensure that all tuples be evaluated correctly. The response will contain the related users in an array in the \"users\" field of the response. These results may include specific objects, usersets or type-bound public access. Each of these types of results is encoded in its own type and not represented as a string.In cases where a type-bound public access result is returned (e.g. `user:*`), it cannot be inferred that all subjects of that type have a relation to the object; it is possible that negations exist and checks should still be queried on individual subjects to ensure access to that document.The number of users in the response array will be limited by the execution timeout specified in the flag OPENFGA_LIST_USERS_DEADLINE and by the upper bound specified in the flag OPENFGA_LIST_USERS_MAX_RESULTS, whichever is hit first. The returned users will not be sorted, and therefore two identical calls may yield different sets of users. /// /// Thrown when fails to make API call /// @@ -312,7 +312,7 @@ public async Task Read(string storeId, ReadRequest body, Cancellat } /// - /// Read assertions for an authorization model ID The ReadAssertions API will return, for a given authorization model id, all the assertions stored for it. An assertion is an object that contains a tuple key, and the expectation of whether a call to the Check API of that tuple key will return true or false. + /// Read assertions for an authorization model ID The ReadAssertions API will return, for a given authorization model id, all the assertions stored for it. /// /// Thrown when fails to make API call /// @@ -492,7 +492,7 @@ public async Task Write(string storeId, WriteRequest body, CancellationT } /// - /// Upsert assertions for an authorization model ID The WriteAssertions API will upsert new assertions for an authorization model id, or overwrite the existing ones. An assertion is an object that contains a tuple key, and the expectation of whether a call to the Check API of that tuple key will return true or false. + /// Upsert assertions for an authorization model ID The WriteAssertions API will upsert new assertions for an authorization model id, or overwrite the existing ones. An assertion is an object that contains a tuple key, the expectation of whether a call to the Check API of that tuple key will return true or false, and optionally a list of contextual tuples. /// /// Thrown when fails to make API call /// diff --git a/src/OpenFga.Sdk/Model/Assertion.cs b/src/OpenFga.Sdk/Model/Assertion.cs index 0827ac6..b1da6ed 100644 --- a/src/OpenFga.Sdk/Model/Assertion.cs +++ b/src/OpenFga.Sdk/Model/Assertion.cs @@ -35,13 +35,17 @@ public Assertion() { /// /// tupleKey (required). /// expectation (required). - public Assertion(AssertionTupleKey tupleKey = default(AssertionTupleKey), bool expectation = default(bool)) { + /// contextualTuples. + /// Additional request context that will be used to evaluate any ABAC conditions encountered in the query evaluation.. + public Assertion(AssertionTupleKey tupleKey = default(AssertionTupleKey), bool expectation = default(bool), List contextualTuples = default(List), Object context = default(Object)) { // to ensure "tupleKey" is required (not null) if (tupleKey == null) { throw new ArgumentNullException("tupleKey is a required property for Assertion and cannot be null"); } this.TupleKey = tupleKey; this.Expectation = expectation; + this.ContextualTuples = contextualTuples; + this.Context = context; this.AdditionalProperties = new Dictionary(); } @@ -61,6 +65,23 @@ public Assertion() { [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public bool Expectation { get; set; } + /// + /// Gets or Sets ContextualTuples + /// + [DataMember(Name = "contextual_tuples", EmitDefaultValue = false)] + [JsonPropertyName("contextual_tuples")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public List? ContextualTuples { get; set; } + + /// + /// Additional request context that will be used to evaluate any ABAC conditions encountered in the query evaluation. + /// + /// Additional request context that will be used to evaluate any ABAC conditions encountered in the query evaluation. + [DataMember(Name = "context", EmitDefaultValue = false)] + [JsonPropertyName("context")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Object? Context { get; set; } + /// /// Gets or Sets additional properties /// @@ -111,6 +132,17 @@ public bool Equals(Assertion input) { ( this.Expectation == input.Expectation || this.Expectation.Equals(input.Expectation) + ) && + ( + this.ContextualTuples == input.ContextualTuples || + this.ContextualTuples != null && + input.ContextualTuples != null && + this.ContextualTuples.SequenceEqual(input.ContextualTuples) + ) && + ( + this.Context == input.Context || + (this.Context != null && + this.Context.Equals(input.Context)) ) && (this.AdditionalProperties.Count == input.AdditionalProperties.Count && !this.AdditionalProperties.Except(input.AdditionalProperties).Any()); } @@ -127,6 +159,12 @@ public override int GetHashCode() { hashCode = (hashCode * 9923) + this.TupleKey.GetHashCode(); } hashCode = (hashCode * 9923) + this.Expectation.GetHashCode(); + if (this.ContextualTuples != null) { + hashCode = (hashCode * 9923) + this.ContextualTuples.GetHashCode(); + } + if (this.Context != null) { + hashCode = (hashCode * 9923) + this.Context.GetHashCode(); + } if (this.AdditionalProperties != null) { hashCode = (hashCode * 9923) + this.AdditionalProperties.GetHashCode(); } diff --git a/src/OpenFga.Sdk/Model/AuthErrorCode.cs b/src/OpenFga.Sdk/Model/AuthErrorCode.cs new file mode 100644 index 0000000..2f902d8 --- /dev/null +++ b/src/OpenFga.Sdk/Model/AuthErrorCode.cs @@ -0,0 +1,79 @@ +// +// OpenFGA/.NET SDK for OpenFGA +// +// API version: 1.x +// Website: https://openfga.dev +// Documentation: https://openfga.dev/docs +// Support: https://openfga.dev/community +// License: [Apache-2.0](https://github.com/openfga/dotnet-sdk/blob/main/LICENSE) +// +// NOTE: This file was auto generated. DO NOT EDIT. +// + + +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace OpenFga.Sdk.Model { + /// + /// Defines AuthErrorCode + /// + [JsonConverter(typeof(JsonStringEnumMemberConverter))] + public enum AuthErrorCode { + /// + /// Enum NoAuthError for value: no_auth_error + /// + [EnumMember(Value = "no_auth_error")] + NoAuthError = 1, + + /// + /// Enum AuthFailedInvalidSubject for value: auth_failed_invalid_subject + /// + [EnumMember(Value = "auth_failed_invalid_subject")] + AuthFailedInvalidSubject = 2, + + /// + /// Enum AuthFailedInvalidAudience for value: auth_failed_invalid_audience + /// + [EnumMember(Value = "auth_failed_invalid_audience")] + AuthFailedInvalidAudience = 3, + + /// + /// Enum AuthFailedInvalidIssuer for value: auth_failed_invalid_issuer + /// + [EnumMember(Value = "auth_failed_invalid_issuer")] + AuthFailedInvalidIssuer = 4, + + /// + /// Enum InvalidClaims for value: invalid_claims + /// + [EnumMember(Value = "invalid_claims")] + InvalidClaims = 5, + + /// + /// Enum AuthFailedInvalidBearerToken for value: auth_failed_invalid_bearer_token + /// + [EnumMember(Value = "auth_failed_invalid_bearer_token")] + AuthFailedInvalidBearerToken = 6, + + /// + /// Enum BearerTokenMissing for value: bearer_token_missing + /// + [EnumMember(Value = "bearer_token_missing")] + BearerTokenMissing = 7, + + /// + /// Enum Unauthenticated for value: unauthenticated + /// + [EnumMember(Value = "unauthenticated")] + Unauthenticated = 8, + + /// + /// Enum Forbidden for value: forbidden + /// + [EnumMember(Value = "forbidden")] + Forbidden = 9 + + } + +} \ No newline at end of file diff --git a/src/OpenFga.Sdk/Model/ConsistencyPreference.cs b/src/OpenFga.Sdk/Model/ConsistencyPreference.cs index eb2c556..7a2e6aa 100644 --- a/src/OpenFga.Sdk/Model/ConsistencyPreference.cs +++ b/src/OpenFga.Sdk/Model/ConsistencyPreference.cs @@ -16,9 +16,9 @@ namespace OpenFga.Sdk.Model { /// - /// - UNSPECIFIED: Default if not set. Behavior will be the same as MINIMIZE_LATENCY - MINIMIZE_LATENCY: Minimize latency at the potential expense of lower consistency. - HIGHER_CONSISTENCY: Prefer higher consistency, at the potential expense of increased latency. + /// Controls the consistency preferences when calling the query APIs. - UNSPECIFIED: Default if not set. Behavior will be the same as MINIMIZE_LATENCY. - MINIMIZE_LATENCY: Minimize latency at the potential expense of lower consistency. - HIGHER_CONSISTENCY: Prefer higher consistency, at the potential expense of increased latency. /// - /// - UNSPECIFIED: Default if not set. Behavior will be the same as MINIMIZE_LATENCY - MINIMIZE_LATENCY: Minimize latency at the potential expense of lower consistency. - HIGHER_CONSISTENCY: Prefer higher consistency, at the potential expense of increased latency. + /// Controls the consistency preferences when calling the query APIs. - UNSPECIFIED: Default if not set. Behavior will be the same as MINIMIZE_LATENCY. - MINIMIZE_LATENCY: Minimize latency at the potential expense of lower consistency. - HIGHER_CONSISTENCY: Prefer higher consistency, at the potential expense of increased latency. [JsonConverter(typeof(JsonStringEnumMemberConverter))] public enum ConsistencyPreference { /// diff --git a/src/OpenFga.Sdk/Model/ErrorCode.cs b/src/OpenFga.Sdk/Model/ErrorCode.cs index ef0758b..b5b6b3a 100644 --- a/src/OpenFga.Sdk/Model/ErrorCode.cs +++ b/src/OpenFga.Sdk/Model/ErrorCode.cs @@ -306,7 +306,13 @@ public enum ErrorCode { /// Enum UnsupportedSchemaVersion for value: unsupported_schema_version /// [EnumMember(Value = "unsupported_schema_version")] - UnsupportedSchemaVersion = 48 + UnsupportedSchemaVersion = 48, + + /// + /// Enum Cancelled for value: cancelled + /// + [EnumMember(Value = "cancelled")] + Cancelled = 49 } diff --git a/src/OpenFga.Sdk/Model/ForbiddenResponse.cs b/src/OpenFga.Sdk/Model/ForbiddenResponse.cs new file mode 100644 index 0000000..3c06b28 --- /dev/null +++ b/src/OpenFga.Sdk/Model/ForbiddenResponse.cs @@ -0,0 +1,143 @@ +// +// OpenFGA/.NET SDK for OpenFGA +// +// API version: 1.x +// Website: https://openfga.dev +// Documentation: https://openfga.dev/docs +// Support: https://openfga.dev/community +// License: [Apache-2.0](https://github.com/openfga/dotnet-sdk/blob/main/LICENSE) +// +// NOTE: This file was auto generated. DO NOT EDIT. +// + + +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace OpenFga.Sdk.Model { + /// + /// ForbiddenResponse + /// + [DataContract(Name = "ForbiddenResponse")] + public partial class ForbiddenResponse : IEquatable, IValidatableObject { + + /// + /// Gets or Sets Code + /// + [DataMember(Name = "code", EmitDefaultValue = false)] + [JsonPropertyName("code")] + public AuthErrorCode? Code { get; set; } + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + public ForbiddenResponse() { + this.AdditionalProperties = new Dictionary(); + } + + /// + /// Initializes a new instance of the class. + /// + /// code. + /// message. + public ForbiddenResponse(AuthErrorCode? code = default(AuthErrorCode?), string message = default(string)) { + this.Code = code; + this.Message = message; + this.AdditionalProperties = new Dictionary(); + } + + /// + /// Gets or Sets Message + /// + [DataMember(Name = "message", EmitDefaultValue = false)] + [JsonPropertyName("message")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string? Message { get; set; } + + /// + /// Gets or Sets additional properties + /// + [JsonExtensionData] + public IDictionary AdditionalProperties { get; set; } + + + /// + /// Returns the JSON string presentation of the object + /// + /// JSON string presentation of the object + public virtual string ToJson() { + return JsonSerializer.Serialize(this); + } + + /// + /// Builds a ForbiddenResponse from the JSON string presentation of the object + /// + /// ForbiddenResponse + public static ForbiddenResponse FromJson(string jsonString) { + return JsonSerializer.Deserialize(jsonString) ?? throw new InvalidOperationException(); + } + + /// + /// Returns true if objects are equal + /// + /// Object to be compared + /// Boolean + public override bool Equals(object input) { + return this.Equals(input as ForbiddenResponse); + } + + /// + /// Returns true if ForbiddenResponse instances are equal + /// + /// Instance of ForbiddenResponse to be compared + /// Boolean + public bool Equals(ForbiddenResponse input) { + if (input == null) { + return false; + } + return + ( + this.Code == input.Code || + this.Code.Equals(input.Code) + ) && + ( + this.Message == input.Message || + (this.Message != null && + this.Message.Equals(input.Message)) + ) + && (this.AdditionalProperties.Count == input.AdditionalProperties.Count && !this.AdditionalProperties.Except(input.AdditionalProperties).Any()); + } + + /// + /// Gets the hash code + /// + /// Hash code + public override int GetHashCode() { + unchecked // Overflow is fine, just wrap + { + int hashCode = 9661; + hashCode = (hashCode * 9923) + this.Code.GetHashCode(); + if (this.Message != null) { + hashCode = (hashCode * 9923) + this.Message.GetHashCode(); + } + if (this.AdditionalProperties != null) { + hashCode = (hashCode * 9923) + this.AdditionalProperties.GetHashCode(); + } + return hashCode; + } + } + + /// + /// To validate all properties of the instance + /// + /// Validation context + /// Validation Result + public IEnumerable Validate(ValidationContext validationContext) { + yield break; + } + + } + +} \ No newline at end of file diff --git a/src/OpenFga.Sdk/Model/InternalErrorCode.cs b/src/OpenFga.Sdk/Model/InternalErrorCode.cs index 34b55ac..9991f61 100644 --- a/src/OpenFga.Sdk/Model/InternalErrorCode.cs +++ b/src/OpenFga.Sdk/Model/InternalErrorCode.cs @@ -32,59 +32,53 @@ public enum InternalErrorCode { [EnumMember(Value = "internal_error")] InternalError = 2, - /// - /// Enum Cancelled for value: cancelled - /// - [EnumMember(Value = "cancelled")] - Cancelled = 3, - /// /// Enum DeadlineExceeded for value: deadline_exceeded /// [EnumMember(Value = "deadline_exceeded")] - DeadlineExceeded = 4, + DeadlineExceeded = 3, /// /// Enum AlreadyExists for value: already_exists /// [EnumMember(Value = "already_exists")] - AlreadyExists = 5, + AlreadyExists = 4, /// /// Enum ResourceExhausted for value: resource_exhausted /// [EnumMember(Value = "resource_exhausted")] - ResourceExhausted = 6, + ResourceExhausted = 5, /// /// Enum FailedPrecondition for value: failed_precondition /// [EnumMember(Value = "failed_precondition")] - FailedPrecondition = 7, + FailedPrecondition = 6, /// /// Enum Aborted for value: aborted /// [EnumMember(Value = "aborted")] - Aborted = 8, + Aborted = 7, /// /// Enum OutOfRange for value: out_of_range /// [EnumMember(Value = "out_of_range")] - OutOfRange = 9, + OutOfRange = 8, /// /// Enum Unavailable for value: unavailable /// [EnumMember(Value = "unavailable")] - Unavailable = 10, + Unavailable = 9, /// /// Enum DataLoss for value: data_loss /// [EnumMember(Value = "data_loss")] - DataLoss = 11 + DataLoss = 10 } diff --git a/src/OpenFga.Sdk/Model/ReadRequest.cs b/src/OpenFga.Sdk/Model/ReadRequest.cs index f605138..173553e 100644 --- a/src/OpenFga.Sdk/Model/ReadRequest.cs +++ b/src/OpenFga.Sdk/Model/ReadRequest.cs @@ -168,6 +168,16 @@ public override int GetHashCode() { /// Validation context /// Validation Result public IEnumerable Validate(ValidationContext validationContext) { + // PageSize (int) maximum + if (this.PageSize > 100) { + yield return new ValidationResult("Invalid value for PageSize, must be a value less than or equal to 100.", new[] { "PageSize" }); + } + + // PageSize (int) minimum + if (this.PageSize < 1) { + yield return new ValidationResult("Invalid value for PageSize, must be a value greater than or equal to 1.", new[] { "PageSize" }); + } + yield break; } diff --git a/src/OpenFga.Sdk/Model/TypeName.cs b/src/OpenFga.Sdk/Model/TypeName.cs index b86937c..c18f2a2 100644 --- a/src/OpenFga.Sdk/Model/TypeName.cs +++ b/src/OpenFga.Sdk/Model/TypeName.cs @@ -14,76 +14,84 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -namespace OpenFga.Sdk.Model; - -/// -/// Defines TypeName -/// -[JsonConverter(typeof(JsonStringEnumMemberConverter))] -public enum TypeName { +namespace OpenFga.Sdk.Model { /// - /// Enum UNSPECIFIED for value: TYPE_NAME_UNSPECIFIED + /// Defines TypeName /// - [EnumMember(Value = "TYPE_NAME_UNSPECIFIED")] - TypeName_UNSPECIFIED = 1, + [JsonConverter(typeof(JsonStringEnumMemberConverter))] + public enum TypeName { + /// + /// Enum UNSPECIFIED for value: TYPE_NAME_UNSPECIFIED + /// + [EnumMember(Value = "TYPE_NAME_UNSPECIFIED")] + UNSPECIFIED = 1, - /// - /// Enum ANY for value: TYPE_NAME_ANY - /// - [EnumMember(Value = "TYPE_NAME_ANY")] TypeName_ANY = 2, + /// + /// Enum ANY for value: TYPE_NAME_ANY + /// + [EnumMember(Value = "TYPE_NAME_ANY")] + ANY = 2, - /// - /// Enum BOOL for value: TYPE_NAME_BOOL - /// - [EnumMember(Value = "TYPE_NAME_BOOL")] TypeName_BOOL = 3, + /// + /// Enum BOOL for value: TYPE_NAME_BOOL + /// + [EnumMember(Value = "TYPE_NAME_BOOL")] + BOOL = 3, - /// - /// Enum STRING for value: TYPE_NAME_STRING - /// - [EnumMember(Value = "TYPE_NAME_STRING")] - STRING = 4, + /// + /// Enum STRING for value: TYPE_NAME_STRING + /// + [EnumMember(Value = "TYPE_NAME_STRING")] + STRING = 4, - /// - /// Enum INT for value: TYPE_NAME_INT - /// - [EnumMember(Value = "TYPE_NAME_INT")] INT = 5, + /// + /// Enum INT for value: TYPE_NAME_INT + /// + [EnumMember(Value = "TYPE_NAME_INT")] + INT = 5, - /// - /// Enum UINT for value: TYPE_NAME_UINT - /// - [EnumMember(Value = "TYPE_NAME_UINT")] UINT = 6, + /// + /// Enum UINT for value: TYPE_NAME_UINT + /// + [EnumMember(Value = "TYPE_NAME_UINT")] + UINT = 6, - /// - /// Enum DOUBLE for value: TYPE_NAME_DOUBLE - /// - [EnumMember(Value = "TYPE_NAME_DOUBLE")] - DOUBLE = 7, + /// + /// Enum DOUBLE for value: TYPE_NAME_DOUBLE + /// + [EnumMember(Value = "TYPE_NAME_DOUBLE")] + DOUBLE = 7, - /// - /// Enum DURATION for value: TYPE_NAME_DURATION - /// - [EnumMember(Value = "TYPE_NAME_DURATION")] - DURATION = 8, + /// + /// Enum DURATION for value: TYPE_NAME_DURATION + /// + [EnumMember(Value = "TYPE_NAME_DURATION")] + DURATION = 8, - /// - /// Enum TIMESTAMP for value: TYPE_NAME_TIMESTAMP - /// - [EnumMember(Value = "TYPE_NAME_TIMESTAMP")] - TIMESTAMP = 9, + /// + /// Enum TIMESTAMP for value: TYPE_NAME_TIMESTAMP + /// + [EnumMember(Value = "TYPE_NAME_TIMESTAMP")] + TIMESTAMP = 9, - /// - /// Enum MAP for value: TYPE_NAME_MAP - /// - [EnumMember(Value = "TYPE_NAME_MAP")] MAP = 10, + /// + /// Enum MAP for value: TYPE_NAME_MAP + /// + [EnumMember(Value = "TYPE_NAME_MAP")] + MAP = 10, - /// - /// Enum LIST for value: TYPE_NAME_LIST - /// - [EnumMember(Value = "TYPE_NAME_LIST")] LIST = 11, + /// + /// Enum LIST for value: TYPE_NAME_LIST + /// + [EnumMember(Value = "TYPE_NAME_LIST")] + LIST = 11, + + /// + /// Enum IPADDRESS for value: TYPE_NAME_IPADDRESS + /// + [EnumMember(Value = "TYPE_NAME_IPADDRESS")] + IPADDRESS = 12 + + } - /// - /// Enum IPADDRESS for value: TYPE_NAME_IPADDRESS - /// - [EnumMember(Value = "TYPE_NAME_IPADDRESS")] - IPADDRESS = 12 } \ No newline at end of file diff --git a/src/OpenFga.Sdk/OpenFga.Sdk.csproj b/src/OpenFga.Sdk/OpenFga.Sdk.csproj index 93050ff..2a7db59 100644 --- a/src/OpenFga.Sdk/OpenFga.Sdk.csproj +++ b/src/OpenFga.Sdk/OpenFga.Sdk.csproj @@ -34,7 +34,7 @@ - + From 2061130f4c51d0aeb81f20fce7a380bd6174c41e Mon Sep 17 00:00:00 2001 From: ovindu-a Date: Thu, 6 Feb 2025 10:32:00 -0600 Subject: [PATCH 2/8] fix : change default retry policy --- README.md | 6 +++--- src/OpenFga.Sdk/Configuration/Configuration.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 983e8ef..9c1199f 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ Search for and install `OpenFga.Sdk` in each of their respective package manager We strongly recommend you initialize the `OpenFgaClient` only once and then re-use it throughout your app, otherwise you will incur the cost of having to re-initialize multiple times or at every request, the cost of reduced connection pooling and re-use, and would be particularly costly in the client credentials flow, as that flow will be preformed on every request. -> The `OpenFga.SdkClient` will by default retry API requests up to 15 times on 429 and 5xx errors. +> The `OpenFga.SdkClient` will by default retry API requests up to 3 times on 429 and 5xx errors. #### No Credentials @@ -545,7 +545,7 @@ var response = await fgaClient.Check(body, options); ##### Batch Check Run a set of [checks](#check). Batch Check will return `allowed: false` if it encounters an error, and will return the error in the body. -If 429s or 5xxs are encountered, the underlying check will retry up to 15 times before giving up. +If 429s or 5xxs are encountered, the underlying check will retry up to 3 times before giving up. ```csharp var options = new ClientBatchCheckOptions { @@ -788,7 +788,7 @@ await fgaClient.WriteAssertions(body, options); ### Retries -If a network request fails with a 429 or 5xx error from the server, the SDK will automatically retry the request up to 15 times with a minimum wait time of 100 milliseconds between each attempt. +If a network request fails with a 429 or 5xx error from the server, the SDK will automatically retry the request up to 3 times with a minimum wait time of 100 milliseconds between each attempt. To customize this behavior, create a `RetryParams` instance and assign values to the `MaxRetry` and `MinWaitInMs` constructor parameters. `MaxRetry` determines the maximum number of retries (up to 15), while `MinWaitInMs` sets the minimum wait time between retries in milliseconds. diff --git a/src/OpenFga.Sdk/Configuration/Configuration.cs b/src/OpenFga.Sdk/Configuration/Configuration.cs index f9042df..bc993a1 100644 --- a/src/OpenFga.Sdk/Configuration/Configuration.cs +++ b/src/OpenFga.Sdk/Configuration/Configuration.cs @@ -148,7 +148,7 @@ public string BasePath { /// Max number of times to retry after a request is rate limited /// /// MaxRetry - public int MaxRetry { get; set; } = 15; + public int MaxRetry { get; set; } = 3; /// /// Minimum time in ms to wait before retrying From eac70e1c609a584c8d75b10ea393c60afe48a77b Mon Sep 17 00:00:00 2001 From: Ryan Quinn Date: Thu, 6 Feb 2025 10:35:10 -0600 Subject: [PATCH 3/8] feat: support start time in read changes request --- README.md | 3 ++- docs/OpenFgaApi.md | 6 ++++-- src/OpenFga.Sdk.Test/Api/OpenFgaApiTests.cs | 3 ++- src/OpenFga.Sdk.Test/Client/OpenFgaClientTests.cs | 3 ++- src/OpenFga.Sdk/Api/OpenFgaApi.cs | 6 +++++- src/OpenFga.Sdk/Client/Client.cs | 4 ++-- src/OpenFga.Sdk/Client/Model/ClientReadChangesRequest.cs | 2 ++ src/OpenFga.Sdk/Model/ErrorCode.cs | 8 +++++++- 8 files changed, 26 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 9c1199f..4412209 100644 --- a/README.md +++ b/README.md @@ -379,7 +379,8 @@ Reads the list of historical relationship tuple writes and deletes. [API Documentation](https://openfga.dev/api/service#/Relationship%20Tuples/ReadChanges) ```csharp -var body = new ClientReadChangesRequest { Type = "document" }; +var startTime = DateTime.Parse("2022-01-01T00:00:00Z"); +var body = new ClientReadChangesRequest { Type = "document", StartTime = startTime }; var options = new ClientReadChangesOptions { PageSize = 10, ContinuationToken = "...", diff --git a/docs/OpenFgaApi.md b/docs/OpenFgaApi.md index c935b38..938f67d 100644 --- a/docs/OpenFgaApi.md +++ b/docs/OpenFgaApi.md @@ -991,7 +991,7 @@ Name | Type | Description | Notes # **ReadChanges** -> ReadChangesResponse ReadChanges (string? type = null, int? pageSize = null, string? continuationToken = null) +> ReadChangesResponse ReadChanges (string? type = null, int? pageSize = null, string? continuationToken = null, DateTime? startTime = null) Return a list of all the tuple changes @@ -1023,11 +1023,12 @@ namespace Example var type = "type_example"; // string? | (optional) var pageSize = 56; // int? | (optional) var continuationToken = "continuationToken_example"; // string? | (optional) + var startTime = DateTime.Parse("2013-10-20T19:20:30+01:00"); // DateTime? | Start date and time of changes to read. Format: ISO 8601 timestamp (e.g., 2022-01-01T00:00:00Z) If a continuation_token is provided along side start_time, the continuation_token will take precedence over start_time. (optional) try { // Return a list of all the tuple changes - ReadChangesResponse response = await openFgaApi.ReadChanges(type, pageSize, continuationToken); + ReadChangesResponse response = await openFgaApi.ReadChanges(type, pageSize, continuationToken, startTime); Debug.WriteLine(response); } catch (ApiException e) @@ -1049,6 +1050,7 @@ Name | Type | Description | Notes **type** | **string?**| | [optional] **pageSize** | **int?**| | [optional] **continuationToken** | **string?**| | [optional] + **startTime** | **DateTime?**| Start date and time of changes to read. Format: ISO 8601 timestamp (e.g., 2022-01-01T00:00:00Z) If a continuation_token is provided along side start_time, the continuation_token will take precedence over start_time. | [optional] ### Return type diff --git a/src/OpenFga.Sdk.Test/Api/OpenFgaApiTests.cs b/src/OpenFga.Sdk.Test/Api/OpenFgaApiTests.cs index b825fed..5e8876c 100644 --- a/src/OpenFga.Sdk.Test/Api/OpenFgaApiTests.cs +++ b/src/OpenFga.Sdk.Test/Api/OpenFgaApiTests.cs @@ -1702,8 +1702,9 @@ public async Task ReadChangesTest() { var type = "repo"; var pageSize = 25; + var startTime = DateTime.Parse("2022-01-01T00:00:00Z"); var continuationToken = "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ=="; - var response = await openFgaApi.ReadChanges(_storeId, type, pageSize, continuationToken); + var response = await openFgaApi.ReadChanges(_storeId, type, pageSize, continuationToken, startTime); mockHandler.Protected().Verify( "SendAsync", diff --git a/src/OpenFga.Sdk.Test/Client/OpenFgaClientTests.cs b/src/OpenFga.Sdk.Test/Client/OpenFgaClientTests.cs index 3d732c1..c24a23a 100644 --- a/src/OpenFga.Sdk.Test/Client/OpenFgaClientTests.cs +++ b/src/OpenFga.Sdk.Test/Client/OpenFgaClientTests.cs @@ -645,9 +645,10 @@ public async Task ReadChangesTest() { var type = "repo"; var pageSize = 25; + var startTime = DateTime.Parse("2022-01-01T00:00:00Z"); var continuationToken = "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ=="; - var response = await fgaClient.ReadChanges(new ClientReadChangesRequest { Type = type }, new ClientReadChangesOptions { + var response = await fgaClient.ReadChanges(new ClientReadChangesRequest { Type = type, StartTime = startTime }, new ClientReadChangesOptions { PageSize = pageSize, ContinuationToken = continuationToken, }); diff --git a/src/OpenFga.Sdk/Api/OpenFgaApi.cs b/src/OpenFga.Sdk/Api/OpenFgaApi.cs index 4d66dbb..6875b5a 100644 --- a/src/OpenFga.Sdk/Api/OpenFgaApi.cs +++ b/src/OpenFga.Sdk/Api/OpenFgaApi.cs @@ -425,9 +425,10 @@ public async Task ReadAuthorizationModel(string /// (optional) /// (optional) /// (optional) + /// Start date and time of changes to read. Format: ISO 8601 timestamp (e.g., 2022-01-01T00:00:00Z) If a continuation_token is provided along side start_time, the continuation_token will take precedence over start_time. (optional) /// Cancellation Token to cancel the request. /// Task of ReadChangesResponse - public async Task ReadChanges(string storeId, string? type = default(string?), int? pageSize = default(int?), string? continuationToken = default(string?), CancellationToken cancellationToken = default) { + public async Task ReadChanges(string storeId, string? type = default(string?), int? pageSize = default(int?), string? continuationToken = default(string?), DateTime? startTime = default(DateTime?), CancellationToken cancellationToken = default) { var pathParams = new Dictionary { }; if (string.IsNullOrWhiteSpace(storeId)) { throw new FgaRequiredParamError("ReadChanges", "StoreId"); @@ -446,6 +447,9 @@ public async Task ReadAuthorizationModel(string if (continuationToken != null) { queryParams.Add("continuation_token", continuationToken.ToString()); } + if (startTime != null) { + queryParams.Add("start_time", startTime.ToString()); + } var requestBuilder = new RequestBuilder { Method = new HttpMethod("GET"), diff --git a/src/OpenFga.Sdk/Client/Client.cs b/src/OpenFga.Sdk/Client/Client.cs index 3e71470..6f7b3f2 100644 --- a/src/OpenFga.Sdk/Client/Client.cs +++ b/src/OpenFga.Sdk/Client/Client.cs @@ -164,9 +164,9 @@ public async Task ReadAuthorizationModel( * Read Changes - Read the list of historical relationship tuple writes and deletes */ public async Task ReadChanges(ClientReadChangesRequest? body = default, - IClientReadChangesOptions? options = default, + ClientReadChangesOptions? options = default, CancellationToken cancellationToken = default) => - await api.ReadChanges(GetStoreId(options), body?.Type, options?.PageSize, options?.ContinuationToken, cancellationToken); + await api.ReadChanges(GetStoreId(options), body?.Type, options?.PageSize, options?.ContinuationToken, body?.StartTime, cancellationToken); /** * Read - Read tuples previously written to the store (does not evaluate) diff --git a/src/OpenFga.Sdk/Client/Model/ClientReadChangesRequest.cs b/src/OpenFga.Sdk/Client/Model/ClientReadChangesRequest.cs index a154158..4ce84cf 100644 --- a/src/OpenFga.Sdk/Client/Model/ClientReadChangesRequest.cs +++ b/src/OpenFga.Sdk/Client/Model/ClientReadChangesRequest.cs @@ -18,11 +18,13 @@ namespace OpenFga.Sdk.Client.Model; public interface IClientReadChangesRequest { string Type { get; set; } + DateTime? StartTime { get; set; } } public class ClientReadChangesRequest : IClientReadChangesRequest, IEquatable, IValidatableObject { public string Type { get; set; } + public DateTime? StartTime { get; set; } public bool Equals(ClientReadChangesRequest input) { if (input == null) { diff --git a/src/OpenFga.Sdk/Model/ErrorCode.cs b/src/OpenFga.Sdk/Model/ErrorCode.cs index b5b6b3a..0d2318a 100644 --- a/src/OpenFga.Sdk/Model/ErrorCode.cs +++ b/src/OpenFga.Sdk/Model/ErrorCode.cs @@ -312,7 +312,13 @@ public enum ErrorCode { /// Enum Cancelled for value: cancelled /// [EnumMember(Value = "cancelled")] - Cancelled = 49 + Cancelled = 49, + + /// + /// Enum InvalidStartTime for value: invalid_start_time + /// + [EnumMember(Value = "invalid_start_time")] + InvalidStartTime = 50 } From f87cd47948e3f68aae1e89970189ad28cc585ca0 Mon Sep 17 00:00:00 2001 From: Ryan Quinn Date: Thu, 6 Feb 2025 10:44:11 -0600 Subject: [PATCH 4/8] chore: update changelog with unreleased changes in #92 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1b2d55..b70dc71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## [Unreleased](https://github.com/openfga/dotnet-sdk/compare/v0.5.1...HEAD) +- feat: support start time option in read changes requests +- feat: change default retry policy - thanks @ovindu-a ## v0.5.1 From 8baeae75c4c02055772ae4b66fc48e688e4f470d Mon Sep 17 00:00:00 2001 From: Ryan Quinn Date: Thu, 6 Feb 2025 10:54:41 -0600 Subject: [PATCH 5/8] fix: Restored deps updates --- src/OpenFga.Sdk.Test/OpenFga.Sdk.Test.csproj | 2 +- src/OpenFga.Sdk/OpenFga.Sdk.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenFga.Sdk.Test/OpenFga.Sdk.Test.csproj b/src/OpenFga.Sdk.Test/OpenFga.Sdk.Test.csproj index cc3e9df..3dff5f3 100644 --- a/src/OpenFga.Sdk.Test/OpenFga.Sdk.Test.csproj +++ b/src/OpenFga.Sdk.Test/OpenFga.Sdk.Test.csproj @@ -9,7 +9,7 @@ - all + all all all diff --git a/src/OpenFga.Sdk/OpenFga.Sdk.csproj b/src/OpenFga.Sdk/OpenFga.Sdk.csproj index 2a7db59..93050ff 100644 --- a/src/OpenFga.Sdk/OpenFga.Sdk.csproj +++ b/src/OpenFga.Sdk/OpenFga.Sdk.csproj @@ -34,7 +34,7 @@ - + From b57886861737d5cf4a725f0ea1e50048149afeef Mon Sep 17 00:00:00 2001 From: Ryan Quinn Date: Fri, 21 Feb 2025 11:33:57 -0600 Subject: [PATCH 6/8] fix: Addressing toString use for ISO8601 --- src/OpenFga.Sdk/Api/OpenFgaApi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFga.Sdk/Api/OpenFgaApi.cs b/src/OpenFga.Sdk/Api/OpenFgaApi.cs index 6875b5a..2f2c0d8 100644 --- a/src/OpenFga.Sdk/Api/OpenFgaApi.cs +++ b/src/OpenFga.Sdk/Api/OpenFgaApi.cs @@ -448,7 +448,7 @@ public async Task ReadAuthorizationModel(string queryParams.Add("continuation_token", continuationToken.ToString()); } if (startTime != null) { - queryParams.Add("start_time", startTime.ToString()); + queryParams.Add("start_time", startTime.ToString("0")); } var requestBuilder = new RequestBuilder { From 798053613d43c7e1b2bcf563acb64b157fdd642d Mon Sep 17 00:00:00 2001 From: Ryan Quinn Date: Fri, 21 Feb 2025 11:50:10 -0600 Subject: [PATCH 7/8] fix: toString for date format --- src/OpenFga.Sdk/Api/OpenFgaApi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFga.Sdk/Api/OpenFgaApi.cs b/src/OpenFga.Sdk/Api/OpenFgaApi.cs index 2f2c0d8..5abbdb1 100644 --- a/src/OpenFga.Sdk/Api/OpenFgaApi.cs +++ b/src/OpenFga.Sdk/Api/OpenFgaApi.cs @@ -448,7 +448,7 @@ public async Task ReadAuthorizationModel(string queryParams.Add("continuation_token", continuationToken.ToString()); } if (startTime != null) { - queryParams.Add("start_time", startTime.ToString("0")); + queryParams.Add("start_time", startTime.ToString("O")); } var requestBuilder = new RequestBuilder { From 72e845210c989591fd0af056c8a78559397392ce Mon Sep 17 00:00:00 2001 From: Ryan Quinn Date: Fri, 21 Feb 2025 11:53:03 -0600 Subject: [PATCH 8/8] Update OpenFgaApi.cs --- src/OpenFga.Sdk/Api/OpenFgaApi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFga.Sdk/Api/OpenFgaApi.cs b/src/OpenFga.Sdk/Api/OpenFgaApi.cs index 5abbdb1..c9a4de0 100644 --- a/src/OpenFga.Sdk/Api/OpenFgaApi.cs +++ b/src/OpenFga.Sdk/Api/OpenFgaApi.cs @@ -448,7 +448,7 @@ public async Task ReadAuthorizationModel(string queryParams.Add("continuation_token", continuationToken.ToString()); } if (startTime != null) { - queryParams.Add("start_time", startTime.ToString("O")); + queryParams.Add("start_time", startTime?.ToString("O")); } var requestBuilder = new RequestBuilder {