Skip to content
59 changes: 57 additions & 2 deletions src/it/java/io/weaviate/containers/Weaviate.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
import io.weaviate.client6.v1.internal.ObjectBuilder;

public class Weaviate extends WeaviateContainer {
public static final String VERSION = "1.32.3";
public static final String VERSION = "1.33.0";
public static final String DOCKER_IMAGE = "semitechnologies/weaviate";
public static String OIDC_ISSUER = "https://auth.wcs.api.weaviate.io/auth/realms/SeMI";

private volatile SharedClient clientInstance;

Expand All @@ -31,6 +32,12 @@ public WeaviateClient getClient() {
* The lifetime of this client is tied to that of its container, which means
* that you do not need to {@code close} it manually. It will only truly close
* after the parent Testcontainer is stopped.
*
* FIXME: we cannot return the same client for 2 different sets of
* configurations.
* What we should do is: {@link #getClient()} returns the shared client, while
* this one always constructs a new instance.
* Otherwise we'll get a race condition once the tests are parallelized.
*/
public WeaviateClient getClient(Function<Config.Custom, ObjectBuilder<Config>> fn) {
if (!isRunning()) {
Expand All @@ -51,6 +58,8 @@ public WeaviateClient getClient(Function<Config.Custom, ObjectBuilder<Config>> f
.httpPort(getMappedPort(8080))
.grpcPort(getMappedPort(50051)));
var config = customFn.apply(new Config.Custom()).build();
if (config.authentication() != null) {
}
try {
clientInstance = new SharedClient(config, this);
} catch (Exception e) {
Expand Down Expand Up @@ -92,7 +101,8 @@ public static Weaviate.Builder custom() {
public static class Builder {
private String versionTag;
private Set<String> enableModules = new HashSet<>();

private Set<String> adminUsers = new HashSet<>();
private Set<String> viewerUsers = new HashSet<>();
private Map<String, String> environment = new HashMap<>();

public Builder() {
Expand Down Expand Up @@ -137,6 +147,37 @@ public Builder withOffloadS3(String accessKey, String secretKey) {
return this;
}

public Builder withAdminUsers(String... admins) {
adminUsers.addAll(Arrays.asList(admins));
return this;
}

public Builder withViewerUsers(String... viewers) {
viewerUsers.addAll(Arrays.asList(viewers));
return this;
}

/** Enable RBAC authorization for this container. */
public Builder withRbac() {
environment.put("AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED", "false");
environment.put("AUTHENTICATION_APIKEY_ENABLED", "true");
environment.put("AUTHORIZATION_RBAC_ENABLED", "true");
environment.put("AUTHENTICATION_DB_USERS_ENABLED", "true");
return this;
}

/**
* Enable API-Key authentication for this container.
*
* @param apiKeys Allowed API keys.
*/
public Builder withApiKeys(String... apiKeys) {
environment.put("AUTHENTICATION_APIKEY_ENABLED", "true");
environment.put("AUTHENTICATION_APIKEY_ALLOWED_KEYS", String.join(",",
apiKeys));
return this;
}

public Builder enableTelemetry(boolean enable) {
environment.put("DISABLE_TELEMETRY", Boolean.toString(!enable));
return this;
Expand Down Expand Up @@ -170,6 +211,20 @@ public Weaviate build() {
c.withEnv("ENABLE_MODULES", String.join(",", enableModules));
}

var apiKeyUsers = new HashSet<String>();
apiKeyUsers.addAll(adminUsers);
apiKeyUsers.addAll(viewerUsers);

if (!adminUsers.isEmpty()) {
environment.put("AUTHORIZATION_ADMIN_USERS", String.join(",", adminUsers));
}
if (!viewerUsers.isEmpty()) {
environment.put("AUTHORIZATION_VIEWER_USERS", String.join(",", viewerUsers));
}
if (!apiKeyUsers.isEmpty()) {
environment.put("AUTHENTICATION_APIKEY_USERS", String.join(",", apiKeyUsers));
}

environment.forEach((name, value) -> c.withEnv(name, value));
c.withCreateContainerCmdModifier(cmd -> cmd.withHostName("weaviate"));
return c;
Expand Down
145 changes: 145 additions & 0 deletions src/it/java/io/weaviate/integration/RbacITest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package io.weaviate.integration;

import java.io.IOException;
import java.util.Arrays;

import org.assertj.core.api.Assertions;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.Test;

import io.weaviate.ConcurrentTest;
import io.weaviate.client6.v1.api.Authentication;
import io.weaviate.client6.v1.api.WeaviateClient;
import io.weaviate.client6.v1.api.rbac.AliasPermission;
import io.weaviate.client6.v1.api.rbac.BackupsPermission;
import io.weaviate.client6.v1.api.rbac.ClusterPermission;
import io.weaviate.client6.v1.api.rbac.CollectionsPermission;
import io.weaviate.client6.v1.api.rbac.DataPermission;
import io.weaviate.client6.v1.api.rbac.GroupsPermission;
import io.weaviate.client6.v1.api.rbac.NodesPermission;
import io.weaviate.client6.v1.api.rbac.Permission;
import io.weaviate.client6.v1.api.rbac.ReplicatePermission;
import io.weaviate.client6.v1.api.rbac.Role;
import io.weaviate.client6.v1.api.rbac.RolesPermission;
import io.weaviate.client6.v1.api.rbac.RolesPermission.Scope;
import io.weaviate.client6.v1.api.rbac.TenantsPermission;
import io.weaviate.client6.v1.api.rbac.UsersPermission;
import io.weaviate.client6.v1.api.rbac.roles.UserAssignment;
import io.weaviate.client6.v1.api.rbac.users.UserType;
import io.weaviate.containers.Weaviate;

public class RbacITest extends ConcurrentTest {
private static final String ADMIN_USER = "admin-alex";
private static final String API_KEY = "admin-alex-secret";

/** Name of the root role, which exists by default. */
private static final String ROOT_ROLE = "root";

/** Name of the admin role, which exists by default. */
private static final String ADMIN_ROLE = "admin";

/** Name of the viewer role, which exists by default. */
private static final String VIEWER_ROLE = "viewer";

private static final WeaviateClient client = Weaviate.custom()
.withAdminUsers(ADMIN_USER)
.withApiKeys(API_KEY)
.withRbac()
.withOIDC( // Enable OIDC to have Weaviate return different user types (db, db_env, oidc)
"wcs",
"https://auth.wcs.api.weaviate.io/auth/realms/SeMI",
"email",
"groups")
.build()
.getClient(fn -> fn.authentication(Authentication.apiKey(API_KEY)));

@Test
public void testLifecycle() throws IOException {
// Arrange
var myCollection = "Things";
var nsRole = ns("VectorOwner");

Permission[] permissions = new Permission[] {
Permission.alias("ThingsAlias", myCollection, AliasPermission.Action.CREATE),
Permission.backups(myCollection, BackupsPermission.Action.MANAGE),
Permission.cluster(ClusterPermission.Action.READ),
Permission.nodes(myCollection, NodesPermission.Action.READ),
Permission.roles(VIEWER_ROLE, Scope.MATCH, RolesPermission.Action.CREATE),
Permission.collections(myCollection, CollectionsPermission.Action.CREATE),
Permission.data(myCollection, DataPermission.Action.UPDATE),
Permission.groups("my-group", "oidc", GroupsPermission.Action.READ),
Permission.tenants(myCollection, "my-tenant", TenantsPermission.Action.DELETE),
Permission.users("my-user", UsersPermission.Action.READ),
Permission.replicate(myCollection, "my-shard", ReplicatePermission.Action.READ),
};

// Act: create role
client.roles.create(nsRole, permissions);

var role = client.roles.get(nsRole);
Assertions.assertThat(role)
.as("created role")
.returns(nsRole, Role::name)
.extracting(Role::permissions, InstanceOfAssertFactories.list(Permission.class))
.containsAll(Arrays.asList(permissions));

// Act:: add extra permissions
var extra = new Permission[] {
Permission.data("Songs", DataPermission.Action.DELETE),
Permission.users("john-doe", UsersPermission.Action.ASSIGN_AND_REVOKE),
};
client.roles.addPermissions(nsRole, extra);

Assertions.assertThat(client.roles.hasPermission(nsRole, extra[0]))
.as("has extra data permission")
.isTrue();

Assertions.assertThat(client.roles.hasPermission(nsRole, extra[1]))
.as("has extra users permission")
.isTrue();

// Act: remove extra permissions
client.roles.removePermissions(nsRole, extra);

Assertions.assertThat(client.roles.hasPermission(nsRole, extra[0]))
.as("extra data permission removed")
.isFalse();

Assertions.assertThat(client.roles.hasPermission(nsRole, extra[1]))
.as("extra users permission removed")
.isFalse();

// Act: delete role
client.roles.delete(nsRole);
Assertions.assertThat(client.roles.exists(nsRole))
.as("role is deleted")
.isFalse();
}

@Test
public void test_list() throws IOException {
Assertions.assertThat(client.roles.list())
.extracting(Role::name)
.contains(ROOT_ROLE, ADMIN_ROLE, VIEWER_ROLE);
}

@Test
public void test_assignedUsers() throws IOException {
Assertions.assertThat(client.roles.assignedUserIds(ROOT_ROLE))
.hasSize(1)
.containsOnly(ADMIN_USER);
}

@Test
public void test_userAssignments() throws IOException {
var assignments = client.roles.userAssignments(ROOT_ROLE);
Assertions.assertThat(assignments)
.hasSize(2)
.extracting(UserAssignment::userId)
.containsOnly(ADMIN_USER);

Assertions.assertThat(assignments)
.extracting(UserAssignment::userType)
.containsOnly(UserType.DB_ENV_USER, UserType.OIDC);
}
}
7 changes: 7 additions & 0 deletions src/main/java/io/weaviate/client6/v1/api/WeaviateClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import io.weaviate.client6.v1.api.alias.WeaviateAliasClient;
import io.weaviate.client6.v1.api.collections.WeaviateCollectionsClient;
import io.weaviate.client6.v1.api.rbac.roles.WeaviateRolesClient;
import io.weaviate.client6.v1.internal.ObjectBuilder;
import io.weaviate.client6.v1.internal.TokenProvider;
import io.weaviate.client6.v1.internal.grpc.DefaultGrpcTransport;
Expand All @@ -31,6 +32,11 @@ public class WeaviateClient implements AutoCloseable {
/** Client for {@code /aliases} endpoints for managing collection aliases. */
public final WeaviateAliasClient alias;

/**
* Client for {@code /authz/roles} endpoints for managing collection aliases.
*/
public final WeaviateRolesClient roles;

public WeaviateClient(Config config) {
RestTransportOptions restOpt;
GrpcChannelOptions grpcOpt;
Expand Down Expand Up @@ -82,6 +88,7 @@ public WeaviateClient(Config config) {
this.grpcTransport = new DefaultGrpcTransport(grpcOpt);
this.alias = new WeaviateAliasClient(restTransport);
this.collections = new WeaviateCollectionsClient(restTransport, grpcTransport);
this.roles = new WeaviateRolesClient(restTransport);
this.config = config;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ public String toString() {
.map(v -> {
var name = v.getKey();
var value = v.getValue();
var array = (value instanceof float[] f)
var array = (value instanceof float[])
? Arrays.toString((float[]) value)
: Arrays.deepToString((float[][]) value);
return "%s=%s".formatted(name, array);
Expand Down
48 changes: 48 additions & 0 deletions src/main/java/io/weaviate/client6/v1/api/rbac/AliasPermission.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.weaviate.client6.v1.api.rbac;

import java.util.Arrays;
import java.util.List;

import com.google.gson.annotations.SerializedName;

public record AliasPermission(
@SerializedName("alias") String alias,
@SerializedName("collection") String collection,
@SerializedName("actions") List<Action> actions) implements Permission {

public AliasPermission(String alias, String collection, Action... actions) {
this(alias, collection, Arrays.asList(actions));
}

@Override
public Permission.Kind _kind() {
return Permission.Kind.ALIASES;
}

@Override
public Object self() {
return this;
}

public enum Action implements RbacAction<Action> {
@SerializedName("create_aliases")
CREATE("create_aliases"),
@SerializedName("read_aliases")
READ("read_aliases"),
@SerializedName("update_aliases")
UPDATE("update_aliases"),
@SerializedName("delete_aliases")
DELETE("delete_aliases");

private final String jsonValue;

private Action(String jsonValue) {
this.jsonValue = jsonValue;
}

@Override
public String jsonValue() {
return jsonValue;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.weaviate.client6.v1.api.rbac;

import java.util.Arrays;
import java.util.List;

import com.google.gson.annotations.SerializedName;

public record BackupsPermission(
@SerializedName("collection") String collection,
@SerializedName("actions") List<Action> actions) implements Permission {

public BackupsPermission(String collection, Action... actions) {
this(collection, Arrays.asList(actions));
}

@Override
public Permission.Kind _kind() {
return Permission.Kind.BACKUPS;
}

@Override
public Object self() {
return this;
}

public enum Action implements RbacAction<Action> {
@SerializedName("manage_backups")
MANAGE("manage_backups");

private final String jsonValue;

private Action(String jsonValue) {
this.jsonValue = jsonValue;
}

@Override
public String jsonValue() {
return jsonValue;
}
}
}
Loading