Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
9dc36e2
feat: serialize different permissions type to the API format
bevzzz Jan 20, 2025
e7686d9
feat: handle permission deserialization
bevzzz Jan 20, 2025
7c33b40
feat: expose all existing endpoints in the sync client
bevzzz Jan 22, 2025
6b0db1e
test: add configuration to run Weaviate Testcontainer with RBAC
bevzzz Jan 23, 2025
312b208
test: add integration tests for add-/remove-/has-permission
bevzzz Jan 23, 2025
96c4301
test: add test for getting user roles
bevzzz Jan 23, 2025
ebb8bdf
test: add test for assinging and revoking roles
bevzzz Jan 23, 2025
ef8311e
test: reduce code duplication
bevzzz Jan 23, 2025
03b697a
test: fix test case -- manage_collections is not currently supported
bevzzz Jan 23, 2025
dc74a3b
ci: fix expected Weaviate server version
bevzzz Jan 23, 2025
d948aa1
chore: delete debug print stmts
bevzzz Jan 23, 2025
9de1f7f
ci: fix expected commit hash
bevzzz Jan 23, 2025
eb5bf54
test: update expected error message for invalid tokenizer
bevzzz Jan 24, 2025
c4f8de2
test: refactor integration test suite to run parametrized tests for s…
bevzzz Jan 24, 2025
6314949
feat: add RBAC to WeaviateAsyncClient
bevzzz Jan 24, 2025
8133383
feat: add overloaded factory methods for multi-action permissions
bevzzz Jan 27, 2025
ec123e6
test: remove duplicated env variable
bevzzz Jan 27, 2025
138e2ce
refactor: rename CustomAction to RbacAction
bevzzz Jan 30, 2025
be0d280
test: move RBAC configuration to a separate container class
bevzzz Jan 30, 2025
bf1d800
chore: fix documentation
bevzzz Jan 30, 2025
84ae4a9
refactor: return Result<Boolean> instead of Result<Void>
bevzzz Jan 30, 2025
65fc863
chore: fix docstring
bevzzz Jan 30, 2025
205995a
feat: add changes from rc2 and align with Python's client
bevzzz Feb 17, 2025
4904cea
refactor: read/manage user roles from 'users' namespace
bevzzz Feb 17, 2025
cdac8b7
feat: fetch current user via MyUserGetter (/users/own-info)
bevzzz Feb 17, 2025
073ab9a
fix: return 'true' for all status codes < 299
bevzzz Feb 18, 2025
bf175e8
feat: do not sent requests with deprecated actions to the server
bevzzz Feb 18, 2025
3b21ed0
fix: collect streams using Collectors.toList
bevzzz Feb 18, 2025
f45572e
refactor: simplify deprecated actions
bevzzz Feb 18, 2025
7068e57
refactor: action argument should go last in Permission.backups to be …
bevzzz Feb 19, 2025
65b3079
ci: bump WEAVIATE_VERSION to 1.29.0
bevzzz Feb 21, 2025
2d6d97b
chore: remove 'tenant' and 'object' filters from permissions
bevzzz Feb 24, 2025
bbd6c47
chore: rename withUser -> withUserId
bevzzz Feb 24, 2025
1d80eb5
feat: read username into userId field if user_id not available
bevzzz Feb 24, 2025
53179f8
ci: fix expected Weaviate version
bevzzz Feb 26, 2025
2b7a1f2
refactor: push toWeaviate() to the parent Permission class
bevzzz Feb 26, 2025
5ab0636
refactor: group permissions by resource
bevzzz Feb 26, 2025
d43e0d0
refactor: store actions in a Set
bevzzz Feb 26, 2025
4cc3d1e
doc: clarify usage for firstToWeaviate()
bevzzz Feb 26, 2025
74a00b8
test: re-activate commented out test case
bevzzz Feb 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/main/java/io/weaviate/client/WeaviateClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
import io.weaviate.client.v1.grpc.GRPC;
import io.weaviate.client.v1.misc.Misc;
import io.weaviate.client.v1.misc.api.MetaGetter;
import io.weaviate.client.v1.rbac.Roles;
import io.weaviate.client.v1.schema.Schema;
import io.weaviate.client.v1.users.Users;

public class WeaviateClient {
private final Config config;
Expand Down Expand Up @@ -101,6 +103,14 @@ public GRPC gRPC() {
return new GRPC(httpClient, config, tokenProvider);
}

public Roles roles() {
return new Roles(httpClient, config);
}

public Users users() {
return new Users(httpClient, config);
}

private DbVersionProvider initDbVersionProvider() {
MetaGetter metaGetter = new Misc(httpClient, config, null).metaGetter();
DbVersionProvider.VersionGetter getter = () -> Optional.ofNullable(metaGetter.run())
Expand Down
1 change: 0 additions & 1 deletion src/main/java/io/weaviate/client/base/BaseClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ private Response<T> sendRequest(String endpoint, Object payload, String method,
HttpResponse response = this.sendHttpRequest(endpoint, payload, method);
int statusCode = response.getStatusCode();
String responseBody = response.getBody();

if (statusCode < 399) {
T body = toResponse(responseBody, classOfT);
return new Response<>(statusCode, body, null);
Expand Down
49 changes: 44 additions & 5 deletions src/main/java/io/weaviate/client/base/Result.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
import java.util.Objects;
import java.util.stream.Collectors;

import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpResponse;

import io.weaviate.client.base.http.async.ResponseParser;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.ToString;
Expand All @@ -23,11 +27,13 @@ public Result(Response<T> response) {

public Result(int statusCode, T body, WeaviateErrorResponse errors) {
if (errors != null && errors.getError() != null) {
List<WeaviateErrorMessage> items = errors.getError().stream().filter(Objects::nonNull).collect(Collectors.toList());
List<WeaviateErrorMessage> items = errors.getError().stream().filter(Objects::nonNull)
.collect(Collectors.toList());
this.error = new WeaviateError(statusCode, items);
this.result = body;
} else if (errors != null && errors.getMessage() != null) {
this.error = new WeaviateError(statusCode, Collections.singletonList(WeaviateErrorMessage.builder().message(errors.getMessage()).build()));
this.error = new WeaviateError(statusCode,
Collections.singletonList(WeaviateErrorMessage.builder().message(errors.getMessage()).build()));
this.result = body;
} else {
this.result = body;
Expand All @@ -40,12 +46,45 @@ public boolean hasErrors() {
}

/**
* Copy the Result object with a null body, preserving only the status code and the error message.
* Copy the Result object with a null body, preserving only the status code and
* the error message.
*
* @param <C> Would-be response type. It's required for type safety, but can be anything since the body is always set to null.
* @param <C> Would-be response type. It's required for type safety, but can be
* anything since the body is always set to null.
* @return A copy of this Result.
*/
public <C> Result<C> toErrorResult() {
return new Result<>(this.error.getStatusCode(), null, WeaviateErrorResponse.builder().error(this.error.getMessages()).build());
return new Result<>(this.error.getStatusCode(), null,
WeaviateErrorResponse.builder().error(this.error.getMessages()).build());
}

/**
* Convert {@code Result<Void>} response to a {@code Result<Boolean>}.
* The result contains true if status code is 200.
*
* @param response Response from a call that does not return a value, like
* {@link BaseClient#sendDeleteRequest}.
* @return {@code Result<Boolean>}
*/
public static Result<Boolean> voidToBoolean(Response<Void> response) {
int status = response.getStatusCode();
return new Result<>(status, status < 299, response.getErrors());
}

/**
* Get a custom parser to convert {@code Result<Void>} response as to a
* {@code Result<Void>}. The result contains true if status code is 200.
*
* @return {@code Result<Boolean>}
*/
public static ResponseParser<Boolean> voidToBooleanParser() {
return new ResponseParser<Boolean>() {
@Override
public Result<Boolean> parse(HttpResponse response, String body, ContentType contentType) {
Response<Object> resp = this.serializer.toResponse(response.getCode(), body, Object.class);
return new Result<>(resp.getStatusCode(), resp.getStatusCode() < 299, resp.getErrors());
}
};
}

}
23 changes: 17 additions & 6 deletions src/main/java/io/weaviate/client/v1/async/WeaviateAsyncClient.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
package io.weaviate.client.v1.async;

import java.util.Optional;
import java.util.concurrent.ExecutionException;

import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
import org.apache.hc.core5.io.CloseMode;

import io.weaviate.client.Config;
import io.weaviate.client.base.Result;
import io.weaviate.client.base.http.async.AsyncHttpClient;
Expand All @@ -13,13 +19,11 @@
import io.weaviate.client.v1.async.data.Data;
import io.weaviate.client.v1.async.graphql.GraphQL;
import io.weaviate.client.v1.async.misc.Misc;
import io.weaviate.client.v1.async.rbac.Roles;
import io.weaviate.client.v1.async.schema.Schema;
import io.weaviate.client.v1.async.users.Users;
import io.weaviate.client.v1.auth.provider.AccessTokenProvider;
import io.weaviate.client.v1.misc.model.Meta;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
import org.apache.hc.core5.io.CloseMode;

public class WeaviateAsyncClient implements AutoCloseable {
private final Config config;
Expand Down Expand Up @@ -72,9 +76,16 @@ public GraphQL graphQL() {
return new GraphQL(client, config, tokenProvider);
}

public Roles roles() {
return new Roles(client, config, tokenProvider);
}

public Users users() {
return new Users(client, config, tokenProvider);
}

private DbVersionProvider initDbVersionProvider() {
DbVersionProvider.VersionGetter getter = () ->
Optional.ofNullable(this.getMeta())
DbVersionProvider.VersionGetter getter = () -> Optional.ofNullable(this.getMeta())
.filter(result -> !result.hasErrors())
.map(result -> result.getResult().getVersion());

Expand Down
77 changes: 77 additions & 0 deletions src/main/java/io/weaviate/client/v1/async/rbac/Roles.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package io.weaviate.client.v1.async.rbac;

import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;

import io.weaviate.client.Config;
import io.weaviate.client.v1.async.rbac.api.AssignedUsersGetter;
import io.weaviate.client.v1.async.rbac.api.PermissionAdder;
import io.weaviate.client.v1.async.rbac.api.PermissionChecker;
import io.weaviate.client.v1.async.rbac.api.PermissionRemover;
import io.weaviate.client.v1.async.rbac.api.RoleAllGetter;
import io.weaviate.client.v1.async.rbac.api.RoleCreator;
import io.weaviate.client.v1.async.rbac.api.RoleDeleter;
import io.weaviate.client.v1.async.rbac.api.RoleExists;
import io.weaviate.client.v1.async.rbac.api.RoleGetter;
import io.weaviate.client.v1.auth.provider.AccessTokenProvider;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class Roles {

private final CloseableHttpAsyncClient client;
private final Config config;
private final AccessTokenProvider tokenProvider;

public RoleCreator creator() {
return new RoleCreator(client, config, tokenProvider);
}

/** Get all existing roles. */
public RoleDeleter deleter() {
return new RoleDeleter(client, config, tokenProvider);
}

/**
* Add permissions to an existing role.
* Note: This method is an upsert operation. If the permission already exists,
* it will be updated. If it does not exist, it will be created.
*/
public PermissionAdder permissionAdder() {
return new PermissionAdder(client, config, tokenProvider);
}

/**
* Remove permissions from a role.
* Note: This method is a downsert operation. If the permission does not
* exist, it will be ignored. If these permissions are the only permissions of
* the role, the role will be deleted.
*/
public PermissionRemover permissionRemover() {
return new PermissionRemover(client, config, tokenProvider);
}

/** Check if a role has a permission. */
public PermissionChecker permissionChecker() {
return new PermissionChecker(client, config, tokenProvider);
}

/** Get all existing roles. */
public RoleAllGetter allGetter() {
return new RoleAllGetter(client, config, tokenProvider);
};

/** Get role and its assiciated permissions. */
public RoleGetter getter() {
return new RoleGetter(client, config, tokenProvider);
};

/** Get users assigned to a role. */
public AssignedUsersGetter assignedUsersGetter() {
return new AssignedUsersGetter(client, config, tokenProvider);
};

/** Check if a role exists. */
public RoleExists exists() {
return new RoleExists(client, config, tokenProvider);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package io.weaviate.client.v1.async.rbac.api;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Future;

import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
import org.apache.hc.core5.concurrent.FutureCallback;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpResponse;

import io.weaviate.client.Config;
import io.weaviate.client.base.AsyncBaseClient;
import io.weaviate.client.base.AsyncClientResult;
import io.weaviate.client.base.Response;
import io.weaviate.client.base.Result;
import io.weaviate.client.base.http.async.ResponseParser;
import io.weaviate.client.v1.auth.provider.AccessTokenProvider;

public class AssignedUsersGetter extends AsyncBaseClient<List<String>> implements AsyncClientResult<List<String>> {
private String role;

public AssignedUsersGetter(CloseableHttpAsyncClient httpClient, Config config, AccessTokenProvider tokenProvider) {
super(httpClient, config, tokenProvider);
}

public AssignedUsersGetter withRole(String role) {
this.role = role;
return this;
}

@Override
public Future<Result<List<String>>> run(FutureCallback<Result<List<String>>> callback) {
return sendGetRequest(path(), callback, new ResponseParser<List<String>>() {
@Override
public Result<List<String>> parse(HttpResponse response, String body, ContentType contentType) {
Response<String[]> resp = this.serializer.toResponse(response.getCode(), body, String[].class);
List<String> roles = Optional.ofNullable(resp.getBody())
.map(Arrays::asList)
.orElse(new ArrayList<>());
return new Result<>(resp.getStatusCode(), roles, resp.getErrors());
}
});
}

private String path() {
return String.format("/authz/roles/%s/users", this.role);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.weaviate.client.v1.async.rbac.api;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Future;

import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
import org.apache.hc.core5.concurrent.FutureCallback;

import io.weaviate.client.Config;
import io.weaviate.client.base.AsyncBaseClient;
import io.weaviate.client.base.AsyncClientResult;
import io.weaviate.client.base.Result;
import io.weaviate.client.v1.auth.provider.AccessTokenProvider;
import io.weaviate.client.v1.rbac.api.WeaviatePermission;
import io.weaviate.client.v1.rbac.model.Permission;
import lombok.AllArgsConstructor;

public class PermissionAdder extends AsyncBaseClient<Boolean> implements AsyncClientResult<Boolean> {
private String role;
private List<Permission<?>> permissions = new ArrayList<>();

public PermissionAdder(CloseableHttpAsyncClient httpClient, Config config, AccessTokenProvider tokenProvider) {
super(httpClient, config, tokenProvider);
}

public PermissionAdder withRole(String name) {
this.role = name;
return this;
}

public PermissionAdder withPermissions(Permission<?>... permissions) {
this.permissions = Arrays.asList(permissions);
return this;
}

public PermissionAdder withPermissions(Permission<?>[]... permissions) {
List<Permission<?>> all = new ArrayList<>();
for (Permission<?>[] perm : permissions) {
all.addAll(Arrays.asList(perm));
}
this.permissions = all;
return this;
}

/** The API signature for this method is { "permissions": [...] } */
@AllArgsConstructor
private static class Body {
public final List<?> permissions;
}

@Override
public Future<Result<Boolean>> run(FutureCallback<Result<Boolean>> callback) {
List<WeaviatePermission> permissions = WeaviatePermission.mergePermissions(this.permissions);
return sendPostRequest(path(), new Body(permissions), callback, Result.voidToBooleanParser());
}

private String path() {
return String.format("/authz/roles/%s/add-permissions", this.role);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.weaviate.client.v1.async.rbac.api;

import java.util.concurrent.Future;

import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
import org.apache.hc.core5.concurrent.FutureCallback;

import io.weaviate.client.Config;
import io.weaviate.client.base.AsyncBaseClient;
import io.weaviate.client.base.AsyncClientResult;
import io.weaviate.client.base.Result;
import io.weaviate.client.v1.auth.provider.AccessTokenProvider;
import io.weaviate.client.v1.rbac.model.Permission;

public class PermissionChecker extends AsyncBaseClient<Boolean> implements AsyncClientResult<Boolean> {
private String role;
private Permission<?> permission;

public PermissionChecker(CloseableHttpAsyncClient httpClient, Config config, AccessTokenProvider tokenProvider) {
super(httpClient, config, tokenProvider);
}

public PermissionChecker withRole(String role) {
this.role = role;
return this;
}

public PermissionChecker withPermission(Permission<?> permission) {
this.permission = permission;
return this;
}

@Override
public Future<Result<Boolean>> run(FutureCallback<Result<Boolean>> callback) {
return sendPostRequest(path(), permission.firstToWeaviate(), Boolean.class, callback);
}

private String path() {
return String.format("/authz/roles/%s/has-permission", this.role);
}
}
Loading