diff --git a/.gitignore b/.gitignore index f0608fe1b..052460830 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ *.iml # Maven target/ +# maven-lombok-plugin +.factorypath diff --git a/pom.xml b/pom.xml index 17126b501..88b46a231 100644 --- a/pom.xml +++ b/pom.xml @@ -237,7 +237,7 @@ maven-compiler-plugin - 3.8.1 + 3.13.0 maven-surefire-plugin @@ -273,6 +273,13 @@ + + + org.projectlombok + lombok + ${lombok.version} + + org.apache.maven.plugins diff --git a/src/main/java/io/weaviate/client/WeaviateClient.java b/src/main/java/io/weaviate/client/WeaviateClient.java index 10be7f45b..e4e833d77 100644 --- a/src/main/java/io/weaviate/client/WeaviateClient.java +++ b/src/main/java/io/weaviate/client/WeaviateClient.java @@ -17,6 +17,7 @@ import io.weaviate.client.v1.graphql.GraphQL; 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 java.util.Optional; @@ -33,7 +34,8 @@ public WeaviateClient(Config config) { } public WeaviateClient(Config config, AccessTokenProvider tokenProvider) { - this(config, new CommonsHttpClientImpl(config.getHeaders(), tokenProvider, HttpApacheClientBuilder.build(config)), tokenProvider); + this(config, new CommonsHttpClientImpl(config.getHeaders(), tokenProvider, HttpApacheClientBuilder.build(config)), + tokenProvider); } public WeaviateClient(Config config, HttpClient httpClient, AccessTokenProvider tokenProvider) { @@ -87,10 +89,13 @@ public GraphQL graphQL() { return new GraphQL(httpClient, config); } + public Roles roles() { + return new Roles(httpClient, config); + } + private DbVersionProvider initDbVersionProvider() { MetaGetter metaGetter = new Misc(httpClient, config, null).metaGetter(); - DbVersionProvider.VersionGetter getter = () -> - Optional.ofNullable(metaGetter.run()) + DbVersionProvider.VersionGetter getter = () -> Optional.ofNullable(metaGetter.run()) .filter(result -> !result.hasErrors()) .map(result -> result.getResult().getVersion()); diff --git a/src/main/java/io/weaviate/client/base/BaseClient.java b/src/main/java/io/weaviate/client/base/BaseClient.java index 81cd6ed97..96f836bfd 100644 --- a/src/main/java/io/weaviate/client/base/BaseClient.java +++ b/src/main/java/io/weaviate/client/base/BaseClient.java @@ -45,7 +45,6 @@ private Response 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); diff --git a/src/main/java/io/weaviate/client/v1/backup/api/BackupCanceler.java b/src/main/java/io/weaviate/client/v1/backup/api/BackupCanceler.java index 15d8890a5..ac557566a 100644 --- a/src/main/java/io/weaviate/client/v1/backup/api/BackupCanceler.java +++ b/src/main/java/io/weaviate/client/v1/backup/api/BackupCanceler.java @@ -15,7 +15,8 @@ * BackupCanceler can cancel an in-progress backup by ID. * *

- * Canceling backups which have successfully completed before being interrupted is not supported and will result in an error. + * Canceling backups which have successfully completed before being interrupted + * is not supported and will result in an error. */ public class BackupCanceler extends BaseClient implements ClientResult { private String backend; @@ -70,4 +71,3 @@ private String path() { return path; } } - diff --git a/src/main/java/io/weaviate/client/v1/rbac/Roles.java b/src/main/java/io/weaviate/client/v1/rbac/Roles.java new file mode 100644 index 000000000..c1c0c63e7 --- /dev/null +++ b/src/main/java/io/weaviate/client/v1/rbac/Roles.java @@ -0,0 +1,93 @@ +package io.weaviate.client.v1.rbac; + +import io.weaviate.client.Config; +import io.weaviate.client.base.http.HttpClient; +import io.weaviate.client.v1.rbac.api.AssignedUsersGetter; +import io.weaviate.client.v1.rbac.api.PermissionAdder; +import io.weaviate.client.v1.rbac.api.PermissionChecker; +import io.weaviate.client.v1.rbac.api.PermissionRemover; +import io.weaviate.client.v1.rbac.api.RoleAllGetter; +import io.weaviate.client.v1.rbac.api.RoleAssigner; +import io.weaviate.client.v1.rbac.api.RoleCreator; +import io.weaviate.client.v1.rbac.api.RoleDeleter; +import io.weaviate.client.v1.rbac.api.RoleExists; +import io.weaviate.client.v1.rbac.api.RoleGetter; +import io.weaviate.client.v1.rbac.api.RoleRevoker; +import io.weaviate.client.v1.rbac.api.UserRolesGetter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class Roles { + + private final HttpClient httpClient; + private final Config config; + + /** Create a new role. */ + public RoleCreator creator() { + return new RoleCreator(httpClient, config); + } + + /** Delete a role. */ + public RoleDeleter deleter() { + return new RoleDeleter(httpClient, config); + } + + /** + * 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(httpClient, config); + } + + /** + * 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(httpClient, config); + } + + /** Check if a role has a permission. */ + public PermissionChecker permissionChecker() { + return new PermissionChecker(httpClient, config); + } + + /** Get all existing roles. */ + public RoleAllGetter allGetter() { + return new RoleAllGetter(httpClient, config); + }; + + /** Get role and its associated permissions. */ + public RoleGetter getter() { + return new RoleGetter(httpClient, config); + }; + + /** Get roles assigned to a user. */ + public UserRolesGetter userRolesGetter() { + return new UserRolesGetter(httpClient, config); + }; + + /** Get users assigned to a role. */ + public AssignedUsersGetter assignedUsersGetter() { + return new AssignedUsersGetter(httpClient, config); + }; + + /** Check if a role exists. */ + public RoleExists exists() { + return new RoleExists(httpClient, config); + } + + /** Assign a role to a user. Note that 'root' cannot be assigned. */ + public RoleAssigner assigner() { + return new RoleAssigner(httpClient, config); + } + + /** Revoke a role from a user. Note that 'root' cannot be revoked. */ + public RoleRevoker revoker() { + return new RoleRevoker(httpClient, config); + } +} diff --git a/src/main/java/io/weaviate/client/v1/rbac/api/AssignedUsersGetter.java b/src/main/java/io/weaviate/client/v1/rbac/api/AssignedUsersGetter.java new file mode 100644 index 000000000..263d170c4 --- /dev/null +++ b/src/main/java/io/weaviate/client/v1/rbac/api/AssignedUsersGetter.java @@ -0,0 +1,39 @@ +package io.weaviate.client.v1.rbac.api; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import io.weaviate.client.Config; +import io.weaviate.client.base.BaseClient; +import io.weaviate.client.base.ClientResult; +import io.weaviate.client.base.Response; +import io.weaviate.client.base.Result; +import io.weaviate.client.base.http.HttpClient; + +public class AssignedUsersGetter extends BaseClient implements ClientResult> { + private String role; + + public AssignedUsersGetter(HttpClient httpClient, Config config) { + super(httpClient, config); + } + + public AssignedUsersGetter withRole(String role) { + this.role = role; + return this; + } + + @Override + public Result> run() { + Response resp = sendGetRequest(path(), String[].class); + List 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); + } +} diff --git a/src/main/java/io/weaviate/client/v1/rbac/api/PermissionAdder.java b/src/main/java/io/weaviate/client/v1/rbac/api/PermissionAdder.java new file mode 100644 index 000000000..e98c5b65f --- /dev/null +++ b/src/main/java/io/weaviate/client/v1/rbac/api/PermissionAdder.java @@ -0,0 +1,47 @@ +package io.weaviate.client.v1.rbac.api; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import io.weaviate.client.Config; +import io.weaviate.client.base.BaseClient; +import io.weaviate.client.base.ClientResult; +import io.weaviate.client.base.Result; +import io.weaviate.client.base.http.HttpClient; +import io.weaviate.client.v1.rbac.model.Permission; +import lombok.AllArgsConstructor; + +public class PermissionAdder extends BaseClient implements ClientResult { + private String role; + private List> permissions = new ArrayList<>(); + + public PermissionAdder(HttpClient httpClient, Config config) { + super(httpClient, config); + } + + public PermissionAdder withRole(String name) { + this.role = name; + return this; + } + + public PermissionAdder withPermissions(Permission... permissions) { + this.permissions = Arrays.asList(permissions); + return this; + } + + @AllArgsConstructor + private static class Body { + public final List permissions; + } + + @Override + public Result run() { + List permissions = WeaviatePermission.mergePermissions(this.permissions); + return new Result(sendPostRequest(path(), new Body(permissions), Void.class)); + } + + private String path() { + return String.format("/authz/roles/%s/add-permissions", this.role); + } +} diff --git a/src/main/java/io/weaviate/client/v1/rbac/api/PermissionChecker.java b/src/main/java/io/weaviate/client/v1/rbac/api/PermissionChecker.java new file mode 100644 index 000000000..330e1e96b --- /dev/null +++ b/src/main/java/io/weaviate/client/v1/rbac/api/PermissionChecker.java @@ -0,0 +1,36 @@ +package io.weaviate.client.v1.rbac.api; + +import io.weaviate.client.Config; +import io.weaviate.client.base.BaseClient; +import io.weaviate.client.base.ClientResult; +import io.weaviate.client.base.Result; +import io.weaviate.client.base.http.HttpClient; +import io.weaviate.client.v1.rbac.model.Permission; + +public class PermissionChecker extends BaseClient implements ClientResult { + private String role; + private Permission permission; + + public PermissionChecker(HttpClient httpClient, Config config) { + super(httpClient, config); + } + + public PermissionChecker withRole(String role) { + this.role = role; + return this; + } + + public PermissionChecker withPermission(Permission permission) { + this.permission = permission; + return this; + } + + @Override + public Result run() { + return new Result(sendPostRequest(path(), permission.toWeaviate(), Boolean.class)); + } + + private String path() { + return String.format("/authz/roles/%s/has-permission", this.role); + } +} diff --git a/src/main/java/io/weaviate/client/v1/rbac/api/PermissionRemover.java b/src/main/java/io/weaviate/client/v1/rbac/api/PermissionRemover.java new file mode 100644 index 000000000..2d76eeab3 --- /dev/null +++ b/src/main/java/io/weaviate/client/v1/rbac/api/PermissionRemover.java @@ -0,0 +1,47 @@ +package io.weaviate.client.v1.rbac.api; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import io.weaviate.client.Config; +import io.weaviate.client.base.BaseClient; +import io.weaviate.client.base.ClientResult; +import io.weaviate.client.base.Result; +import io.weaviate.client.base.http.HttpClient; +import io.weaviate.client.v1.rbac.model.Permission; +import lombok.AllArgsConstructor; + +public class PermissionRemover extends BaseClient implements ClientResult { + private String role; + private List> permissions = new ArrayList<>(); + + public PermissionRemover(HttpClient httpClient, Config config) { + super(httpClient, config); + } + + public PermissionRemover withRole(String role) { + this.role = role; + return this; + } + + public PermissionRemover withPermissions(Permission... permissions) { + this.permissions = Arrays.asList(permissions); + return this; + } + + @AllArgsConstructor + private static class Body { + public final List permissions; + } + + @Override + public Result run() { + List permissions = WeaviatePermission.mergePermissions(this.permissions); + return new Result(sendPostRequest(path(), new Body(permissions), Void.class)); + } + + private String path() { + return String.format("/authz/roles/%s/remove-permissions", this.role); + } +} diff --git a/src/main/java/io/weaviate/client/v1/rbac/api/RoleAllGetter.java b/src/main/java/io/weaviate/client/v1/rbac/api/RoleAllGetter.java new file mode 100644 index 000000000..fe1f55bdc --- /dev/null +++ b/src/main/java/io/weaviate/client/v1/rbac/api/RoleAllGetter.java @@ -0,0 +1,33 @@ +package io.weaviate.client.v1.rbac.api; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import io.weaviate.client.Config; +import io.weaviate.client.base.BaseClient; +import io.weaviate.client.base.ClientResult; +import io.weaviate.client.base.Response; +import io.weaviate.client.base.Result; +import io.weaviate.client.base.http.HttpClient; +import io.weaviate.client.v1.rbac.model.Role; + +public class RoleAllGetter extends BaseClient implements ClientResult> { + + public RoleAllGetter(HttpClient httpClient, Config config) { + super(httpClient, config); + } + + @Override + public Result> run() { + Response resp = sendGetRequest("/authz/roles", WeaviateRole[].class); + List roles = Optional.ofNullable(resp.getBody()) + .map(Arrays::asList) + .orElse(new ArrayList<>()) + .stream() + .map(w -> w.toRole()) + .toList(); + return new Result<>(resp.getStatusCode(), roles, resp.getErrors()); + } +} diff --git a/src/main/java/io/weaviate/client/v1/rbac/api/RoleAssigner.java b/src/main/java/io/weaviate/client/v1/rbac/api/RoleAssigner.java new file mode 100644 index 000000000..cc9ca3686 --- /dev/null +++ b/src/main/java/io/weaviate/client/v1/rbac/api/RoleAssigner.java @@ -0,0 +1,46 @@ +package io.weaviate.client.v1.rbac.api; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import io.weaviate.client.Config; +import io.weaviate.client.base.BaseClient; +import io.weaviate.client.base.ClientResult; +import io.weaviate.client.base.Result; +import io.weaviate.client.base.http.HttpClient; +import lombok.AllArgsConstructor; + +public class RoleAssigner extends BaseClient implements ClientResult { + private String user; + private List roles = new ArrayList<>(); + + public RoleAssigner(HttpClient httpClient, Config config) { + super(httpClient, config); + } + + public RoleAssigner withUser(String user) { + this.user = user; + return this; + } + + public RoleAssigner witRoles(String... roles) { + this.roles = Arrays.asList(roles); + return this; + } + + /** The API signature for this method is { "roles": [...] } */ + @AllArgsConstructor + private static class Body { + public final List roles; + } + + @Override + public Result run() { + return new Result(sendPostRequest(path(), new Body(this.roles), Void.class)); + } + + private String path() { + return String.format("/authz/users/%s/assign", this.user); + } +} diff --git a/src/main/java/io/weaviate/client/v1/rbac/api/RoleCreator.java b/src/main/java/io/weaviate/client/v1/rbac/api/RoleCreator.java new file mode 100644 index 000000000..66078f9b5 --- /dev/null +++ b/src/main/java/io/weaviate/client/v1/rbac/api/RoleCreator.java @@ -0,0 +1,37 @@ +package io.weaviate.client.v1.rbac.api; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import io.weaviate.client.Config; +import io.weaviate.client.base.BaseClient; +import io.weaviate.client.base.ClientResult; +import io.weaviate.client.base.Result; +import io.weaviate.client.base.http.HttpClient; +import io.weaviate.client.v1.rbac.model.Permission; + +public class RoleCreator extends BaseClient implements ClientResult { + private String name; + private List> permissions = new ArrayList<>(); + + public RoleCreator(HttpClient httpClient, Config config) { + super(httpClient, config); + } + + public RoleCreator withName(String name) { + this.name = name; + return this; + } + + public RoleCreator withPermissions(Permission... permissions) { + this.permissions = Arrays.asList(permissions); + return this; + } + + @Override + public Result run() { + WeaviateRole role = new WeaviateRole(this.name, this.permissions); + return new Result(sendPostRequest("/authz/roles", role, Void.class)); + } +} diff --git a/src/main/java/io/weaviate/client/v1/rbac/api/RoleDeleter.java b/src/main/java/io/weaviate/client/v1/rbac/api/RoleDeleter.java new file mode 100644 index 000000000..4a25b8847 --- /dev/null +++ b/src/main/java/io/weaviate/client/v1/rbac/api/RoleDeleter.java @@ -0,0 +1,25 @@ +package io.weaviate.client.v1.rbac.api; + +import io.weaviate.client.Config; +import io.weaviate.client.base.BaseClient; +import io.weaviate.client.base.ClientResult; +import io.weaviate.client.base.Result; +import io.weaviate.client.base.http.HttpClient; + +public class RoleDeleter extends BaseClient implements ClientResult { + private String name; + + public RoleDeleter(HttpClient httpClient, Config config) { + super(httpClient, config); + } + + public RoleDeleter withName(String name) { + this.name = name; + return this; + } + + @Override + public Result run() { + return new Result(sendDeleteRequest("/authz/roles/" + this.name, null, Void.class)); + } +} diff --git a/src/main/java/io/weaviate/client/v1/rbac/api/RoleExists.java b/src/main/java/io/weaviate/client/v1/rbac/api/RoleExists.java new file mode 100644 index 000000000..6b1d5731d --- /dev/null +++ b/src/main/java/io/weaviate/client/v1/rbac/api/RoleExists.java @@ -0,0 +1,38 @@ +package io.weaviate.client.v1.rbac.api; + +import org.apache.hc.core5.http.HttpStatus; + +import io.weaviate.client.Config; +import io.weaviate.client.base.BaseClient; +import io.weaviate.client.base.ClientResult; +import io.weaviate.client.base.Result; +import io.weaviate.client.base.WeaviateError; +import io.weaviate.client.base.WeaviateErrorResponse; +import io.weaviate.client.base.http.HttpClient; +import io.weaviate.client.v1.rbac.model.Role; + +public class RoleExists extends BaseClient implements ClientResult { + private RoleGetter getter; + + public RoleExists(HttpClient httpClient, Config config) { + super(httpClient, config); + this.getter = new RoleGetter(httpClient, config); + } + + public RoleExists withName(String name) { + this.getter.withName(name); + return this; + } + + @Override + public Result run() { + Result resp = this.getter.run(); + if (resp.hasErrors()) { + WeaviateError error = resp.getError(); + return new Result<>(error.getStatusCode(), null, + WeaviateErrorResponse.builder().error(error.getMessages()).build()); + + } + return new Result(HttpStatus.SC_OK, resp.getResult() != null, null); + } +} diff --git a/src/main/java/io/weaviate/client/v1/rbac/api/RoleGetter.java b/src/main/java/io/weaviate/client/v1/rbac/api/RoleGetter.java new file mode 100644 index 000000000..c07672311 --- /dev/null +++ b/src/main/java/io/weaviate/client/v1/rbac/api/RoleGetter.java @@ -0,0 +1,31 @@ +package io.weaviate.client.v1.rbac.api; + +import java.util.Optional; + +import io.weaviate.client.Config; +import io.weaviate.client.base.BaseClient; +import io.weaviate.client.base.ClientResult; +import io.weaviate.client.base.Response; +import io.weaviate.client.base.Result; +import io.weaviate.client.base.http.HttpClient; +import io.weaviate.client.v1.rbac.model.Role; + +public class RoleGetter extends BaseClient implements ClientResult { + private String name; + + public RoleGetter(HttpClient httpClient, Config config) { + super(httpClient, config); + } + + public RoleGetter withName(String name) { + this.name = name; + return this; + } + + @Override + public Result run() { + Response resp = sendGetRequest("/authz/roles/" + this.name, WeaviateRole.class); + Role role = Optional.ofNullable(resp.getBody()).map(WeaviateRole::toRole).orElse(null); + return new Result(resp.getStatusCode(), role, resp.getErrors()); + } +} diff --git a/src/main/java/io/weaviate/client/v1/rbac/api/RoleRevoker.java b/src/main/java/io/weaviate/client/v1/rbac/api/RoleRevoker.java new file mode 100644 index 000000000..d6f888c46 --- /dev/null +++ b/src/main/java/io/weaviate/client/v1/rbac/api/RoleRevoker.java @@ -0,0 +1,47 @@ +package io.weaviate.client.v1.rbac.api; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import io.weaviate.client.Config; +import io.weaviate.client.base.BaseClient; +import io.weaviate.client.base.ClientResult; +import io.weaviate.client.base.Result; +import io.weaviate.client.base.http.HttpClient; +import lombok.AllArgsConstructor; + +public class RoleRevoker extends BaseClient implements ClientResult { + private String user; + private List roles = new ArrayList<>(); + + public RoleRevoker(HttpClient httpClient, Config config) { + super(httpClient, config); + } + + public RoleRevoker withUser(String user) { + this.user = user; + return this; + } + + public RoleRevoker witRoles(String... roles) { + this.roles = Collections.unmodifiableList(Arrays.asList(roles)); + return this; + } + + /** The API signature for this method is { "roles": [...] } */ + @AllArgsConstructor + private static class Body { + public final List roles; + } + + @Override + public Result run() { + return new Result(sendPostRequest(path(), new Body(this.roles), Void.class)); + } + + private String path() { + return String.format("/authz/users/%s/revoke", this.user); + } +} diff --git a/src/main/java/io/weaviate/client/v1/rbac/api/UserRolesGetter.java b/src/main/java/io/weaviate/client/v1/rbac/api/UserRolesGetter.java new file mode 100644 index 000000000..5ee7fa2c6 --- /dev/null +++ b/src/main/java/io/weaviate/client/v1/rbac/api/UserRolesGetter.java @@ -0,0 +1,49 @@ +package io.weaviate.client.v1.rbac.api; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import io.weaviate.client.Config; +import io.weaviate.client.base.BaseClient; +import io.weaviate.client.base.ClientResult; +import io.weaviate.client.base.Response; +import io.weaviate.client.base.Result; +import io.weaviate.client.base.http.HttpClient; +import io.weaviate.client.v1.rbac.model.Role; + +public class UserRolesGetter extends BaseClient implements ClientResult> { + private String user; + + public UserRolesGetter(HttpClient httpClient, Config config) { + super(httpClient, config); + } + + /** Leave unset to fetch roles assigned to the current user. */ + public UserRolesGetter withUser(String user) { + this.user = user; + return this; + } + + @Override + public Result> run() { + Response resp; + if (this.user == null) { + resp = sendGetRequest("/authz/users/own-roles", WeaviateRole[].class); + } else { + resp = sendGetRequest(path(), WeaviateRole[].class); + } + List roles = Optional.ofNullable(resp.getBody()) + .map(Arrays::asList) + .orElse(new ArrayList<>()) + .stream() + .map(w -> w.toRole()) + .toList(); + return new Result<>(resp.getStatusCode(), roles, resp.getErrors()); + } + + private String path() { + return String.format("/authz/users/%s/roles", this.user); + } +} diff --git a/src/main/java/io/weaviate/client/v1/rbac/api/WeaviatePermission.java b/src/main/java/io/weaviate/client/v1/rbac/api/WeaviatePermission.java new file mode 100644 index 000000000..26d6b159e --- /dev/null +++ b/src/main/java/io/weaviate/client/v1/rbac/api/WeaviatePermission.java @@ -0,0 +1,52 @@ +package io.weaviate.client.v1.rbac.api; + +import java.util.List; + +import io.weaviate.client.v1.rbac.model.BackupsPermission; +import io.weaviate.client.v1.rbac.model.CollectionsPermission; +import io.weaviate.client.v1.rbac.model.DataPermission; +import io.weaviate.client.v1.rbac.model.NodesPermission; +import io.weaviate.client.v1.rbac.model.Permission; +import io.weaviate.client.v1.rbac.model.RolesPermission; +import io.weaviate.client.v1.rbac.model.TenantsPermission; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class WeaviatePermission { + String action; + BackupsPermission backups; + CollectionsPermission collections; + DataPermission data; + NodesPermission nodes; + RolesPermission roles; + TenantsPermission tenants; + + public WeaviatePermission(String action) { + this.action = action; + } + + public

> WeaviatePermission(String action, Permission

perm) { + this.action = action; + if (perm instanceof BackupsPermission) { + this.backups = (BackupsPermission) perm; + } else if (perm instanceof CollectionsPermission) { + this.collections = (CollectionsPermission) perm; + } else if (perm instanceof DataPermission) { + this.data = (DataPermission) perm; + } else if (perm instanceof NodesPermission) { + this.nodes = (NodesPermission) perm; + } else if (perm instanceof RolesPermission) { + this.roles = (RolesPermission) perm; + } else if (perm instanceof TenantsPermission) { + this.tenants = (TenantsPermission) perm; + } + } + + public static List mergePermissions(List> permissions) { + return permissions.stream().map(perm -> perm.toWeaviate()).toList(); + } +} diff --git a/src/main/java/io/weaviate/client/v1/rbac/api/WeaviateRole.java b/src/main/java/io/weaviate/client/v1/rbac/api/WeaviateRole.java new file mode 100644 index 000000000..2675ac724 --- /dev/null +++ b/src/main/java/io/weaviate/client/v1/rbac/api/WeaviateRole.java @@ -0,0 +1,24 @@ +package io.weaviate.client.v1.rbac.api; + +import java.util.List; + +import io.weaviate.client.v1.rbac.model.Permission; +import io.weaviate.client.v1.rbac.model.Role; +import lombok.Getter; + +@Getter +public class WeaviateRole { + String name; + List permissions; + + public WeaviateRole(String name, List> permissions) { + this.name = name; + this.permissions = WeaviatePermission.mergePermissions(permissions); + } + + public Role toRole() { + List> permissions = this.permissions.stream() + .>map(perm -> Permission.fromWeaviate(perm)).toList(); + return new Role(this.name, permissions); + } +} diff --git a/src/main/java/io/weaviate/client/v1/rbac/model/BackupsPermission.java b/src/main/java/io/weaviate/client/v1/rbac/model/BackupsPermission.java new file mode 100644 index 000000000..e40bafb5e --- /dev/null +++ b/src/main/java/io/weaviate/client/v1/rbac/model/BackupsPermission.java @@ -0,0 +1,34 @@ +package io.weaviate.client.v1.rbac.model; + +import io.weaviate.client.v1.rbac.api.WeaviatePermission; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode(callSuper = true) +public class BackupsPermission extends Permission { + final String collection; + + public BackupsPermission(Action action, String collection) { + super(action); + this.collection = collection; + } + + BackupsPermission(String action, String collection) { + this(CustomAction.fromString(Action.class, action), collection); + } + + @Override + public WeaviatePermission toWeaviate() { + return new WeaviatePermission(this.action, this); + } + + @AllArgsConstructor + public enum Action implements CustomAction { + MANAGE("manage_backups"); + + @Getter + private final String value; + } +} diff --git a/src/main/java/io/weaviate/client/v1/rbac/model/ClusterPermission.java b/src/main/java/io/weaviate/client/v1/rbac/model/ClusterPermission.java new file mode 100644 index 000000000..c70dd7a5f --- /dev/null +++ b/src/main/java/io/weaviate/client/v1/rbac/model/ClusterPermission.java @@ -0,0 +1,31 @@ +package io.weaviate.client.v1.rbac.model; + +import io.weaviate.client.v1.rbac.api.WeaviatePermission; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode(callSuper = true) +public class ClusterPermission extends Permission { + public ClusterPermission(Action action) { + super(action); + } + + ClusterPermission(String action) { + this(CustomAction.fromString(Action.class, action)); + } + + @Override + public WeaviatePermission toWeaviate() { + return new WeaviatePermission(this.action); + } + + @AllArgsConstructor + public enum Action implements CustomAction { + READ("read_cluster"); + + @Getter + private final String value; + } +} diff --git a/src/main/java/io/weaviate/client/v1/rbac/model/CollectionsPermission.java b/src/main/java/io/weaviate/client/v1/rbac/model/CollectionsPermission.java new file mode 100644 index 000000000..a5b97e8b1 --- /dev/null +++ b/src/main/java/io/weaviate/client/v1/rbac/model/CollectionsPermission.java @@ -0,0 +1,46 @@ +package io.weaviate.client.v1.rbac.model; + +import io.weaviate.client.v1.rbac.api.WeaviatePermission; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode(callSuper = true) +public class CollectionsPermission extends Permission { + final String collection; + final String tenant; + + public CollectionsPermission(Action action, String collection) { + this(action, collection, "*"); + } + + CollectionsPermission(String action, String collection) { + this(CustomAction.fromString(Action.class, action), collection); + } + + private CollectionsPermission(Action action, String collection, String tenant) { + super(action); + this.collection = collection; + this.tenant = tenant; + } + + @Override + public WeaviatePermission toWeaviate() { + return new WeaviatePermission(this.action, this); + } + + @AllArgsConstructor + public enum Action implements CustomAction { + CREATE("create_collections"), + READ("read_collections"), + UPDATE("update_collections"), + DELETE("delete_collections"); + + // Not part of the public API yet. + // MANAGE("manage_collections"); + + @Getter + private final String value; + } +} diff --git a/src/main/java/io/weaviate/client/v1/rbac/model/DataPermission.java b/src/main/java/io/weaviate/client/v1/rbac/model/DataPermission.java new file mode 100644 index 000000000..700119b39 --- /dev/null +++ b/src/main/java/io/weaviate/client/v1/rbac/model/DataPermission.java @@ -0,0 +1,46 @@ +package io.weaviate.client.v1.rbac.model; + +import io.weaviate.client.v1.rbac.api.WeaviatePermission; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode(callSuper = true) +public class DataPermission extends Permission { + final String collection; + final String object; + final String tenant; + + public DataPermission(Action action, String collection) { + this(action, collection, "*", "*"); + } + + DataPermission(String action, String collection) { + this(CustomAction.fromString(Action.class, action), collection); + } + + private DataPermission(Action action, String collection, String object, String tenant) { + super(action); + this.collection = collection; + this.object = object; + this.tenant = tenant; + } + + @Override + public WeaviatePermission toWeaviate() { + return new WeaviatePermission(this.action, this); + } + + @AllArgsConstructor + public enum Action implements CustomAction { + CREATE("create_data"), + READ("read_data"), + UPDATE("update_data"), + DELETE("delete_data"), + MANAGE("manage_data"); + + @Getter + private final String value; + } +} diff --git a/src/main/java/io/weaviate/client/v1/rbac/model/NodesPermission.java b/src/main/java/io/weaviate/client/v1/rbac/model/NodesPermission.java new file mode 100644 index 000000000..2d2794f7d --- /dev/null +++ b/src/main/java/io/weaviate/client/v1/rbac/model/NodesPermission.java @@ -0,0 +1,55 @@ +package io.weaviate.client.v1.rbac.model; + +import com.google.gson.annotations.SerializedName; + +import io.weaviate.client.v1.rbac.api.WeaviatePermission; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode(callSuper = true) +public class NodesPermission extends Permission { + final String collection; + final Verbosity verbosity; + + public NodesPermission(Action action, Verbosity verbosity) { + this(action, verbosity, "*"); + } + + NodesPermission(String action, Verbosity verbosity) { + this(CustomAction.fromString(Action.class, action), verbosity); + } + + NodesPermission(String action, Verbosity verbosity, String collection) { + this(CustomAction.fromString(Action.class, action), verbosity, collection); + } + + public NodesPermission(Action action, Verbosity verbosity, String collection) { + super(action); + this.collection = collection; + this.verbosity = verbosity; + } + + @Override + public WeaviatePermission toWeaviate() { + return new WeaviatePermission(this.action, this); + } + + @AllArgsConstructor + public enum Action implements CustomAction { + READ("read_nodes"); + + @Getter + private final String value; + } + + @AllArgsConstructor + public enum Verbosity { + @SerializedName("minimal") + MINIMAL, + @SerializedName("verbose") + VERBOSE; + } + +} diff --git a/src/main/java/io/weaviate/client/v1/rbac/model/Permission.java b/src/main/java/io/weaviate/client/v1/rbac/model/Permission.java new file mode 100644 index 000000000..76b1616f3 --- /dev/null +++ b/src/main/java/io/weaviate/client/v1/rbac/model/Permission.java @@ -0,0 +1,106 @@ +package io.weaviate.client.v1.rbac.model; + +import io.weaviate.client.v1.rbac.api.WeaviatePermission; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode +public abstract class Permission

> { + @Getter + final transient String action; + + Permission(CustomAction action) { + this.action = action.getValue(); + } + + public abstract WeaviatePermission toWeaviate(); + + public static Permission fromWeaviate(WeaviatePermission perm) { + String action = perm.getAction(); + if (perm.getBackups() != null) { + return new BackupsPermission(action, perm.getBackups().getCollection()); + } else if (perm.getCollections() != null) { + return new CollectionsPermission(action, perm.getCollections().getCollection()); + } else if (perm.getData() != null) { + return new DataPermission(action, perm.getData().getCollection()); + } else if (perm.getNodes() != null) { + NodesPermission nodes = perm.getNodes(); + if (nodes.getCollection() != null) { + return new NodesPermission(action, perm.getNodes().getVerbosity(), nodes.getCollection()); + } + return new NodesPermission(action, perm.getNodes().getVerbosity()); + } else if (perm.getRoles() != null) { + return new RolesPermission(action, perm.getRoles().getRole()); + } else if (perm.getTenants() != null) { + return new TenantsPermission(action); + } else if (CustomAction.isValid(ClusterPermission.Action.class, action)) { + return new ClusterPermission(action); + } else if (CustomAction.isValid(UsersPermission.Action.class, action)) { + return new UsersPermission(action); + } + return null; + } + + public static BackupsPermission backups(BackupsPermission.Action action, String collection) { + return new BackupsPermission(action, collection); + } + + public static ClusterPermission cluster(ClusterPermission.Action action) { + return new ClusterPermission(action); + } + + public static CollectionsPermission collections(CollectionsPermission.Action action, String collection) { + return new CollectionsPermission(action, collection); + } + + public static DataPermission data(DataPermission.Action action, String collection) { + return new DataPermission(action, collection); + } + + public static NodesPermission nodes(NodesPermission.Action action, NodesPermission.Verbosity verbosity) { + return new NodesPermission(action, verbosity); + } + + public static NodesPermission nodes(NodesPermission.Action action, NodesPermission.Verbosity verbosity, + String collection) { + return new NodesPermission(action, verbosity, collection); + } + + public static RolesPermission roles(RolesPermission.Action action, String role) { + return new RolesPermission(action, role); + } + + public static TenantsPermission tenants(TenantsPermission.Action action) { + return new TenantsPermission(action); + } + + // public static UsersPermission users(UsersPermission.Action action) { + // return new UsersPermission(action); + // } + + public String toString() { + return String.format("Permission", this.action); + } +} + +interface CustomAction { + String getValue(); + + static & CustomAction> E fromString(Class enumClass, String value) { + for (E action : enumClass.getEnumConstants()) { + if (action.getValue().equals(value)) { + return action; + } + } + throw new IllegalArgumentException("No enum constant for value: " + value); + } + + static boolean isValid(Class enumClass, String value) { + for (CustomAction action : enumClass.getEnumConstants()) { + if (action.getValue().equals(value)) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/io/weaviate/client/v1/rbac/model/Role.java b/src/main/java/io/weaviate/client/v1/rbac/model/Role.java new file mode 100644 index 000000000..21597d0fc --- /dev/null +++ b/src/main/java/io/weaviate/client/v1/rbac/model/Role.java @@ -0,0 +1,24 @@ +package io.weaviate.client.v1.rbac.model; + +import java.util.ArrayList; +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@EqualsAndHashCode +public class Role { + public final String name; + public List> permissions = new ArrayList<>(); + + public String toString() { + return String.format( + "Role", + this.name, permissions.isEmpty() + ? "none" + : String.join(", ", permissions.stream().map(Permission::getAction).toList())); + } +} diff --git a/src/main/java/io/weaviate/client/v1/rbac/model/RolesPermission.java b/src/main/java/io/weaviate/client/v1/rbac/model/RolesPermission.java new file mode 100644 index 000000000..950d4cca4 --- /dev/null +++ b/src/main/java/io/weaviate/client/v1/rbac/model/RolesPermission.java @@ -0,0 +1,35 @@ +package io.weaviate.client.v1.rbac.model; + +import io.weaviate.client.v1.rbac.api.WeaviatePermission; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode(callSuper = true) +public class RolesPermission extends Permission { + final String role; + + public RolesPermission(Action action, String role) { + super(action); + this.role = role; + } + + RolesPermission(String action, String role) { + this(CustomAction.fromString(Action.class, action), role); + } + + @Override + public WeaviatePermission toWeaviate() { + return new WeaviatePermission(this.action, this); + } + + @AllArgsConstructor + public enum Action implements CustomAction { + READ("read_roles"), + MANAGE("manage_roles"); + + @Getter + private final String value; + } +} diff --git a/src/main/java/io/weaviate/client/v1/rbac/model/TenantsPermission.java b/src/main/java/io/weaviate/client/v1/rbac/model/TenantsPermission.java new file mode 100644 index 000000000..c5002249a --- /dev/null +++ b/src/main/java/io/weaviate/client/v1/rbac/model/TenantsPermission.java @@ -0,0 +1,41 @@ +package io.weaviate.client.v1.rbac.model; + +import io.weaviate.client.v1.rbac.api.WeaviatePermission; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode(callSuper = true) +public class TenantsPermission extends Permission { + final String tenant; + + public TenantsPermission(Action action) { + this(action, "*"); + } + + TenantsPermission(String action) { + this(CustomAction.fromString(Action.class, action)); + } + + private TenantsPermission(Action action, String tenant) { + super(action); + this.tenant = tenant; + } + + @Override + public WeaviatePermission toWeaviate() { + return new WeaviatePermission(this.action, this); + } + + @AllArgsConstructor + public enum Action implements CustomAction { + CREATE("create_tenants"), + READ("read_tenants"), + UPDATE("update_tenants"), + DELETE("delete_tenants"); + + @Getter + private final String value; + } +} diff --git a/src/main/java/io/weaviate/client/v1/rbac/model/UsersPermission.java b/src/main/java/io/weaviate/client/v1/rbac/model/UsersPermission.java new file mode 100644 index 000000000..b7758bf80 --- /dev/null +++ b/src/main/java/io/weaviate/client/v1/rbac/model/UsersPermission.java @@ -0,0 +1,34 @@ + +package io.weaviate.client.v1.rbac.model; + +import io.weaviate.client.v1.rbac.api.WeaviatePermission; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * UsersPermission controls access to dynamic user management capabilities. + * These will be introduced in v1.30. Until then the class will remain + * package-private. + */ +class UsersPermission extends Permission { + public UsersPermission(Action action) { + super(action); + } + + UsersPermission(String action) { + this(CustomAction.fromString(Action.class, action)); + } + + @Override + public WeaviatePermission toWeaviate() { + return new WeaviatePermission(this.action); + } + + @AllArgsConstructor + public enum Action implements CustomAction { + MANAGE("manage_users"); + + @Getter + private final String value; + } +} diff --git a/src/test/java/io/weaviate/client/v1/rbac/model/PermissionTest.java b/src/test/java/io/weaviate/client/v1/rbac/model/PermissionTest.java new file mode 100644 index 000000000..d3278befb --- /dev/null +++ b/src/test/java/io/weaviate/client/v1/rbac/model/PermissionTest.java @@ -0,0 +1,126 @@ +package io.weaviate.client.v1.rbac.model; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.function.Supplier; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.testcontainers.shaded.org.hamcrest.Matcher; +import org.testcontainers.shaded.org.hamcrest.MatcherAssert; +import org.testcontainers.shaded.org.hamcrest.beans.SamePropertyValuesAs; + +import com.jparams.junit4.JParamsTestRunner; +import com.jparams.junit4.data.DataMethod; + +import io.weaviate.client.v1.rbac.api.WeaviatePermission; +import io.weaviate.client.v1.rbac.model.NodesPermission.Verbosity; + +@RunWith(JParamsTestRunner.class) +public class PermissionTest { + public static Object[][] serializationTestCases() { + UsersPermission users = new UsersPermission(UsersPermission.Action.MANAGE); + BackupsPermission backups = new BackupsPermission(BackupsPermission.Action.MANAGE, "Pizza"); + DataPermission data = new DataPermission(DataPermission.Action.MANAGE, "Pizza"); + NodesPermission nodes = new NodesPermission(NodesPermission.Action.READ, Verbosity.MINIMAL, "Pizza"); + RolesPermission roles = new RolesPermission(RolesPermission.Action.MANAGE, "TestWriter"); + CollectionsPermission collections = new CollectionsPermission(CollectionsPermission.Action.CREATE, "Pizza"); + ClusterPermission cluster = new ClusterPermission(ClusterPermission.Action.READ); + TenantsPermission tenants = new TenantsPermission(TenantsPermission.Action.READ); + + return new Object[][] { + { + "user permission", + (Supplier>) () -> users, + new WeaviatePermission("manage_users"), + }, + { + "backup permission", + (Supplier>) () -> backups, + new WeaviatePermission("manage_backups", backups), + }, + { + "data permission", + (Supplier>) () -> data, + new WeaviatePermission("manage_data", data), + }, + { + "nodes permission", + (Supplier>) () -> nodes, + new WeaviatePermission("read_nodes", nodes), + }, + { + "roles permission", + (Supplier>) () -> roles, + new WeaviatePermission("manage_roles", roles), + }, + { + "collections permission", + (Supplier>) () -> collections, + new WeaviatePermission("create_collections", collections), + }, + { + "cluster permission", + (Supplier>) () -> cluster, + new WeaviatePermission("read_cluster"), + }, + { + "tenants permission", + (Supplier>) () -> tenants, + new WeaviatePermission("read_tenants", tenants), + }, + }; + } + + @DataMethod(source = PermissionTest.class, method = "serializationTestCases") + @Test + public void testToWeaviate(String name, Supplier> permFunc, WeaviatePermission expected) + throws Exception { + Permission perm = permFunc.get(); + MatcherAssert.assertThat(name, perm.toWeaviate(), sameAs(expected)); + } + + private static Matcher sameAs(T expected) { + return new SamePropertyValuesAs(expected, new ArrayList<>()); + } + + @Test + public void testDefaultDataPermission() { + DataPermission perm = new DataPermission(DataPermission.Action.MANAGE, "Pizza"); + assertThat(perm).as("data permission must have object=* and tenant=*") + .returns("*", DataPermission::getObject) + .returns("*", DataPermission::getTenant); + } + + @Test + public void testDefaultCollectionsPermission() { + CollectionsPermission perm = new CollectionsPermission(CollectionsPermission.Action.CREATE, "Pizza"); + assertThat(perm).as("collection permission must have tenant=*") + .returns("*", CollectionsPermission::getTenant); + } + + @Test + public void testDefaultNodesPermission() { + NodesPermission perm = new NodesPermission(NodesPermission.Action.READ, NodesPermission.Verbosity.MINIMAL); + assertThat(perm).as("nodes permission should affect all collections if one is not specified") + .returns("*", NodesPermission::getCollection); + } + + @Test + public void testDefaultTenantsPermission() { + TenantsPermission perm = new TenantsPermission(TenantsPermission.Action.READ); + assertThat(perm).as("tenants permission must have tenant=*") + .returns("*", TenantsPermission::getTenant); + } + + @DataMethod(source = PermissionTest.class, method = "serializationTestCases") + @Test + public void testFromWeaviate(String name, + Supplier> expectedFunc, WeaviatePermission input) + throws Exception { + Permission expected = expectedFunc.get(); + Permission actual = Permission.fromWeaviate(input); + MatcherAssert.assertThat(name, actual, sameAs(expected)); + } +} diff --git a/src/test/java/io/weaviate/integration/client/WeaviateDockerCompose.java b/src/test/java/io/weaviate/integration/client/WeaviateDockerCompose.java index bc71c2625..78926d58a 100644 --- a/src/test/java/io/weaviate/integration/client/WeaviateDockerCompose.java +++ b/src/test/java/io/weaviate/integration/client/WeaviateDockerCompose.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.List; + import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; @@ -13,22 +14,40 @@ public class WeaviateDockerCompose implements TestRule { + /** Weaviate Docker image to create a container from. */ private final String weaviateVersion; private final boolean withOffloadS3; + /** Username of the admin user for instances using RBAC. */ + private final String adminUser; + public WeaviateDockerCompose() { this.weaviateVersion = WeaviateDockerImage.WEAVIATE_DOCKER_IMAGE; this.withOffloadS3 = false; + this.adminUser = null; } public WeaviateDockerCompose(String version) { this.weaviateVersion = String.format("semitechnologies/weaviate:%s", version); this.withOffloadS3 = false; + this.adminUser = null; } public WeaviateDockerCompose(String version, boolean withOffloadS3) { this.weaviateVersion = String.format("semitechnologies/weaviate:%s", version); this.withOffloadS3 = withOffloadS3; + this.adminUser = null; + } + + public WeaviateDockerCompose(String version, String adminUser) { + this.weaviateVersion = WeaviateDockerImage.WEAVIATE_DOCKER_IMAGE; + this.withOffloadS3 = false; + this.adminUser = adminUser; + } + + /** Create docker-compose deployment with auth and RBAC-authz enabled. */ + public static WeaviateDockerCompose rbac(String adminUser) { + return new WeaviateDockerCompose(WeaviateDockerImage.WEAVIATE_DOCKER_IMAGE, adminUser); } public static class Weaviate extends WeaviateContainer { @@ -57,6 +76,27 @@ public Weaviate(String dockerImageName, boolean withOffloadS3) { withEnv("ENABLE_MODULES", String.join(",", enableModules)); withCreateContainerCmdModifier(cmd -> cmd.withHostName("weaviate")); } + + /** Create Weaviate container with RBAC authz and an admin user. */ + public Weaviate(String dockerImageName, boolean withOffloadS3, String adminUser) { + this(dockerImageName, withOffloadS3); + withEnv("AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED", "false"); + withEnv("AUTHENTICATION_APIKEY_ENABLED", "true"); + withEnv("AUTHORIZATION_RBAC_ENABLED", "true"); + withEnv("AUTHENTICATION_APIKEY_USERS", adminUser); + withEnv("AUTHENTICATION_APIKEY_ALLOWED_KEYS", makeSecret(adminUser)); + withEnv("AUTHORIZATION_ADMIN_USERS", adminUser); + } + + /** + * Generate API secret for a username. When running an instance with + * authentication enabled, {@link Weaviate} will use this method to generate + * secrets for all users. + * Use this method to get a valid API key for a test client. + */ + public static String makeSecret(String user) { + return user + "-secret"; + } } public static class Contextionary extends GenericContainer { @@ -98,7 +138,11 @@ public void start() { } contextionary = new Contextionary(); contextionary.start(); - weaviate = new Weaviate(this.weaviateVersion, withOffloadS3); + if (adminUser == null) { + weaviate = new Weaviate(this.weaviateVersion, this.withOffloadS3); + } else { + weaviate = new Weaviate(this.weaviateVersion, this.withOffloadS3, this.adminUser); + } weaviate.start(); } diff --git a/src/test/java/io/weaviate/integration/client/WeaviateVersion.java b/src/test/java/io/weaviate/integration/client/WeaviateVersion.java index dc37e52f5..75ef83870 100644 --- a/src/test/java/io/weaviate/integration/client/WeaviateVersion.java +++ b/src/test/java/io/weaviate/integration/client/WeaviateVersion.java @@ -3,12 +3,12 @@ public class WeaviateVersion { // docker image version - public static final String WEAVIATE_IMAGE = "1.27.7-8f0e033"; + public static final String WEAVIATE_IMAGE = "1.28.3-f73fcee"; // to be set according to weaviate docker image - public static final String EXPECTED_WEAVIATE_VERSION = "1.27.7"; + public static final String EXPECTED_WEAVIATE_VERSION = "1.28.3"; // to be set according to weaviate docker image - public static final String EXPECTED_WEAVIATE_GIT_HASH = "8f0e033"; + public static final String EXPECTED_WEAVIATE_GIT_HASH = "f73fcee"; private WeaviateVersion() { } diff --git a/src/test/java/io/weaviate/integration/client/async/schema/ClientSchemaTest.java b/src/test/java/io/weaviate/integration/client/async/schema/ClientSchemaTest.java index 2c15d028e..6d2218548 100644 --- a/src/test/java/io/weaviate/integration/client/async/schema/ClientSchemaTest.java +++ b/src/test/java/io/weaviate/integration/client/async/schema/ClientSchemaTest.java @@ -468,7 +468,7 @@ public void testSchemaCreateClassWithInvalidTokenizationProperty() throws Execut //then assertResultTrue(createStatus); - assertResultError("tokenization in body should be one of [word lowercase whitespace field trigram gse kagome_kr]", notExistingTokenizationCreateStatus); + assertResultError("tokenization in body should be one of [word lowercase whitespace field trigram gse kagome_kr kagome_ja]", notExistingTokenizationCreateStatus); assertResultError("Tokenization is not allowed for data type 'int'", notSupportedTokenizationForIntCreateStatus); } } diff --git a/src/test/java/io/weaviate/integration/client/rbac/ClientRbacTest.java b/src/test/java/io/weaviate/integration/client/rbac/ClientRbacTest.java new file mode 100644 index 000000000..9ae2e8999 --- /dev/null +++ b/src/test/java/io/weaviate/integration/client/rbac/ClientRbacTest.java @@ -0,0 +1,249 @@ +package io.weaviate.integration.client.rbac; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.util.List; + +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.rules.TestName; + +import io.weaviate.client.Config; +import io.weaviate.client.WeaviateAuthClient; +import io.weaviate.client.base.Result; +import io.weaviate.client.v1.auth.exception.AuthException; +import io.weaviate.client.v1.rbac.Roles; +import io.weaviate.client.v1.rbac.model.BackupsPermission; +import io.weaviate.client.v1.rbac.model.ClusterPermission; +import io.weaviate.client.v1.rbac.model.CollectionsPermission; +import io.weaviate.client.v1.rbac.model.DataPermission; +import io.weaviate.client.v1.rbac.model.NodesPermission; +import io.weaviate.client.v1.rbac.model.NodesPermission.Verbosity; +import io.weaviate.client.v1.rbac.model.Permission; +import io.weaviate.client.v1.rbac.model.Role; +import io.weaviate.client.v1.rbac.model.RolesPermission; +import io.weaviate.client.v1.rbac.model.TenantsPermission; +import io.weaviate.integration.client.WeaviateDockerCompose; +import io.weaviate.integration.client.WeaviateDockerCompose.Weaviate; + +public class ClientRbacTest { + private static final String adminRole = "admin"; + private static final String viewerRole = "viewer"; + private static final String adminUser = "john-doe"; + + private Roles roles; + + @Rule + public TestName currentTest = new TestName(); + + @ClassRule + public static WeaviateDockerCompose compose = WeaviateDockerCompose.rbac(adminUser); + + @Before + public void before() throws AuthException { + Config config = new Config("http", compose.getHttpHostAddress()); + roles = WeaviateAuthClient.apiKey(config, Weaviate.makeSecret(adminUser)).roles(); + } + + public static Object[][] rolesToCreate() { + return new Object[][] { + }; + } + + /** + * By default the admin user which we use to run the tests + * will have 'admin' and 'viewer' roles. + */ + @Test + public void testGetAll() { + Result> response = roles.allGetter().run(); + List all = response.getResult(); + + assertThat(response.getError()).as("get all roles error").isNull(); + assertThat(all).hasSize(2).as("wrong number of roles"); + assertThat(all.get(0)).returns(adminRole, Role::getName); + assertThat(all.get(1)).returns(viewerRole, Role::getName); + } + + @Test + public void testGetUserRoles() { + Result> responseCurrent = roles.userRolesGetter().run(); + assertThat(responseCurrent.getError()).as("get roles for current user error").isNull(); + Result> responseAdminUser = roles.userRolesGetter().withUser(adminUser).run(); + assertThat(responseAdminUser.getError()).as("get roles for user error").isNull(); + + List currentRoles = responseCurrent.getResult(); + List adminRoles = responseAdminUser.getResult(); + + Assertions.assertArrayEquals(currentRoles.toArray(), adminRoles.toArray(), "expect same set of roles"); + } + + public void testGetAssignedUsers() { + Result> response = roles.assignedUsersGetter().withRole(adminRole).run(); + assertThat(response.getError()).as("get assigned users error").isNull(); + + List users = response.getResult(); + assertThat(users).as("users assigned to " + adminRole + " role").hasSize(1); + assertEquals(adminUser, users.get(0), "wrong user assinged to " + adminRole + " role"); + } + + // TODO: check if I can create a role with a name that's not a valid URL + // paramter + + @Test + public void testCreate() { + String myRole = roleName("VectorOwner"); + String myCollection = "Pizza"; + + Permission[] wantPermissions = new Permission[] { + Permission.backups(BackupsPermission.Action.MANAGE, myCollection), + Permission.cluster(ClusterPermission.Action.READ), + Permission.nodes(NodesPermission.Action.READ, Verbosity.MINIMAL, myCollection), + Permission.roles(RolesPermission.Action.MANAGE, viewerRole), + Permission.collections(CollectionsPermission.Action.CREATE, myCollection), + Permission.data(DataPermission.Action.UPDATE, myCollection), + Permission.tenants(TenantsPermission.Action.DELETE), + }; + + try { + // Arrange + deleteRole(myRole); + + // Act + createRole(myRole, wantPermissions); + + Result response = roles.getter().withName(myRole).run(); + Role role = response.getResult(); + assertNull("error fetching a role", response.getError()); + assertThat(role).as("wrong role name").returns(myRole, Role::getName); + + for (int i = 0; i < wantPermissions.length; i++) { + Permission perm = wantPermissions[i]; + assertTrue("should have permission " + perm, checkHasPermission(myRole, perm)); + } + } finally { + deleteRole(myRole); + } + } + + @Test + public void testAddPermissions() { + String myRole = roleName("VectorOwner"); + Permission toAdd = Permission.cluster(ClusterPermission.Action.READ); + try { + // Arrange + createRole(myRole, Permission.tenants(TenantsPermission.Action.DELETE)); + + // Act + Result response = roles.permissionAdder().withRole(myRole) + .withPermissions(toAdd) + .run(); + assertNull("add-permissions operation error", response.getError()); + + // Assert + assertTrue("should have permission " + toAdd, checkHasPermission(myRole, toAdd)); + } finally { + deleteRole(myRole); + } + } + + @Test + public void testRemovePermissions() { + String myRole = roleName("VectorOwner"); + Permission toRemove = Permission.tenants(TenantsPermission.Action.DELETE); + try { + // Arrange + createRole(myRole, + Permission.cluster(ClusterPermission.Action.READ), + Permission.tenants(TenantsPermission.Action.DELETE)); + + // Act + Result response = roles.permissionRemover().withRole(myRole) + .withPermissions(toRemove) + .run(); + assertNull("remove-permissions operation error", response.getError()); + + // Assert + assertFalse("should not have permission " + toRemove, checkHasPermission(myRole, toRemove)); + } finally { + deleteRole(myRole); + } + } + + @Test + public void testRevokeRole() { + String myRole = roleName("VectorOwner"); + try { + // Arrange + createRole(myRole, Permission.tenants(TenantsPermission.Action.DELETE)); + roles.assigner().withUser(adminUser).witRoles(myRole).run(); + assumeTrue(checkHasRole(adminUser, myRole), adminUser + " should have the assigned role"); + + // Act + Result response = roles.revoker().withUser(adminUser).witRoles(myRole).run(); + assertNull("revoke operation error", response.getError()); + + // Assert + assertFalse("should not have " + myRole + "role", checkHasRole(adminUser, myRole)); + } finally { + deleteRole(myRole); + } + } + + @Test + public void testAssignRole() { + String myRole = roleName("VectorOwner"); + try { + // Arrange + createRole(myRole, Permission.tenants(TenantsPermission.Action.DELETE)); + assumeFalse(checkHasRole(adminUser, myRole), adminUser + " should not have the new role"); + + // Act + Result response = roles.assigner().withUser(adminUser).witRoles(myRole).run(); + assertNull("assign operation error", response.getError()); + + // Assert + assertTrue("should have " + myRole + "role", checkHasRole(adminUser, myRole)); + } finally { + deleteRole(myRole); + } + } + + /** Prefix the role with the name of the current test for easier debugging */ + private String roleName(String name) { + return String.format("%s-%s", currentTest.getMethodName(), name); + } + + private boolean checkHasPermission(String role, Permission> perm) { + return roles.permissionChecker().withRole(role).withPermission(perm).run().getResult(); + } + + private boolean checkRoleExists(String role) { + return roles.exists().withName(role).run().getResult(); + } + + private boolean checkHasRole(String user, String role) { + return roles.assignedUsersGetter().withRole(role).run().getResult().contains(user); + } + + private void createRole(String role, Permission... permissions) { + roles.creator().withName(role).withPermissions(permissions).run(); + assumeTrue(checkRoleExists(role), "role should exist after creation"); + + } + + private void deleteRole(String role) { + roles.deleter().withName(role).run(); + assertFalse("role should not exist after deletion", checkRoleExists(role)); + + } +} diff --git a/src/test/java/io/weaviate/integration/client/schema/ClientSchemaTest.java b/src/test/java/io/weaviate/integration/client/schema/ClientSchemaTest.java index 8506578ba..3ed6b8553 100644 --- a/src/test/java/io/weaviate/integration/client/schema/ClientSchemaTest.java +++ b/src/test/java/io/weaviate/integration/client/schema/ClientSchemaTest.java @@ -361,7 +361,7 @@ public void testSchemaCreateClassWithInvalidTokenizationProperty() { //then assertResultTrue(createStatus); - assertResultError("tokenization in body should be one of [word lowercase whitespace field trigram gse kagome_kr]", notExistingTokenizationCreateStatus); + assertResultError("tokenization in body should be one of [word lowercase whitespace field trigram gse kagome_kr kagome_ja]", notExistingTokenizationCreateStatus); assertResultError("Tokenization is not allowed for data type 'int'", notSupportedTokenizationForIntCreateStatus); }