Skip to content

Commit

Permalink
feat: support list users (#351)
Browse files Browse the repository at this point in the history
  • Loading branch information
rhamzeh authored Apr 30, 2024
2 parents 7db8db4 + 7160f2f commit 051c804
Show file tree
Hide file tree
Showing 43 changed files with 1,548 additions and 9,383 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ jobs:

- name: Run SDK Tests
run: make test-client-js
env:
OPEN_API_REF: list-users

- name: Check for SDK changes
run: |
Expand Down Expand Up @@ -63,6 +65,8 @@ jobs:

- name: Run SDK Tests
run: make test-client-go
env:
OPEN_API_REF: list-users

- name: Check for SDK changes
run: |
Expand Down Expand Up @@ -93,6 +97,8 @@ jobs:

- name: Run All Tests
run: make test-client-dotnet
env:
OPEN_API_REF: list-users

- name: Check for SDK changes
run: |
Expand Down
7 changes: 4 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Main config
OPENFGA_DOCKER_TAG = v1.5.1
OPENFGA_DOCKER_TAG = v1.5.3
OPEN_API_REF ?= main
OPEN_API_URL = https://raw.githubusercontent.com/openfga/api/${OPEN_API_REF}/docs/openapiv2/apidocs.swagger.json
OPENAPI_GENERATOR_CLI_DOCKER_TAG = v6.4.0
Expand Down Expand Up @@ -66,7 +66,6 @@ build-client-js:
sed -i -e "s|_this|this|g" ${CLIENTS_OUTPUT_DIR}/fga-js-sdk/*.md
rm -rf ${CLIENTS_OUTPUT_DIR}/fga-js-sdk/*-e
make run-in-docker sdk_language=js image=node:${NODE_DOCKER_TAG} command="/bin/sh -c 'npm i --lockfile-version 2 && npm run lint:fix -- --quiet'"
make run-in-docker sdk_language=js image=busybox:${BUSYBOX_DOCKER_TAG} command="/bin/sh -c 'patch -p1 /module/api.ts /config/clients/js/patches/add-missing-first-param.patch'"
make run-in-docker sdk_language=js image=node:${NODE_DOCKER_TAG} command="/bin/sh -c 'npm run lint:fix && npm run build;'"

### Go
Expand All @@ -84,7 +83,7 @@ test-client-go: build-client-go
build-client-go:
make build-client sdk_language=go tmpdir=${TMP_DIR}
make run-in-docker sdk_language=go image=busybox:${BUSYBOX_DOCKER_TAG} command="/bin/sh -c 'patch -p1 /module/api_open_fga.go /config/clients/go/patches/add-missing-first-param.patch'"
make run-in-docker sdk_language=go image=golang:${GO_DOCKER_TAG} command="/bin/sh -c 'gofmt -w .'"
make run-in-docker sdk_language=go image=golang:${GO_DOCKER_TAG} command="/bin/sh -c 'gofmt -w . && go mod tidy && cd example/example1 && gofmt -w . && go mod tidy'"

### .NET
.PHONY: tag-client-dotnet
Expand Down Expand Up @@ -165,6 +164,8 @@ build-openapi: init get-openapi-doc
cat "${DOCS_CACHE_DIR}/openfga.openapiv2.raw.json" | \
jq '(.. | .tags? | select(.)) |= ["OpenFga"] | (.tags? | select(.)) |= [{"name":"OpenFga"}] | del(.definitions.ReadTuplesParams, .definitions.ReadTuplesResponse, .paths."/stores/{store_id}/read-tuples", .definitions.StreamedListObjectsRequest, .definitions.StreamedListObjectsResponse, .paths."/stores/{store_id}/streamed-list-objects")' > \
${DOCS_CACHE_DIR}/openfga.openapiv2.json
sed -i -e 's/"Object"/"FgaObject"/g' ${DOCS_CACHE_DIR}/openfga.openapiv2.json
sed -i -e 's/#\/definitions\/Object"/#\/definitions\/FgaObject"/g' ${DOCS_CACHE_DIR}/openfga.openapiv2.json
sed -i -e 's/v1.//g' ${DOCS_CACHE_DIR}/openfga.openapiv2.json

.PHONY: get-openapi-doc
Expand Down
8 changes: 8 additions & 0 deletions config/clients/dotnet/CHANGELOG.md.mustache
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## v0.3.2

### [0.3.2](https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/compare/v0.3.1...v0.3.2) (2024-04-30)

- feat: support the [ListUsers](https://github.com/openfga/rfcs/blob/main/20231214-listUsers-api.md) endpoint (#57)
- feat: add retries to client credential requests (#51)
- feat: add support for modular models metadata (#53)

## v0.3.1

### [0.3.1](https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/compare/v0.3.0...v0.3.1) (2024-02-13)
Expand Down
6 changes: 5 additions & 1 deletion config/clients/dotnet/config.overrides.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"packageGuid": "b8d9e3e9-0156-4948-9de7-5e0d3f9c4d9e",
"testPackageGuid": "d119dfae-509a-4eba-a973-645b739356fc",
"packageName": "OpenFga.Sdk",
"packageVersion": "0.3.1",
"packageVersion": "0.3.2",
"licenseUrl": "https://github.com/openfga/dotnet-sdk/blob/main/LICENSE",
"fossaComplianceNoticeId": "f8ac2ec4-84fc-44f4-a617-5800cd3d180e",
"termsOfService": "",
Expand Down Expand Up @@ -122,6 +122,10 @@
"destinationFilename": "src/OpenFga.Sdk/Client/Model/ClientListStoresOptions.cs",
"templateType": "SupportingFiles"
},
"Client/Model/ClientListUsersRequest.mustache": {
"destinationFilename": "src/OpenFga.Sdk/Client/Model/ClientListUsersRequest.cs",
"templateType": "SupportingFiles"
},
"Client/Model/ClientPaginationOptions.mustache": {
"destinationFilename": "src/OpenFga.Sdk/Client/Model/ClientPaginationOptions.cs",
"templateType": "SupportingFiles"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with:
fetch-depth: 0

Expand All @@ -36,7 +36,7 @@ jobs:
path: src/<%packageName%>/bin/Release/<%packageName%>.*.nupkg

- name: Upload coverage to Codecov
uses: codecov/codecov-action@c16abc29c95fcf9174b58eb7e1abf4c866893bc8 # v4.1.1
uses: codecov/codecov-action@84508663e988701840491b86de86b666e8a86bed # v4.3.0
continue-on-error: true
with:
token: ${{ secrets.CODECOV_TOKEN }}
Expand All @@ -48,7 +48,7 @@ jobs:
needs: [test]

steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with:
fetch-depth: 0

Expand Down Expand Up @@ -82,7 +82,7 @@ jobs:
needs: publish

steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with:
fetch-depth: 0

Expand Down
16 changes: 16 additions & 0 deletions config/clients/dotnet/template/Client/Client.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,22 @@ public class {{appShortName}}Client : IDisposable {
return responses;
}

/**
* ListUsers - List all users of the given type that the object has a relation with (evaluates)
*/
public async Task<ListUsersResponse> ListUsers(IClientListUsersRequest body,
IClientRequestOptionsWithAuthZModelId? options = default,
CancellationToken cancellationToken = default) =>
await api.ListUsers(GetStoreId(options), new ListUsersRequest {
Object = body.Object,
Relation = body.Relation,
UserFilters = body.UserFilters,
ContextualTuples = body.ContextualTuples?.ConvertAll(tupleKey => tupleKey.ToTupleKey()) ??
new List<TupleKey>(),
Context = body.Context,
AuthorizationModelId = GetAuthorizationModelId(options)
});

/**************
* Assertions *
**************/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
{{>partial_header}}

using {{packageName}}.Model;
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace {{packageName}}.Client.Model;

public interface IClientListUsersRequest : IClientQueryContextWrapper {
public FgaObject Object { get; set; }
public string Relation { get; set; }
public List<UserTypeFilter> UserFilters { get; set; }
}

/// <summary>
/// ClientListUsersRequest
/// </summary>
public class ClientListUsersRequest : IClientListUsersRequest, IEquatable<ClientListUsersRequest>,
IValidatableObject {
/// <summary>
/// Gets or Sets Object
/// </summary>
[DataMember(Name = "object", IsRequired = true, EmitDefaultValue = false)]
[JsonPropertyName("object")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public FgaObject Object { get; set; }

/// <summary>
/// Gets or Sets Relation
/// </summary>
[DataMember(Name = "relation", IsRequired = true, EmitDefaultValue = false)]
[JsonPropertyName("relation")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public string Relation { get; set; }

/// <summary>
/// Gets or Sets UserFilters
/// </summary>
[DataMember(Name = "user_filters", IsRequired = true, EmitDefaultValue = false)]
[JsonPropertyName("user_filters")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public List<UserTypeFilter> UserFilters { get; set; }

/// <summary>
/// Gets or Sets ContextualTuples
/// </summary>
[DataMember(Name = "contextual_tuples", EmitDefaultValue = false)]
[JsonPropertyName("contextual_tuples")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public List<ClientTupleKey>? ContextualTuples { get; set; }

/// <summary>
/// Additional request context that will be used to evaluate any ABAC conditions encountered in the query evaluation.
/// </summary>
/// <value>Additional request context that will be used to evaluate any ABAC conditions encountered in the query evaluation.</value>
[DataMember(Name = "context", EmitDefaultValue = false)]
[JsonPropertyName("context")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public Object? Context { get; set; }

public bool Equals(ClientListUsersRequest input) {
if (input == null) {
return false;
}

return
(
Object == input.Object ||
(Object != null &&
Object.Equals(input.Object))
) &&
(
Relation == input.Relation ||
(Relation != null &&
Relation.Equals(input.Relation))
) &&
(
UserFilters == input.UserFilters ||
(UserFilters != null &&
UserFilters.Equals(input.UserFilters))
) &&
(
ContextualTuples == input.ContextualTuples ||
(ContextualTuples != null &&
ContextualTuples.Equals(input.ContextualTuples))
) &&
(
this.Context == input.Context ||
(this.Context != null &&
this.Context.Equals(input.Context))
);
}

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
yield break;
}

public virtual string ToJson() => JsonSerializer.Serialize(this);

public static ClientListUsersRequest FromJson(string jsonString) =>
JsonSerializer.Deserialize<ClientListUsersRequest>(jsonString) ?? throw new InvalidOperationException();

public override bool Equals(object input) => Equals(input as ClientListUsersRequest);

public override int GetHashCode() {
unchecked // Overflow is fine, just wrap
{
var hashCode = 9661;
if (Object != null) {
hashCode = (hashCode * 9923) + Object.GetHashCode();
}

if (Relation != null) {
hashCode = (hashCode * 9923) + Relation.GetHashCode();
}

if (UserFilters != null) {
hashCode = (hashCode * 9923) + UserFilters.GetHashCode();
}

if (ContextualTuples != null) {
hashCode = (hashCode * 9923) + ContextualTuples.GetHashCode();
}

if (Context != null) {
hashCode = (hashCode * 9923) + Context.GetHashCode();
}

return hashCode;
}
}
}
95 changes: 95 additions & 0 deletions config/clients/dotnet/template/OpenFgaClientTests.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -1411,6 +1411,101 @@ public class {{appShortName}}ClientTests {
ItExpr.IsAny<CancellationToken>()
);
}
/// <summary>
/// Test ListUsers
/// </summary>
[Fact]
public async Task ListUsersTest() {
var mockHandler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
var expectedResponse = new ListUsersResponse {
// A real API would not return all these for the filter provided, these are just for test purposes
Users = new List<User> {
new () {
Object = new FgaObject {
Type = "user",
Id = "81684243-9356-4421-8fbf-a4f8d36aa31b"
}
},
new () {
Userset = new UsersetUser() {
Type = "team",
Id = "fga",
Relation = "member"
}
},
new () {
Wildcard = new TypedWildcard() {
Type = "user"
}
}

},
ExcludedUsers = new List<ObjectOrUserset>()
};
mockHandler.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.Is<HttpRequestMessage>(req =>
req.RequestUri == new Uri($"{_config.BasePath}/stores/{_storeId}/list-users") &&
req.Method == HttpMethod.Post),
ItExpr.IsAny<CancellationToken>()
)
.ReturnsAsync(new HttpResponseMessage() {
StatusCode = HttpStatusCode.OK,
Content = Utils.CreateJsonStringContent(expectedResponse),
});

var httpClient = new HttpClient(mockHandler.Object);
var fgaClient = new {{appShortName}}Client(_config, httpClient);

var body = new ClientListUsersRequest {
Relation = "can_read",
Object = new FgaObject {
Type = "document",
Id = "roadmap"
},
UserFilters = new List<UserTypeFilter> {
// API does not allow sending multiple filters, done here for testing purposes
new() {
Type = "user"
},
new() {
Type = "team",
Relation = "member"
}
},
ContextualTuples = new List<ClientTupleKey> {
new() {
User = "folder:product",
Relation = "editor",
Object = "folder:product",
},
new() {
User = "folder:product",
Relation = "parent",
Object = "document:roadmap",
},
},
Context = new { ViewCount = 100 }
};

var response = await fgaClient.ListUsers(body, new ClientWriteOptions {
AuthorizationModelId = "01GXSA8YR785C4FYS3C0RTG7B1",
});

mockHandler.Protected().Verify(
"SendAsync",
Times.Exactly(1),
ItExpr.Is<HttpRequestMessage>(req =>
req.RequestUri == new Uri($"{_config.BasePath}/stores/{_storeId}/list-users") &&
req.Method == HttpMethod.Post),
ItExpr.IsAny<CancellationToken>()
);

Assert.IsType<ListUsersResponse>(response);
Assert.Equal(3, response.Users.Count);
Assert.Equal(response, expectedResponse);
}

/**************
* Assertions *
Expand Down
Loading

0 comments on commit 051c804

Please sign in to comment.