diff --git a/config/clients/go/CHANGELOG.md.mustache b/config/clients/go/CHANGELOG.md.mustache index abe17fc2..57e6f3fa 100644 --- a/config/clients/go/CHANGELOG.md.mustache +++ b/config/clients/go/CHANGELOG.md.mustache @@ -1,5 +1,14 @@ # Changelog +## v0.3.4 + +### [0.3.4](https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/compare/v0.3.3...v0.3.4) (2023-12-21) + +- feat: support for conditions +- chore: use latest API interfaces for type info +- chore: add [example project](./example) +- chore: dependency updates + ## v0.3.3 ### [0.3.3](https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/compare/v0.3.2...v0.3.3) (2023-12-21) diff --git a/config/clients/python/config.overrides.json b/config/clients/python/config.overrides.json index ee36f5ea..70dc964b 100644 --- a/config/clients/python/config.overrides.json +++ b/config/clients/python/config.overrides.json @@ -2,7 +2,7 @@ "sdkId": "python", "gitRepoId": "python-sdk", "packageName": "openfga_sdk", - "packageVersion": "0.3.3", + "packageVersion": "0.3.4", "packageDescription": "Python SDK for OpenFGA", "packageDetailedDescription": "This is an autogenerated python SDK for OpenFGA. It provides a wrapper around the [OpenFGA API definition](https://openfga.dev/api).", "fossaComplianceNoticeId": "2f8a8629-b46c-435e-b8cd-1174a674fb4b", @@ -166,7 +166,7 @@ "destinationFilename": "example/example1/example1.py", "templateType": "SupportingFiles" }, - "example/example1/requirements.txt": { + "example/example1/requirements.txt.mustache": { "destinationFilename": "example/example1/requirements.txt", "templateType": "SupportingFiles" }, diff --git a/config/clients/python/template/README_calling_api.mustache b/config/clients/python/template/README_calling_api.mustache index 42bdff13..a00e7fe0 100644 --- a/config/clients/python/template/README_calling_api.mustache +++ b/config/clients/python/template/README_calling_api.mustache @@ -85,10 +85,10 @@ Create a new authorization model. ```python body = WriteAuthorizationModelRequest( - schema_version = "1.1", + schema_version="1.1", type_definitions=[ TypeDefinition( - type="user", + type="user" ), TypeDefinition( type="document", @@ -107,9 +107,41 @@ body = WriteAuthorizationModelRequest( ], ), ), + ), + metadata=Metadata( + relations=dict( + writer=RelationMetadata( + directly_related_user_types=[ + RelationReference(type="user"), + RelationReference(type="user", condition="ViewCountLessThan200"), + ] + ), + viewer=RelationMetadata( + directly_related_user_types=[ + RelationReference(type="user"), + ] + ) + ) ) - ), + ) ], + conditions=dict( + ViewCountLessThan200=Condition( + name="ViewCountLessThan200", + expression="ViewCount < 200", + parameters=dict( + ViewCount=ConditionParamTypeRef( + type_name="TYPE_NAME_INT" + ), + Type=ConditionParamTypeRef( + type_name="TYPE_NAME_STRING" + ), + Name=ConditionParamTypeRef( + type_name="TYPE_NAME_STRING" + ), + ) + ) + ) ) response = await fga_client.write_authorization_model(body) @@ -129,7 +161,7 @@ options = { "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1" } -response = await fga_client.read_authorization_model(id) +response = await fga_client.read_authorization_model(options) # response.authorization_model = AuthorizationModel(id='01GXSA8YR785C4FYS3C0RTG7B1', schema_version = '1.1', type_definitions=type_definitions[...]) ``` @@ -159,7 +191,7 @@ options = { "page_size": "25", "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" } -body = ClientReadChangesRequest("document") +body = ClientReadChangesRequest(type="document") response = await fga_client.read_changes(body, options) # response.continuation_token = ... @@ -174,7 +206,7 @@ Reads the relationship tuples stored in the database. It does not evaluate nor e ```python # Find if a relationship tuple stating that a certain user is a viewer of certain document -body = TupleKey( +body = ReadRequestTupleKey( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="viewer", object="document:roadmap", @@ -186,7 +218,7 @@ response = await fga_client.read(body) ```python # Find all relationship tuples where a certain user has a relationship as any relation to a certain document -body = TupleKey( +body = ReadRequestTupleKey( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", object="document:roadmap", ) @@ -198,7 +230,7 @@ response = await fga_client.read(body) ```python # Find all relationship tuples where a certain user is a viewer of any document -body = TupleKey( +body = ReadRequestTupleKey( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="viewer", object="document:", @@ -210,7 +242,7 @@ response = await fga_client.read(body) ```python # Find all relationship tuples where any user has a relationship as any relation with a particular document -body = TupleKey( +body = ReadRequestTupleKey( object="document:roadmap", ) @@ -220,7 +252,7 @@ response = await fga_client.read(body) ```python # Read all stored relationship tuples -body = TupleKey() +body = ReadRequestTupleKey() response = await api_instance.read(body) # response = ReadResponse({"tuples": [Tuple({"key": TupleKey({"user":"...","relation":"...","object":"..."}), "timestamp": datetime.fromisoformat("...") })]}) @@ -247,6 +279,13 @@ body = ClientWriteRequest( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="viewer", object="document:roadmap", + condition=RelationshipCondition( + name='ViewCountLessThan200', + context=dict( + Name='Roadmap', + Type='Document', + ), + ), ), ClientTuple( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", @@ -293,6 +332,13 @@ body = ClientWriteRequest( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="viewer", object="document:budget", + condition=RelationshipCondition( + name='ViewCountLessThan200', + context=dict( + Name='Roadmap', + Type='Document', + ), + ), ), ], deletes=[ @@ -324,6 +370,9 @@ body = ClientCheckRequest( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="writer", object="document:roadmap", + context=dict( + ViewCount=100 + ), ) response = await fga_client.check(body, options) @@ -351,7 +400,10 @@ body = [ClientCheckRequest( relation="editor", object="document:roadmap", ), - ] + ], + context=dict( + ViewCount=100 + ) ), ClientCheckRequest( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="admin", @@ -384,7 +436,10 @@ response = await fga_client.batch_check(body, options) # user: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", # relation: "editor", # object: "document:roadmap" -# }] +# }], +# context=dict( +# ViewCount=100 +# ) # } # }, { # allowed: false, @@ -458,7 +513,10 @@ body = ClientListObjectsRequest( relation="writer", object="document:budget", ), - ] + ], + context=dict( + ViewCount=100 + ) ) response = await fga_client.list_objects(body) @@ -484,7 +542,10 @@ body = ClientListRelationsRequest( relation="writer", object="document:budget", ), - ] + ], + context=dict( + ViewCount=100 + ) ) var response = await fga_client.list_relations(body, options); @@ -499,7 +560,7 @@ Read assertions for a particular authorization model. [API Documentation]({{apiDocsUrl}}#/Assertions/Read%20Assertions) -```csharp +```python options = { # You can rely on the model id set in the configuration or override it for this specific request "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1" @@ -513,7 +574,7 @@ Update the assertions for a particular authorization model. [API Documentation]({{apiDocsUrl}}#/Assertions/Write%20Assertions) -```csharp +```python options = { # You can rely on the model id set in the configuration or override it for this specific request "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1" diff --git a/config/clients/python/template/client/client.mustache b/config/clients/python/template/client/client.mustache index c95da928..1ccf767d 100644 --- a/config/clients/python/template/client/client.mustache +++ b/config/clients/python/template/client/client.mustache @@ -646,7 +646,7 @@ class OpenFgaClient(): options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "ListRelations") options = set_heading_if_not_set(options, CLIENT_BULK_REQUEST_ID_HEADER, str(uuid.uuid4())) - request_body = [construct_check_request(user=body.user, relation=i, object=body.object, contextual_tuples=body.contextual_tuples) for i in body.relations] + request_body = [construct_check_request(user=body.user, relation=i, object=body.object, contextual_tuples=body.contextual_tuples, context=body.context) for i in body.relations] result = {{#asyncio}}await {{/asyncio}}self.batch_check(request_body, options) # need to filter with the allowed response result_iterator = filter(_check_allowed, result) diff --git a/config/clients/python/template/client/client_sync.mustache b/config/clients/python/template/client/client_sync.mustache index 4ae66936..3e5a8229 100644 --- a/config/clients/python/template/client/client_sync.mustache +++ b/config/clients/python/template/client/client_sync.mustache @@ -616,7 +616,7 @@ class OpenFgaClient(): options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "ListRelations") options = set_heading_if_not_set(options, CLIENT_BULK_REQUEST_ID_HEADER, str(uuid.uuid4())) - request_body = [construct_check_request(user=body.user, relation=i, object=body.object, contextual_tuples=body.contextual_tuples) for i in body.relations] + request_body = [construct_check_request(user=body.user, relation=i, object=body.object, contextual_tuples=body.contextual_tuples, context=body.context) for i in body.relations] result = self.batch_check(request_body, options) # need to filter with the allowed response result_iterator = filter(_check_allowed, result) diff --git a/config/clients/python/template/client/models/check_request.mustache b/config/clients/python/template/client/models/check_request.mustache index 340403d9..e6ec4dc6 100644 --- a/config/clients/python/template/client/models/check_request.mustache +++ b/config/clients/python/template/client/models/check_request.mustache @@ -6,11 +6,11 @@ from {{packageName}}.client.models.tuple import ClientTuple from typing import List, Any -def construct_check_request(user: str, relation: str, object: str, context: Any = None, contextual_tuples: List[ClientTuple]=None): +def construct_check_request(user: str, relation: str, object: str, contextual_tuples: List[ClientTuple] = None, context: object = None): """ helper function to construct the check request body """ - return ClientCheckRequest(user, relation, object, context, contextual_tuples) + return ClientCheckRequest(user, relation, object, contextual_tuples, context) class ClientCheckRequest(): @@ -18,14 +18,14 @@ class ClientCheckRequest(): ClientCheckRequest encapsulates the parameters for check request """ - def __init__(self, user: str, relation: str, object: str, context: Any = None, contextual_tuples: List[ClientTuple]=None): + def __init__(self, user: str, relation: str, object: str, contextual_tuples: List[ClientTuple] = None, context: object = None): self._user = user self._relation = relation self._object = object - self._context = context self._contextual_tuples = None if contextual_tuples: self._contextual_tuples = contextual_tuples + self._context = context @property def user(self): @@ -49,18 +49,18 @@ class ClientCheckRequest(): return self._object @property - def context(self): + def contextual_tuples(self): """ - Return context + Return contextual tuples """ - return self._context + return self._contextual_tuples @property - def contextual_tuples(self): + def context(self): """ - Return contextual tuples + Return context """ - return self._contextual_tuples + return self._context @user.setter def user(self, value): @@ -83,16 +83,16 @@ class ClientCheckRequest(): """ self._object = value - @context.setter - def context(self, value): - """ - Set context - """ - self._context = value - @contextual_tuples.setter def contextual_tuples(self, value): """ Set contextual tuples """ self._contextual_tuples = value + + @context.setter + def context(self, value): + """ + Set context + """ + self._context = value diff --git a/config/clients/python/template/client/models/list_objects_request.mustache b/config/clients/python/template/client/models/list_objects_request.mustache index 2cc03b67..307fdafb 100644 --- a/config/clients/python/template/client/models/list_objects_request.mustache +++ b/config/clients/python/template/client/models/list_objects_request.mustache @@ -10,12 +10,12 @@ class ClientListObjectsRequest(): ClientListObjectsRequest encapsulates the parameters required for list objects """ - def __init__(self, user: str, relation: str, type: str, context: Any = None, contextual_tuples: List[ClientTuple]=None): + def __init__(self, user: str, relation: str, type: str, contextual_tuples: List[ClientTuple] = None, context: object = None): self._user = user self._relation = relation self._type = type - self._context = context self._contextual_tuples = contextual_tuples + self._context = context @property def user(self): @@ -39,18 +39,18 @@ class ClientListObjectsRequest(): return self._type @property - def context(self): + def contextual_tuples(self): """ - Return context + Return contextual_tuples """ - return self._context + return self._contextual_tuples @property - def contextual_tuples(self): + def context(self): """ - Return contextual_tuples + Return context """ - return self._contextual_tuples + return self._context @user.setter def user(self, value): @@ -73,16 +73,16 @@ class ClientListObjectsRequest(): """ self._type = value - @context.setter - def context(self, value): - """ - Set context - """ - self._context = value - @contextual_tuples.setter def contextual_tuples(self, value): """ Set contextual tuples """ self._contextual_tuples = value + + @context.setter + def context(self, value): + """ + Set context + """ + self._context = value diff --git a/config/clients/python/template/client/models/list_relations_request.mustache b/config/clients/python/template/client/models/list_relations_request.mustache index faad11a7..9812d531 100644 --- a/config/clients/python/template/client/models/list_relations_request.mustache +++ b/config/clients/python/template/client/models/list_relations_request.mustache @@ -10,11 +10,12 @@ class ClientListRelationsRequest(): ClientListRelationsRequest encapsulates the parameters required for list all relations user have with object """ - def __init__(self, user: str, relations: List[str], object: str, contextual_tuples: List[ClientTuple]=None): + def __init__(self, user: str, relations: List[str], object: str, contextual_tuples: List[ClientTuple] = None, context: object = None): self._user = user self._relations = relations self._object = object self._contextual_tuples = contextual_tuples + self._context = context @property def user(self): @@ -44,6 +45,13 @@ class ClientListRelationsRequest(): """ return self._contextual_tuples + @property + def context(self): + """ + Return context + """ + return self._context + @user.setter def user(self, value): """ @@ -71,3 +79,10 @@ class ClientListRelationsRequest(): Set contextual tuples """ self._contextual_tuples = value + + @context.setter + def context(self, value): + """ + Set context + """ + self._context = value diff --git a/config/clients/python/template/example/Makefile b/config/clients/python/template/example/Makefile index 161efbae..007c2d81 100644 --- a/config/clients/python/template/example/Makefile +++ b/config/clients/python/template/example/Makefile @@ -3,8 +3,11 @@ all: run project_name=example1 openfga_version=latest -run: - python example1/example1.py +setup: + cd "${project_name}/" && pip3 install -r requirements.txt && cd - + +run: setup + python3 "${project_name}/${project_name}.py" run-openfga: docker pull docker.io/openfga/openfga:${openfga_version} && \ diff --git a/config/clients/python/template/example/example1/auth-model.json b/config/clients/python/template/example/example1/auth-model.json deleted file mode 100644 index e1361656..00000000 --- a/config/clients/python/template/example/example1/auth-model.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "schema_version": "1.1", - "type_definitions": [ - { - "type": "user" - }, - { - "type": "document", - "relations": { - "reader": { - "this": {} - }, - "writer": { - "this": {} - }, - "owner": { - "this": {} - } - }, - "metadata": { - "relations": { - "reader": { - "directly_related_user_types": [ - { - "type": "user" - } - ] - }, - "writer": { - "directly_related_user_types": [ - { - "type": "user" - } - ] - }, - "owner": { - "directly_related_user_types": [ - { - "type": "user" - } - ] - }, - "conditional_reader": { - "directly_related_user_types": [ - { - "condition": "name_starts_with_a", - "type": "user" - } - ] - } - } - } - } - ], - "conditions": { - "ViewCountLessThan200": { - "name": "ViewCountLessThan200", - "expression": "ViewCount < 200", - "parameters": { - "ViewCount": { - "type_name": "TYPE_NAME_INT" - }, - "Type": { - "type_name": "TYPE_NAME_STRING" - }, - "Name": { - "type_name": "TYPE_NAME_STRING" - } - } - } - } - } - \ No newline at end of file diff --git a/config/clients/python/template/example/example1/example1.py b/config/clients/python/template/example/example1/example1.py index 3687c806..c06f3c5e 100644 --- a/config/clients/python/template/example/example1/example1.py +++ b/config/clients/python/template/example/example1/example1.py @@ -1,10 +1,10 @@ import asyncio -import json -import openfga_sdk -from openfga_sdk.client.models import ClientAssertion, ClientCheckRequest, ClientReadChangesRequest, ClientTuple, ClientWriteRequest -from openfga_sdk.models import CreateStoreRequest, Metadata, ObjectRelation, RelationMetadata, TupleKey, TypeDefinition, Userset, Usersets, WriteAuthorizationModelRequest -from openfga_sdk import ClientConfiguration, OpenFgaClient +from openfga_sdk.client.models import ClientAssertion, ClientCheckRequest, ClientReadChangesRequest, ClientTuple, \ + ClientWriteRequest, ClientListRelationsRequest, ClientListObjectsRequest, WriteTransactionOpts +from openfga_sdk import ClientConfiguration, OpenFgaClient, RelationReference, RelationshipCondition, \ + ConditionParamTypeRef, Condition, ReadRequestTupleKey, CreateStoreRequest, Metadata, ObjectRelation, \ + RelationMetadata, TypeDefinition, Userset, Usersets, WriteAuthorizationModelRequest from openfga_sdk.credentials import CredentialConfiguration, Credentials import os @@ -76,9 +76,65 @@ async def main(): # WriteAuthorizationModel print('Writing an Authorization Model') - with open(os.path.join(os.path.dirname(__file__), 'auth-model.json')) as f: - auth_model_request = json.load(f) - response = await fga_client.write_authorization_model(auth_model_request) + response = await fga_client.write_authorization_model(WriteAuthorizationModelRequest( + schema_version="1.1", + type_definitions=[ + TypeDefinition( + type="user" + ), + TypeDefinition( + type="document", + relations=dict( + writer=Userset( + this=dict(), + ), + viewer=Userset( + union=Usersets( + child=[ + Userset(this=dict()), + Userset(computed_userset=ObjectRelation( + object="", + relation="writer", + )), + ], + ), + ), + ), + metadata=Metadata( + relations=dict( + writer=RelationMetadata( + directly_related_user_types=[ + RelationReference(type="user"), + RelationReference(type="user", condition="ViewCountLessThan200"), + ] + ), + viewer=RelationMetadata( + directly_related_user_types=[ + RelationReference(type="user"), + ] + ) + ) + ) + ) + ], + conditions=dict( + ViewCountLessThan200=Condition( + name="ViewCountLessThan200", + expression="ViewCount < 200", + parameters=dict( + ViewCount=ConditionParamTypeRef( + type_name="TYPE_NAME_INT" + ), + Type=ConditionParamTypeRef( + type_name="TYPE_NAME_STRING" + ), + Name=ConditionParamTypeRef( + type_name="TYPE_NAME_STRING" + ), + ) + ) + ) + )) print(f"Authorization Model ID: {response.authorization_model_id}") # ReadAuthorizationModels (after write) @@ -101,13 +157,13 @@ async def main(): user='user:anne', relation='writer', object='document:roadmap', - # condition=RelationshipCondition( - # name='ViewCountLessThan200', - # context=dict( - # Name='Roadmap', - # Type='Document', - # ), - # ), + condition=RelationshipCondition( + name='ViewCountLessThan200', + context=dict( + Name='Roadmap', + Type='Document', + ), + ), ), ], ) @@ -118,31 +174,113 @@ async def main(): await fga_client.write(body, options) print('Done Writing Tuples') + # Write + print('Writing Tuples - non txn') + body = ClientWriteRequest( + writes=[ + ClientTuple( + user='user:beth', + relation='writer', + object='document:1', + condition=RelationshipCondition( + name='ViewCountLessThan200', + context=dict( + Name='Roadmap', + Type='Document', + ), + ), + ), + ClientTuple( + user='user:beth', + relation='viewer', + object='document:2' + ), + ], + ) + options = { + # You can rely on the model id set in the configuration or override it for this specific request + "authorization_model_id": auth_model_id, + "transaction": WriteTransactionOpts( + max_per_chunk=1 + ) + } + await fga_client.write(body, options) + print('Done Writing Tuples') + # Set the model ID fga_client.set_authorization_model_id(auth_model_id) # Read print('Reading Tuples') - response = await fga_client.read(TupleKey(user='user:anne', object='document:')) + response = await fga_client.read(ReadRequestTupleKey(user='user:anne', object='document:')) print(f"Read Tuples: {response.tuples}") # ReadChanges print('Reading Tuple Changes') - body = ClientReadChangesRequest('document') + body = ClientReadChangesRequest(type='document') response = await fga_client.read_changes(body) print(f"Read Changes Tuples: {response.changes}") # Check - print('Checking for access') + print('Checking for access w/o context') + try: + response = await fga_client.check(ClientCheckRequest( + user='user:anne', + relation='viewer', + object='document:roadmap' + )) + print(f"Allowed: {response.allowed}") + except Exception as err: + print(f"Failed due to: {err}") + + # Checking for access with context + print('Checking for access with context') + response = await fga_client.check(ClientCheckRequest( user='user:anne', - relation='reader', + relation='viewer', object='document:roadmap', + context=dict( + ViewCount=100 + ) )) print(f"Allowed: {response.allowed}") - # Checking for access with context - # TODO + # List objects with context + print('Listing objects for access with context') + + response = await fga_client.list_objects(ClientListObjectsRequest( + user='user:anne', + relation='viewer', + type='document', + context=dict( + ViewCount=100 + ) + )) + print(f"Objects: {response.objects}") + + # List relations w/o context + print('Listing relations for access w/o context') + + response = await fga_client.list_relations(ClientListRelationsRequest( + user='user:anne', + relations=['viewer', 'writer'], + object='document:roadmap' + )) + print(f"Relations: {response}") + + # List relations with context + print('Listing relations for access with context') + + response = await fga_client.list_relations(ClientListRelationsRequest( + user='user:anne', + relations=['viewer', 'writer'], + object='document:roadmap', + context=dict( + ViewCount=100 + ) + )) + print(f"Relations: {response}") # WriteAssertions await fga_client.write_assertions([ @@ -154,7 +292,7 @@ async def main(): ), ClientAssertion( user='user:anne', - relation='reader', + relation='viewer', object='document:roadmap', expectation=False, ), diff --git a/config/clients/python/template/example/example1/requirements.txt b/config/clients/python/template/example/example1/requirements.txt.mustache similarity index 82% rename from config/clients/python/template/example/example1/requirements.txt rename to config/clients/python/template/example/example1/requirements.txt.mustache index f400ac51..7bb741ac 100644 --- a/config/clients/python/template/example/example1/requirements.txt +++ b/config/clients/python/template/example/example1/requirements.txt.mustache @@ -4,7 +4,7 @@ attrs==23.1.0 frozenlist==1.4.1 idna==3.6 multidict==6.0.4 -openfga-sdk==0.3.2 +openfga-sdk=={{packageVersion}} python-dateutil==2.8.2 six==1.16.0 urllib3==2.1.0