diff --git a/README.md b/README.md index 1ba9b291..247bbb8c 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ applicationDefaultJvmArgs += listOf( ## Supported APIs + ### Tucked Builder Tucked Builder is an iteration of the Builder pattern that reduces boilerplate and leverages static typing and autocompletion to help API discovery. @@ -193,6 +194,7 @@ WeaviateClient wcd = WeaviateClient.connectToWeaviateCloud("my-cluster-url.io", > ``` > WeaviateClient will be automatically closed when execution exits the block. + #### Authentication Weaviate supports several authentication methods: @@ -214,6 +216,7 @@ WeaviateClient.connectToCustom( Follow the [documentation](https://docs.weaviate.io/deploy/configuration/authentication) for a detailed discussion. + ### Collection management ```java @@ -249,6 +252,7 @@ Other methods in `collections` namespace include: - `list()` to fetch collection configurations for all existing collections - `deleteAll()` to drop all collections and their data + #### Using a Collection Handle Once a collection is created, you can obtain another client object that's scoped to that collection, called a _"handle"_. @@ -274,6 +278,7 @@ Thread.run(() -> popSongs.forEach(song -> songs.data.insert(song))); For the rest of the document, assume `songs` is handle for the "Songs" collection defined elsewhere. + #### Generic `PropertiesT` Weaviate client lets you insert object properties in different "shapes". The compile-time type in which the properties must be passed is determined by a generic paramter in CollectionHandle object. @@ -283,10 +288,12 @@ In practice this means you'll be passing an instance of `Map` to If you prefer stricter typing, you can leverage our built-in ORM to work with properties as custom Java types. We will return to this in the **ORM** section later. Assume for now that properties are always being passed around as an "untyped" map. + ### Ingesting data Data operations are concentrated behind the `.data` namespace. + #### Insert single object ```java @@ -401,6 +408,7 @@ songs.query.nearImage("base64-encoded-image"); > [!TIP] > The first object returned in a NearObject query will _always_ be the search object itself. To filter it out, use the `.excludeSelf()` helper as in the example above. + #### Keyword and Hybrid search ```java @@ -481,6 +489,7 @@ Where.property("title").like("summer").not(); Passing `null` and and empty `Where[]` to any of the logical operators as well as to the `.where()` method is safe -- the empty operators will simply be ignored. + #### Grouping results Every query above has an overloaded variant that accepts a group-by clause. @@ -502,6 +511,7 @@ songs.query.bm25( The shape of the response object is different too, see [`QueryResponseGrouped`](./src/main/java/io/weaviate/client6/v1/api/collections/query/QueryResponseGrouped.java). + ### Pagination Paginating a Weaviate collection is straighforward and its API should is instantly familiar. `CursorSpliterator` powers 2 patterns for iterating over objects: @@ -700,6 +710,7 @@ System.out.println( Some of these features may be added in future releases. + ### Collection alias ```java @@ -710,6 +721,85 @@ client.collections.update("Songs_Alias", "PopSongs"); client.collections.delete("Songs_Alias"); ``` +### RBAC + +#### Roles + +The client supports all permission types existing as of `v1.33`. + +```java +import io.weaviate.client6.v1.api.rbac.Permission; + +client.roles.create( + "ManagerRole", + Permission.collections("Songs", CollectionsPermission.Action.READ, CollectionsPermission.Action.DELETE), + Permission.backups("Albums", BackupsPermission.Action.MANAGE) +); +assert !client.roles.hasPermission("ManagerRole", Permission.collections("Songs", CollectionsPermission.Action.UPDATE)); + +client.roles.create( + "ArtistRole", + Permission.collections("Songs", CollectionsPermission.Action.CREATE) +); + +client.roles.delete("PromoterRole"); +``` + +#### Users + +> [!NOTE] +> Not all modifications which can be done to _DB_ users (managed by Weaviate) are equally applicable to _OIDC_ users (managed by an external IdP). +> For this reason their APIs are separated into two distinct namespaces: `users.db` and `users.oidc`. + +```java +// DB users must be either defined in the server's environment configuration or created explicitly +if (!client.users.db.exists("ManagerUser")) { + client.users.db.create("ManagerUser"); +} + +client.users.db.assignRole("ManagerUser", "ManagerRole"); + + +// OIDC users originate from the IdP and do not need to be (and cannot) be created. +client.users.oidc.assignRole("DaveMustaine", "ArtistRole"); +client.users.oidc.assignRole("Tarkan", "ArtistRole"); + + +// There's a number of other actions you can take on a DB user: +Optional user = client.users.db.byName("ManagerUser"); +assert user.isPresent(); + +DbUser manager = user.get(); +if (!manager.active()) { + client.users.db.activate(manager.id()); +} + +String newApiKey = client.users.db.rotateKey(manager.id()); +client.users.db.deactivate(manager.id()); +client.users.db.delete(manager.id()); +``` + +You can get a brief information about the currently authenticated user: + +```java +User current = client.users.myUser(); +System.out.println(current.userType()); // Prints "DB_USER", "DB_ENV", or "OIDC". +``` + +#### Groups + +RBAC groups are created by assigning roles to a previously-inexisted groups and remove when no roles are longer assigned to a group. + +```java +client.groups.assignRoles("./friend-group", "BestFriendRole", "OldFriendRole"); + +assert client.groups.knownGroupNames().size() == 1; // "./friend-group" +assert client.groups.assignedRoles("./friend-group").size() == 2; + +client.groups.assignRoles("./friend-group", "BestFriendRole", "OldFriendRole"); +assert client.groups.knownGroupNames().isEmpty(); +``` + ## Useful resources - [Documentation](https://weaviate.io/developers/weaviate/current/client-libraries/java.html). diff --git a/src/it/java/io/weaviate/containers/Weaviate.java b/src/it/java/io/weaviate/containers/Weaviate.java index 88cd23ee..cc8a9604 100644 --- a/src/it/java/io/weaviate/containers/Weaviate.java +++ b/src/it/java/io/weaviate/containers/Weaviate.java @@ -15,15 +15,12 @@ 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; - public WeaviateClient getClient() { - return getClient(ObjectBuilder.identity()); - } - /** * Create a new instance of WeaviateClient connected to this container if none * exist. Get an existing client otherwise. @@ -32,7 +29,7 @@ public WeaviateClient getClient() { * that you do not need to {@code close} it manually. It will only truly close * after the parent Testcontainer is stopped. */ - public WeaviateClient getClient(Function> fn) { + public WeaviateClient getClient() { if (!isRunning()) { start(); } @@ -42,17 +39,8 @@ public WeaviateClient getClient(Function> f synchronized (this) { if (clientInstance == null) { - var host = getHost(); - var customFn = ObjectBuilder.partial(fn, - conn -> conn - .scheme("http") - .httpHost(host) - .grpcHost(host) - .httpPort(getMappedPort(8080)) - .grpcPort(getMappedPort(50051))); - var config = customFn.apply(new Config.Custom()).build(); try { - clientInstance = new SharedClient(config, this); + clientInstance = new SharedClient(Config.of(defaultConfigFn()), this); } catch (Exception e) { throw new RuntimeException("create WeaviateClient for Weaviate container", e); } @@ -66,19 +54,26 @@ public WeaviateClient getClient(Function> f * Prefer using {@link #getClient} unless your test needs the initialization * steps to run, e.g. OIDC authorization grant exchange. */ - public WeaviateClient getNewClient(Function> fn) { + public WeaviateClient getClient(Function> fn) { if (!isRunning()) { start(); } + + var customFn = ObjectBuilder.partial(fn, defaultConfigFn()); + var config = customFn.apply(new Config.Custom()).build(); + try { + return new WeaviateClient(config); + } catch (Exception e) { + throw new RuntimeException("create WeaviateClient for Weaviate container", e); + } + } + + private Function> defaultConfigFn() { var host = getHost(); - var customFn = ObjectBuilder.partial(fn, - conn -> conn - .scheme("http") - .httpHost(host) - .grpcHost(host) - .httpPort(getMappedPort(8080)) - .grpcPort(getMappedPort(50051))); - return WeaviateClient.connectToCustom(customFn); + return conn -> conn + .scheme("http") + .httpHost(host).httpPort(getMappedPort(8080)) + .grpcHost(host).grpcPort(getMappedPort(50051)); } public static Weaviate createDefault() { @@ -92,7 +87,8 @@ public static Weaviate.Builder custom() { public static class Builder { private String versionTag; private Set enableModules = new HashSet<>(); - + private Set adminUsers = new HashSet<>(); + private Set viewerUsers = new HashSet<>(); private Map environment = new HashMap<>(); public Builder() { @@ -137,6 +133,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; @@ -170,6 +197,20 @@ public Weaviate build() { c.withEnv("ENABLE_MODULES", String.join(",", enableModules)); } + var apiKeyUsers = new HashSet(); + 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; diff --git a/src/it/java/io/weaviate/integration/OIDCSupportITest.java b/src/it/java/io/weaviate/integration/OIDCSupportITest.java index 9b9a3669..eb3b86be 100644 --- a/src/it/java/io/weaviate/integration/OIDCSupportITest.java +++ b/src/it/java/io/weaviate/integration/OIDCSupportITest.java @@ -128,7 +128,7 @@ public void test_clientCredentials() throws Exception { /** Send an HTTP and gRPC requests using a "sync" client. */ private static void pingWeaviate(final Weaviate container, Authentication auth) throws Exception { - try (final var client = container.getNewClient(conn -> conn.authentication(auth))) { + try (final var client = container.getClient(conn -> conn.authentication(auth))) { // Make an authenticated HTTP call Assertions.assertThat(client.isLive()).isTrue(); @@ -143,7 +143,7 @@ private static void pingWeaviate(final Weaviate container, Authentication auth) /** Send an HTTP and gRPC requests using an "async" client. */ private static void pingWeaviateAsync(final Weaviate container, Authentication auth) throws Exception { - try (final var client = container.getNewClient(conn -> conn.authentication(auth))) { + try (final var client = container.getClient(conn -> conn.authentication(auth))) { try (final var async = client.async()) { // Make an authenticated HTTP call Assertions.assertThat(async.isLive().join()).isTrue(); diff --git a/src/it/java/io/weaviate/integration/RbacITest.java b/src/it/java/io/weaviate/integration/RbacITest.java new file mode 100644 index 00000000..7f3b90ff --- /dev/null +++ b/src/it/java/io/weaviate/integration/RbacITest.java @@ -0,0 +1,266 @@ +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.AliasesPermission; +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.groups.GroupType; +import io.weaviate.client6.v1.api.rbac.roles.UserAssignment; +import io.weaviate.client6.v1.api.rbac.users.DbUser; +import io.weaviate.client6.v1.api.rbac.users.User; +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 Weaviate container = 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(); + + private static final WeaviateClient client = container + .getClient(fn -> fn.authentication(Authentication.apiKey(API_KEY))); + + @Test + public void test_roles_Lifecycle() throws IOException { + // Arrange + var myCollection = "Things"; + var nsRole = ns("VectorOwner"); + + Permission[] permissions = new Permission[] { + Permission.aliases("ThingsAlias", myCollection, AliasesPermission.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", GroupType.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).get() + .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_roles_list() throws IOException { + Assertions.assertThat(client.roles.list()) + .extracting(Role::name) + .contains(ROOT_ROLE, ADMIN_ROLE, VIEWER_ROLE); + } + + @Test + public void test_roles_assignedUsers() throws IOException { + Assertions.assertThat(client.roles.assignedUserIds(ROOT_ROLE)) + .hasSize(1) + .containsOnly(ADMIN_USER); + } + + @Test + public void test_roles_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); + } + + @Test + public void test_groups() throws IOException { + var mediaGroup = "./media-group"; + var friendGroup = "./friend-group"; + + client.groups.assignRoles(mediaGroup, VIEWER_ROLE); + client.groups.assignRoles(friendGroup, ADMIN_ROLE, VIEWER_ROLE); + + Assertions.assertThat(client.groups.assignedRoles(friendGroup)) + .as("assigned to " + friendGroup) + .extracting(Role::name) + .containsOnly(ADMIN_ROLE, VIEWER_ROLE); + + Assertions.assertThat(client.groups.knownGroupNames()) + .as("known group names") + .contains(mediaGroup, friendGroup); + + client.groups.revokeRoles(mediaGroup, VIEWER_ROLE); + Assertions.assertThat(client.groups.knownGroupNames()) + .as("know group names (no root)") + .doesNotContain(mediaGroup); + } + + @Test + public void test_users_myUser() throws IOException { + var adminRoles = Assertions.assertThat(client.users.myUser()) + .returns(ADMIN_USER, User::id) + .extracting(User::roles, InstanceOfAssertFactories.list(Role.class)) + .extracting(Role::name) + .actual(); + + Assertions.assertThat(client.users.db.assignedRoles(ADMIN_USER)) + .extracting(Role::name) + .containsAll(adminRoles); + } + + @Test + public void test_users_db() throws IOException { + var userId = ns("user"); + var roleName = ns("rock-n-role"); + + var apiKey = client.users.db.create(userId); + assertValidApiKey(apiKey); + + client.roles.create(roleName); + + client.users.db.assignRoles(userId, roleName); + Assertions.assertThat(client.users.db.assignedRoles(userId)) + .as("role assigned") + .extracting(Role::name) + .contains(roleName); + + client.users.db.revokeRoles(userId, roleName); + Assertions.assertThat(client.users.db.assignedRoles(userId)) + .as("role revoked") + .extracting(Role::name) + .doesNotContain(roleName); + + client.users.db.activate(userId); + Assertions.assertThat(client.users.db.byName(userId)).get() + .as("user is activated") + .returns(true, DbUser::active); + + apiKey = client.users.db.rotateKey(userId); + assertValidApiKey(apiKey); + + client.users.db.deactivate(userId); + Assertions.assertThat(client.users.db.byName(userId)).get() + .as("user is deactivated") + .returns(false, DbUser::active); + + var all = client.users.db.list(users -> users.includeLastUsedAt(true)); + Assertions.assertThat(all) + .as("list users include lastUsedTime ") + .allMatch(user -> user.lastUsedAt() != null) + .extracting(DbUser::id) + .contains(userId, ADMIN_USER); + + client.users.db.delete(userId); + Assertions.assertThat(client.users.db.byName(userId)) + .as("user is deleted") + .isEmpty(); + } + + @Test + public void test_users_oidc() throws IOException { + var userId = ns("user"); + var roleName = ns("rock-n-role"); + + client.roles.create(roleName); + + client.users.oidc.assignRoles(userId, roleName); + Assertions.assertThat(client.users.oidc.assignedRoles(userId)) + .as("role assigned") + .extracting(Role::name) + .contains(roleName); + + client.users.oidc.revokeRoles(userId, roleName); + Assertions.assertThat(client.users.oidc.assignedRoles(userId)) + .as("role revoked") + .extracting(Role::name) + .doesNotContain(roleName); + } + + /** + * Create a new client with API-key authentication + * and check that it can make authenticated requests. + */ + private void assertValidApiKey(String apiKey) { + try (final var c = container.getClient(cfg -> cfg.authentication(Authentication.apiKey(apiKey)))) { + Assertions.assertThatCode(() -> c.isLive()).as("check API key is valid").doesNotThrowAnyException(); + } catch (Exception e) { + throw new AssertionError(e); + } + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/WeaviateClient.java b/src/main/java/io/weaviate/client6/v1/api/WeaviateClient.java index 0101dc12..910016ac 100644 --- a/src/main/java/io/weaviate/client6/v1/api/WeaviateClient.java +++ b/src/main/java/io/weaviate/client6/v1/api/WeaviateClient.java @@ -5,6 +5,9 @@ import io.weaviate.client6.v1.api.alias.WeaviateAliasClient; import io.weaviate.client6.v1.api.collections.WeaviateCollectionsClient; +import io.weaviate.client6.v1.api.rbac.groups.WeaviateGroupsClient; +import io.weaviate.client6.v1.api.rbac.roles.WeaviateRolesClient; +import io.weaviate.client6.v1.api.rbac.users.WeaviateUsersClient; import io.weaviate.client6.v1.internal.ObjectBuilder; import io.weaviate.client6.v1.internal.TokenProvider; import io.weaviate.client6.v1.internal.grpc.DefaultGrpcTransport; @@ -31,6 +34,21 @@ 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 RBAC roles. + */ + public final WeaviateRolesClient roles; + + /** + * Client for {@code /authz/groups} endpoints for managing RBAC groups. + */ + public final WeaviateGroupsClient groups; + + /** + * Client for {@code /users} endpoints for managing DB / OIDC users. + */ + public final WeaviateUsersClient users; + public WeaviateClient(Config config) { RestTransportOptions restOpt; GrpcChannelOptions grpcOpt; @@ -82,6 +100,9 @@ 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.groups = new WeaviateGroupsClient(restTransport); + this.users = new WeaviateUsersClient(restTransport); this.config = config; } diff --git a/src/main/java/io/weaviate/client6/v1/api/WeaviateClientAsync.java b/src/main/java/io/weaviate/client6/v1/api/WeaviateClientAsync.java index 7255fbc6..0783c574 100644 --- a/src/main/java/io/weaviate/client6/v1/api/WeaviateClientAsync.java +++ b/src/main/java/io/weaviate/client6/v1/api/WeaviateClientAsync.java @@ -7,6 +7,9 @@ import io.weaviate.client6.v1.api.alias.WeaviateAliasClientAsync; import io.weaviate.client6.v1.api.collections.WeaviateCollectionsClient; import io.weaviate.client6.v1.api.collections.WeaviateCollectionsClientAsync; +import io.weaviate.client6.v1.api.rbac.groups.WeaviateGroupsClientAsync; +import io.weaviate.client6.v1.api.rbac.roles.WeaviateRolesClientAsync; +import io.weaviate.client6.v1.api.rbac.users.WeaviateUsersClientAsync; import io.weaviate.client6.v1.internal.ObjectBuilder; import io.weaviate.client6.v1.internal.TokenProvider; import io.weaviate.client6.v1.internal.grpc.DefaultGrpcTransport; @@ -30,6 +33,21 @@ public class WeaviateClientAsync implements AutoCloseable { /** Client for {@code /aliases} endpoints for managing collection aliases. */ public final WeaviateAliasClientAsync alias; + /** + * Client for {@code /authz/roles} endpoints for managing RBAC roles. + */ + public final WeaviateRolesClientAsync roles; + + /** + * Client for {@code /authz/groups} endpoints for managing RBAC groups. + */ + public final WeaviateGroupsClientAsync groups; + + /** + * Client for {@code /users} endpoints for managing DB / OIDC users. + */ + public final WeaviateUsersClientAsync users; + /** * This constructor is blocking if {@link Authentication} configured, * as the client will need to do the initial token exchange. @@ -84,6 +102,9 @@ public WeaviateClientAsync(Config config) { this.restTransport = _restTransport; this.grpcTransport = new DefaultGrpcTransport(grpcOpt); this.alias = new WeaviateAliasClientAsync(restTransport); + this.roles = new WeaviateRolesClientAsync(restTransport); + this.groups = new WeaviateGroupsClientAsync(restTransport); + this.users = new WeaviateUsersClientAsync(restTransport); this.collections = new WeaviateCollectionsClientAsync(restTransport, grpcTransport); } diff --git a/src/main/java/io/weaviate/client6/v1/api/collections/Vectors.java b/src/main/java/io/weaviate/client6/v1/api/collections/Vectors.java index b89cf7fc..d255a319 100644 --- a/src/main/java/io/weaviate/client6/v1/api/collections/Vectors.java +++ b/src/main/java/io/weaviate/client6/v1/api/collections/Vectors.java @@ -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); diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/AliasesPermission.java b/src/main/java/io/weaviate/client6/v1/api/rbac/AliasesPermission.java new file mode 100644 index 00000000..1e5d03b0 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/AliasesPermission.java @@ -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 AliasesPermission( + @SerializedName("alias") String alias, + @SerializedName("collection") String collection, + @SerializedName("actions") List actions) implements Permission { + + public AliasesPermission(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 { + @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; + } + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/BackupsPermission.java b/src/main/java/io/weaviate/client6/v1/api/rbac/BackupsPermission.java new file mode 100644 index 00000000..b2cd93de --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/BackupsPermission.java @@ -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 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 { + @SerializedName("manage_backups") + MANAGE("manage_backups"); + + private final String jsonValue; + + private Action(String jsonValue) { + this.jsonValue = jsonValue; + } + + @Override + public String jsonValue() { + return jsonValue; + } + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/ClusterPermission.java b/src/main/java/io/weaviate/client6/v1/api/rbac/ClusterPermission.java new file mode 100644 index 00000000..5d6c3e20 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/ClusterPermission.java @@ -0,0 +1,40 @@ +package io.weaviate.client6.v1.api.rbac; + +import java.util.Arrays; +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +public record ClusterPermission( + @SerializedName("actions") List actions) implements Permission { + + public ClusterPermission(Action... actions) { + this(Arrays.asList(actions)); + } + + @Override + public Permission.Kind _kind() { + return Permission.Kind.CLUSTER; + } + + @Override + public Object self() { + return this; + } + + public enum Action implements RbacAction { + @SerializedName("read_cluster") + READ("read_cluster"); + + private final String jsonValue; + + private Action(String jsonValue) { + this.jsonValue = jsonValue; + } + + @Override + public String jsonValue() { + return jsonValue; + } + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/CollectionsPermission.java b/src/main/java/io/weaviate/client6/v1/api/rbac/CollectionsPermission.java new file mode 100644 index 00000000..f9d6a96d --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/CollectionsPermission.java @@ -0,0 +1,47 @@ +package io.weaviate.client6.v1.api.rbac; + +import java.util.Arrays; +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +public record CollectionsPermission( + @SerializedName("collection") String collection, + @SerializedName("actions") List actions) implements Permission { + + public CollectionsPermission(String collection, Action... actions) { + this(collection, Arrays.asList(actions)); + } + + @Override + public Permission.Kind _kind() { + return Permission.Kind.COLLECTIONS; + } + + @Override + public Object self() { + return this; + } + + public enum Action implements RbacAction { + @SerializedName("create_collections") + CREATE("create_collections"), + @SerializedName("read_collections") + READ("read_collections"), + @SerializedName("update_collections") + UPDATE("update_collections"), + @SerializedName("delete_collections") + DELETE("delete_collections"); + + private final String jsonValue; + + private Action(String jsonValue) { + this.jsonValue = jsonValue; + } + + @Override + public String jsonValue() { + return jsonValue; + } + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/DataPermission.java b/src/main/java/io/weaviate/client6/v1/api/rbac/DataPermission.java new file mode 100644 index 00000000..f6b0dfd9 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/DataPermission.java @@ -0,0 +1,60 @@ +package io.weaviate.client6.v1.api.rbac; + +import java.util.Arrays; +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +public record DataPermission( + @SerializedName("collection") String collection, + @SerializedName("actions") List actions) implements Permission { + + public DataPermission(String collection, Action... actions) { + this(collection, Arrays.asList(actions)); + } + + @Override + public Permission.Kind _kind() { + return Permission.Kind.DATA; + } + + @Override + public Object self() { + return this; + } + + public enum Action implements RbacAction { + @SerializedName("create_data") + CREATE("create_data"), + @SerializedName("read_data") + READ("read_data"), + @SerializedName("update_data") + UPDATE("update_data"), + @SerializedName("delete_data") + DELETE("delete_data"), + /* + * DO NOT CREATE NEW PERMISSIONS WITH THIS ACTION. + * It is preserved for backward compatibility with 1.28 + * and should only be used internally to read legacy permissions. + */ + @SerializedName("manage_data") + @Deprecated + MANAGE("manage_data") { + @Override + public boolean isDeprecated() { + return true; + }; + }; + + private final String jsonValue; + + private Action(String jsonValue) { + this.jsonValue = jsonValue; + } + + @Override + public String jsonValue() { + return jsonValue; + } + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/GroupsPermission.java b/src/main/java/io/weaviate/client6/v1/api/rbac/GroupsPermission.java new file mode 100644 index 00000000..182ff809 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/GroupsPermission.java @@ -0,0 +1,46 @@ +package io.weaviate.client6.v1.api.rbac; + +import java.util.Arrays; +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +import io.weaviate.client6.v1.api.rbac.groups.GroupType; + +public record GroupsPermission( + @SerializedName("group") String groupId, + @SerializedName("groupType") GroupType groupType, + @SerializedName("actions") List actions) implements Permission { + + public GroupsPermission(String groupId, GroupType groupType, Action... actions) { + this(groupId, groupType, Arrays.asList(actions)); + } + + @Override + public Permission.Kind _kind() { + return Permission.Kind.GROUPS; + } + + @Override + public Object self() { + return this; + } + + public enum Action implements RbacAction { + @SerializedName("read_groups") + READ("read_groups"), + @SerializedName("assign_and_revoke_groups") + ASSIGN_AND_REVOKE("assign_and_revoke_groups"); + + private final String jsonValue; + + private Action(String jsonValue) { + this.jsonValue = jsonValue; + } + + @Override + public String jsonValue() { + return jsonValue; + } + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/NodesPermission.java b/src/main/java/io/weaviate/client6/v1/api/rbac/NodesPermission.java new file mode 100644 index 00000000..1d3919c0 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/NodesPermission.java @@ -0,0 +1,49 @@ +package io.weaviate.client6.v1.api.rbac; + +import java.util.Arrays; +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +public record NodesPermission( + @SerializedName("collection") String collection, + @SerializedName("verbosity") Verbosity verbosity, + @SerializedName("actions") List actions) implements Permission { + + public NodesPermission(String collection, Verbosity verbosity, Action... actions) { + this(collection, verbosity, Arrays.asList(actions)); + } + + @Override + public Permission.Kind _kind() { + return Permission.Kind.NODES; + } + + @Override + public Object self() { + return this; + } + + public enum Action implements RbacAction { + @SerializedName("read_nodes") + READ("read_nodes"); + + private final String jsonValue; + + private Action(String jsonValue) { + this.jsonValue = jsonValue; + } + + @Override + public String jsonValue() { + return jsonValue; + } + } + + public enum Verbosity { + @SerializedName("minimal") + MINIMAL, + @SerializedName("verbose") + VERBOSE; + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/Permission.java b/src/main/java/io/weaviate/client6/v1/api/rbac/Permission.java new file mode 100644 index 00000000..0d088b85 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/Permission.java @@ -0,0 +1,302 @@ +package io.weaviate.client6.v1.api.rbac; + +import java.io.IOException; +import java.util.EnumMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.builder.HashCodeBuilder; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.internal.Streams; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import io.weaviate.client6.v1.api.rbac.NodesPermission.Verbosity; +import io.weaviate.client6.v1.api.rbac.RolesPermission.Scope; +import io.weaviate.client6.v1.api.rbac.groups.GroupType; +import io.weaviate.client6.v1.internal.json.JsonEnum; + +public interface Permission { + List> actions(); + + enum Kind implements JsonEnum { + ALIASES("aliases"), + BACKUPS("backups"), + COLLECTIONS("collections"), + DATA("data"), + GROUPS("groups"), + ROLES("roles"), + NODES("nodes"), + TENANTS("tenants"), + REPLICATE("replicate"), + USERS("users"), + + // Fake permission kinds: Weaviate does not use those. + CLUSTER("cluster"); + + private static final Map jsonValueMap = JsonEnum.collectNames(Kind.values()); + private final String jsonValue; + + private Kind(String jsonValue) { + this.jsonValue = jsonValue; + } + + @Override + public String jsonValue() { + return jsonValue; + } + + public static Kind valueOfJson(String jsonValue) { + return JsonEnum.valueOfJson(jsonValue, jsonValueMap, Kind.class); + } + } + + Permission.Kind _kind(); + + Object self(); + + /** + * Create {@link AliasesPermission} for an alias. + */ + public static AliasesPermission aliases(String alias, String collection, AliasesPermission.Action... actions) { + checkDeprecation(actions); + return new AliasesPermission(alias, collection, actions); + } + + /** + * Create {@link BackupsPermission} for a collection. + */ + public static BackupsPermission backups(String collection, BackupsPermission.Action... actions) { + checkDeprecation(actions); + return new BackupsPermission(collection, actions); + } + + /** + * Create {@link ClusterPermission} permission. + */ + public static ClusterPermission cluster(ClusterPermission.Action... actions) { + checkDeprecation(actions); + return new ClusterPermission(actions); + } + + /** + * Create permission for collection's configuration. + */ + public static CollectionsPermission collections(String collection, CollectionsPermission.Action... actions) { + checkDeprecation(actions); + return new CollectionsPermission(collection, actions); + } + + /** + * Create permissions for managing collection's data. + */ + public static DataPermission data(String collection, DataPermission.Action... actions) { + checkDeprecation(actions); + return new DataPermission(collection, actions); + } + + /** + * Create permissions for managing RBAC groups. + */ + public static GroupsPermission groups(String groupId, GroupType groupType, GroupsPermission.Action... actions) { + checkDeprecation(actions); + return new GroupsPermission(groupId, groupType, actions); + } + + /** + * Create {@link NodesPermission} scoped to all collections. + */ + public static NodesPermission nodes(NodesPermission.Verbosity verbosity, NodesPermission.Action... actions) { + checkDeprecation(actions); + return new NodesPermission("*", verbosity, actions); + } + + /** + * Create {@link NodesPermission} scoped to a specific collection. Verbosity is + * set to {@link Verbosity#VERBOSE} by default. + */ + public static NodesPermission nodes(String collection, NodesPermission.Action... actions) { + checkDeprecation(actions); + return new NodesPermission(collection, Verbosity.VERBOSE, actions); + } + + /** + * Create {@link RolesPermission} for multiple actions. + */ + public static RolesPermission roles(String roleName, Scope scope, RolesPermission.Action... actions) { + checkDeprecation(actions); + return new RolesPermission(roleName, scope, actions); + } + + /** + * Create {@link TenantsPermission} for a tenant. + */ + public static TenantsPermission tenants(String collection, String tenant, TenantsPermission.Action... actions) { + checkDeprecation(actions); + return new TenantsPermission(collection, tenant, actions); + } + + /** + * Create {@link UsersPermission}. + */ + public static UsersPermission users(String userId, UsersPermission.Action... actions) { + checkDeprecation(actions); + return new UsersPermission(userId, actions); + } + + /** + * Create {@link ReplicatePermission}. + * + *

+ * Example: + * {@code Permissions.replicate("Pizza", "shard-123", ReplicatePermission.Action.CREATE)} + */ + public static ReplicatePermission replicate(String collection, String shard, ReplicatePermission.Action... actions) { + checkDeprecation(actions); + return new ReplicatePermission(collection, shard, actions); + } + + private static void checkDeprecation(RbacAction... actions) throws IllegalArgumentException { + for (var action : actions) { + if (action.isDeprecated()) { + throw new IllegalArgumentException(action.jsonValue() + + " is hard-deprecated and should only be used to read legacy permissions created in v1.28"); + } + } + } + + @SuppressWarnings("unchecked") + static List merge(List permissions) { + record Key( + /** + * hash is computed on all permission fields apart from "actions" + * which is what we need to aggregate. + */ + int hash, + /** + * Permission types which do not have any filters differentiate by their class. + */ + Class cls) { + private Key(Object object) { + this(HashCodeBuilder.reflectionHashCode(object, "actions"), object.getClass()); + } + } + + var result = new LinkedHashMap(); // preserve insertion order + for (Permission perm : permissions) { + var key = new Key(perm); + var stored = result.putIfAbsent(key, perm); + if (stored != null) { // A permission for this key already exists, add all actions. + assert stored.actions() != null : "actions == null for " + stored.getClass(); + ((List>) stored.actions()).addAll(perm.actions()); + } + } + return result.values().stream().collect(Collectors.toList()); + } + + public static enum CustomTypeAdapterFactory implements TypeAdapterFactory { + INSTANCE; + + private static final EnumMap> readAdapters = new EnumMap<>( + Permission.Kind.class); + + private final void addAdapter(Gson gson, Permission.Kind kind, Class cls) { + readAdapters.put(kind, (TypeAdapter) gson.getDelegateAdapter(this, TypeToken.get(cls))); + } + + private final void init(Gson gson) { + addAdapter(gson, Permission.Kind.ALIASES, AliasesPermission.class); + addAdapter(gson, Permission.Kind.BACKUPS, BackupsPermission.class); + addAdapter(gson, Permission.Kind.COLLECTIONS, CollectionsPermission.class); + addAdapter(gson, Permission.Kind.DATA, DataPermission.class); + addAdapter(gson, Permission.Kind.GROUPS, GroupsPermission.class); + addAdapter(gson, Permission.Kind.ROLES, RolesPermission.class); + addAdapter(gson, Permission.Kind.NODES, NodesPermission.class); + addAdapter(gson, Permission.Kind.TENANTS, TenantsPermission.class); + addAdapter(gson, Permission.Kind.REPLICATE, ReplicatePermission.class); + addAdapter(gson, Permission.Kind.USERS, UsersPermission.class); + addAdapter(gson, Permission.Kind.CLUSTER, ClusterPermission.class); + } + + @SuppressWarnings("unchecked") + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + var rawType = type.getRawType(); + if (!Permission.class.isAssignableFrom(rawType)) { + return null; + } + + if (readAdapters.isEmpty()) { + init(gson); + } + + final var writeAdapter = gson.getDelegateAdapter(this, TypeToken.get(rawType)); + return (TypeAdapter) new TypeAdapter() { + + @Override + public void write(JsonWriter out, Permission value) throws IOException { + for (RbacAction action : value.actions()) { + out.beginObject(); + // User might not have provided many actions by mistake + out.name("action"); + out.value(action.jsonValue()); + + if (value.self() != null) { + var permission = writeAdapter.toJsonTree((T) value.self()); + permission.getAsJsonObject().remove("actions"); + + // Some permission types do not have a body + if (!permission.getAsJsonObject().keySet().isEmpty()) { + out.name(value._kind().jsonValue()); + Streams.write(permission, out); + } + } + out.endObject(); + } + } + + @Override + public Permission read(JsonReader in) throws IOException { + var jsonObject = JsonParser.parseReader(in).getAsJsonObject(); + + var actions = new JsonArray(1); + var permission = new JsonObject(); + + var action = jsonObject.remove("action"); + actions.add(action); + + Permission.Kind kind; + if (!jsonObject.keySet().isEmpty()) { + var kindString = jsonObject.keySet().iterator().next(); + kind = Permission.Kind.valueOfJson(kindString); + permission = jsonObject.get(kindString).getAsJsonObject(); + } else { + var actionString = action.getAsString(); + if (actionString.endsWith("_cluster")) { + kind = Permission.Kind.CLUSTER; + } else { + throw new IllegalArgumentException("unknown RBAC action " + actionString); + } + } + + var readAdapter = readAdapters.get(kind); + if (readAdapter == null) { + return null; + } + + permission.add("actions", actions); + return readAdapter.fromJsonTree(permission); + } + }.nullSafe(); + } + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/RbacAction.java b/src/main/java/io/weaviate/client6/v1/api/rbac/RbacAction.java new file mode 100644 index 00000000..1d647614 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/RbacAction.java @@ -0,0 +1,42 @@ +package io.weaviate.client6.v1.api.rbac; + +import io.weaviate.client6.v1.internal.json.JsonEnum; + +public interface RbacAction> extends JsonEnum { + + /** + * Returns true if the action is hard deprecated. + * + *

+ * Override default return for a deprecated enum value like so: + * + *

{@code
+   * OLD_ACTION("old_action") {
+   *  {@literal @Override}
+   *  public boolean isDeprecated() { return true; }
+   * };
+   * }
+ */ + default boolean isDeprecated() { + return false; + } + + static >> E fromString(Class enumClass, String value) + throws IllegalArgumentException { + for (E action : enumClass.getEnumConstants()) { + if (action.jsonValue().equals(value)) { + return action; + } + } + throw new IllegalArgumentException("No enum constant for value: " + value); + } + + static >> boolean isValid(Class enumClass, String value) { + for (var action : enumClass.getEnumConstants()) { + if (action.jsonValue().equals(value)) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/ReplicatePermission.java b/src/main/java/io/weaviate/client6/v1/api/rbac/ReplicatePermission.java new file mode 100644 index 00000000..aea5bd86 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/ReplicatePermission.java @@ -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 ReplicatePermission( + @SerializedName("collection") String collection, + @SerializedName("shard") String shard, + @SerializedName("actions") List actions) implements Permission { + + public ReplicatePermission(String collection, String shard, Action... actions) { + this(collection, shard, Arrays.asList(actions)); + } + + @Override + public Permission.Kind _kind() { + return Permission.Kind.REPLICATE; + } + + @Override + public Object self() { + return this; + } + + public enum Action implements RbacAction { + @SerializedName("create_replicate") + CREATE("create_replicate"), + @SerializedName("read_replicate") + READ("read_replicate"), + @SerializedName("update_replicate") + UPDATE("update_replicate"), + @SerializedName("delete_replicate") + DELETE("delete_replicate"); + + private final String jsonValue; + + private Action(String jsonValue) { + this.jsonValue = jsonValue; + } + + @Override + public String jsonValue() { + return jsonValue; + } + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/Role.java b/src/main/java/io/weaviate/client6/v1/api/rbac/Role.java new file mode 100644 index 00000000..3451e83b --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/Role.java @@ -0,0 +1,51 @@ +package io.weaviate.client6.v1.api.rbac; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +public record Role( + @SerializedName("name") String name, + @SerializedName("permissions") List permissions) { + + public Role(String name, Permission... permissions) { + this(name, Arrays.asList(permissions)); + } + + public static enum CustomTypeAdapterFactory implements TypeAdapterFactory { + INSTANCE; + + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + if (!Role.class.isAssignableFrom(type.getRawType())) { + return null; + } + var delegate = gson.getDelegateAdapter(this, type); + return new TypeAdapter() { + + @Override + public void write(JsonWriter out, T value) throws IOException { + delegate.write(out, value); + } + + @SuppressWarnings("unchecked") + @Override + public T read(JsonReader in) throws IOException { + var role = (Role) delegate.read(in); + if (role.permissions == null) { + return (T) role; + } + return (T) new Role(role.name(), Permission.merge(role.permissions)); + } + }; + } + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/RolesPermission.java b/src/main/java/io/weaviate/client6/v1/api/rbac/RolesPermission.java new file mode 100644 index 00000000..ffa779e1 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/RolesPermission.java @@ -0,0 +1,69 @@ +package io.weaviate.client6.v1.api.rbac; + +import java.util.Arrays; +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +public record RolesPermission( + @SerializedName("role") String role, + @SerializedName("scope") Scope scope, + @SerializedName("actions") List actions) implements Permission { + + public RolesPermission(String roleName, Scope scope, Action... actions) { + this(roleName, scope, Arrays.asList(actions)); + } + + @Override + public Permission.Kind _kind() { + return Permission.Kind.ROLES; + } + + @Override + public Object self() { + return this; + } + + public enum Action implements RbacAction { + @SerializedName("create_roles") + CREATE("create_roles"), + @SerializedName("read_roles") + READ("read_roles"), + @SerializedName("update_roles") + UPDATE("update_roles"), + @SerializedName("delete_roles") + DELETE("delete_roles"), + + /* + * DO NOT CREATE NEW PERMISSIONS WITH THIS ACTION. + * It is preserved for backward compatibility with 1.28 + * and should only be used internally to read legacy permissions. + */ + @SerializedName("manage_roles") + @Deprecated + MANAGE("manage_roles") { + @Override + public boolean isDeprecated() { + return true; + }; + }; + + private final String jsonValue; + + private Action(String jsonValue) { + this.jsonValue = jsonValue; + } + + @Override + public String jsonValue() { + return jsonValue; + } + } + + public enum Scope { + @SerializedName("all") + ALL, + @SerializedName("match") + MATCH; + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/TenantsPermission.java b/src/main/java/io/weaviate/client6/v1/api/rbac/TenantsPermission.java new file mode 100644 index 00000000..1c5c3b7d --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/TenantsPermission.java @@ -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 TenantsPermission( + @SerializedName("collection") String collection, + @SerializedName("tenant") String tenant, + @SerializedName("actions") List actions) implements Permission { + + public TenantsPermission(String collection, String tenant, Action... actions) { + this(collection, tenant, Arrays.asList(actions)); + } + + @Override + public Permission.Kind _kind() { + return Permission.Kind.TENANTS; + } + + @Override + public Object self() { + return this; + } + + public enum Action implements RbacAction { + @SerializedName("create_tenants") + CREATE("create_tenants"), + @SerializedName("read_tenants") + READ("read_tenants"), + @SerializedName("update_tenants") + UPDATE("update_tenants"), + @SerializedName("delete_tenants") + DELETE("delete_tenants"); + + private final String jsonValue; + + private Action(String jsonValue) { + this.jsonValue = jsonValue; + } + + @Override + public String jsonValue() { + return jsonValue; + } + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/UsersPermission.java b/src/main/java/io/weaviate/client6/v1/api/rbac/UsersPermission.java new file mode 100644 index 00000000..43376297 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/UsersPermission.java @@ -0,0 +1,49 @@ +package io.weaviate.client6.v1.api.rbac; + +import java.util.Arrays; +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +public record UsersPermission( + @SerializedName("users") String userId, + @SerializedName("actions") List actions) implements Permission { + + public UsersPermission(String userId, Action... actions) { + this(userId, Arrays.asList(actions)); + } + + @Override + public Permission.Kind _kind() { + return Permission.Kind.USERS; + } + + @Override + public Object self() { + return this; + } + + public enum Action implements RbacAction { + @SerializedName("create_users") + CREATE("create_users"), + @SerializedName("update_users") + UPDATE("update_users"), + @SerializedName("read_users") + READ("read_users"), + @SerializedName("delete_users") + DELETE("delete_users"), + @SerializedName("assign_and_revoke_users") + ASSIGN_AND_REVOKE("assign_and_revoke_users"); + + private final String jsonValue; + + private Action(String jsonValue) { + this.jsonValue = jsonValue; + } + + @Override + public String jsonValue() { + return jsonValue; + } + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/groups/AssignRolesRequest.java b/src/main/java/io/weaviate/client6/v1/api/rbac/groups/AssignRolesRequest.java new file mode 100644 index 00000000..0b04a311 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/groups/AssignRolesRequest.java @@ -0,0 +1,26 @@ +package io.weaviate.client6.v1.api.rbac.groups; + +import java.util.Collections; +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +import io.weaviate.client6.v1.internal.json.JSON; +import io.weaviate.client6.v1.internal.rest.Endpoint; +import io.weaviate.client6.v1.internal.rest.SimpleEndpoint; +import io.weaviate.client6.v1.internal.rest.UrlEncoder; + +public record AssignRolesRequest(String groupId, List roleNames) { + + public static final Endpoint _ENDPOINT = SimpleEndpoint.sideEffect( + __ -> "POST", + request -> "/authz/groups/" + UrlEncoder.encodeValue(request.groupId) + "/assign", + request -> Collections.emptyMap(), + request -> JSON.serialize(new Body(request.roleNames, GroupType.OIDC))); + + /** Request body should be {"roles": [...], "groupType": "oidc"} */ + private static record Body( + @SerializedName("roles") List roleNames, + @SerializedName("groupType") GroupType groupType) { + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/groups/GetAssignedRolesRequest.java b/src/main/java/io/weaviate/client6/v1/api/rbac/groups/GetAssignedRolesRequest.java new file mode 100644 index 00000000..8a6b7443 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/groups/GetAssignedRolesRequest.java @@ -0,0 +1,59 @@ +package io.weaviate.client6.v1.api.rbac.groups; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import com.google.gson.reflect.TypeToken; + +import io.weaviate.client6.v1.api.rbac.Role; +import io.weaviate.client6.v1.internal.ObjectBuilder; +import io.weaviate.client6.v1.internal.json.JSON; +import io.weaviate.client6.v1.internal.rest.Endpoint; +import io.weaviate.client6.v1.internal.rest.SimpleEndpoint; +import io.weaviate.client6.v1.internal.rest.UrlEncoder; + +public record GetAssignedRolesRequest(String groupId, Boolean includePermissions) { + + @SuppressWarnings("unchecked") + public static final Endpoint> _ENDPOINT = SimpleEndpoint.noBody( + __ -> "GET", + request -> "/authz/groups/" + UrlEncoder.encodeValue(request.groupId) + "/roles/oidc", + request -> request.includePermissions != null ? Map.of("includePermissions", request.includePermissions) + : Collections.emptyMap(), + (statusCode, + response) -> (List) JSON.deserialize(response, TypeToken.getParameterized(List.class, Role.class))); + + public static GetAssignedRolesRequest of(String groupId) { + return of(groupId, ObjectBuilder.identity()); + } + + public static GetAssignedRolesRequest of(String groupId, + Function> fn) { + return fn.apply(new Builder(groupId)).build(); + } + + public GetAssignedRolesRequest(Builder builder) { + this(builder.groupId, builder.includePermissions); + } + + public static class Builder implements ObjectBuilder { + private final String groupId; + private Boolean includePermissions; + + public Builder(String groupId) { + this.groupId = groupId; + } + + public Builder includePermissions(boolean includePermissions) { + this.includePermissions = includePermissions; + return this; + } + + @Override + public GetAssignedRolesRequest build() { + return new GetAssignedRolesRequest(this); + } + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/groups/GetKnownGroupNamesRequest.java b/src/main/java/io/weaviate/client6/v1/api/rbac/groups/GetKnownGroupNamesRequest.java new file mode 100644 index 00000000..deb1dc34 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/groups/GetKnownGroupNamesRequest.java @@ -0,0 +1,21 @@ +package io.weaviate.client6.v1.api.rbac.groups; + +import java.util.Collections; +import java.util.List; + +import com.google.gson.reflect.TypeToken; + +import io.weaviate.client6.v1.internal.json.JSON; +import io.weaviate.client6.v1.internal.rest.Endpoint; +import io.weaviate.client6.v1.internal.rest.SimpleEndpoint; + +public record GetKnownGroupNamesRequest() { + + @SuppressWarnings("unchecked") + public static final Endpoint> _ENDPOINT = SimpleEndpoint.noBody( + __ -> "GET", + request -> "/authz/groups/oidc", + request -> Collections.emptyMap(), + (statusCode, + response) -> (List) JSON.deserialize(response, TypeToken.getParameterized(List.class, String.class))); +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/groups/GroupType.java b/src/main/java/io/weaviate/client6/v1/api/rbac/groups/GroupType.java new file mode 100644 index 00000000..4adc41cb --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/groups/GroupType.java @@ -0,0 +1,8 @@ +package io.weaviate.client6.v1.api.rbac.groups; + +import com.google.gson.annotations.SerializedName; + +public enum GroupType { + @SerializedName("oidc") + OIDC; +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/groups/RevokeRolesRequest.java b/src/main/java/io/weaviate/client6/v1/api/rbac/groups/RevokeRolesRequest.java new file mode 100644 index 00000000..6b6486ff --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/groups/RevokeRolesRequest.java @@ -0,0 +1,26 @@ +package io.weaviate.client6.v1.api.rbac.groups; + +import java.util.Collections; +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +import io.weaviate.client6.v1.internal.json.JSON; +import io.weaviate.client6.v1.internal.rest.Endpoint; +import io.weaviate.client6.v1.internal.rest.SimpleEndpoint; +import io.weaviate.client6.v1.internal.rest.UrlEncoder; + +public record RevokeRolesRequest(String groupId, List roleNames) { + + public static final Endpoint _ENDPOINT = SimpleEndpoint.sideEffect( + __ -> "POST", + request -> "/authz/groups/" + UrlEncoder.encodeValue(request.groupId) + "/revoke", + request -> Collections.emptyMap(), + request -> JSON.serialize(new Body(request.roleNames, GroupType.OIDC))); + + /** Request body should be {"roles": [...], "groupType": "oidc"} */ + private static record Body( + @SerializedName("roles") List roleNames, + @SerializedName("groupType") GroupType groupType) { + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/groups/UserType.java b/src/main/java/io/weaviate/client6/v1/api/rbac/groups/UserType.java new file mode 100644 index 00000000..7a490415 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/groups/UserType.java @@ -0,0 +1,12 @@ +package io.weaviate.client6.v1.api.rbac.groups; + +import com.google.gson.annotations.SerializedName; + +public enum UserType { + @SerializedName("db_user") + DB_USER, + @SerializedName("db_end_user") + DB_ENV_USER, + @SerializedName("oidc") + OIDC +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/groups/WeaviateGroupsClient.java b/src/main/java/io/weaviate/client6/v1/api/rbac/groups/WeaviateGroupsClient.java new file mode 100644 index 00000000..0ea438af --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/groups/WeaviateGroupsClient.java @@ -0,0 +1,95 @@ +package io.weaviate.client6.v1.api.rbac.groups; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; + +import io.weaviate.client6.v1.api.WeaviateApiException; +import io.weaviate.client6.v1.api.rbac.Role; +import io.weaviate.client6.v1.internal.ObjectBuilder; +import io.weaviate.client6.v1.internal.rest.RestTransport; + +public class WeaviateGroupsClient { + private final RestTransport restTransport; + + public WeaviateGroupsClient(RestTransport restTransport) { + this.restTransport = restTransport; + } + + /** + * Get the roles assigned an OIDC group. + * + * @param groupId OIDC group ID. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public List assignedRoles(String groupId) throws IOException { + return this.restTransport.performRequest(GetAssignedRolesRequest.of(groupId), GetAssignedRolesRequest._ENDPOINT); + } + + /** + * Get the roles assigned an OIDC group. + * + * @param groupId OIDC group ID. + * @param fn Lambda expression for optional parameters. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public List assignedRoles(String groupId, + Function> fn) throws IOException { + return this.restTransport.performRequest(GetAssignedRolesRequest.of(groupId, fn), + GetAssignedRolesRequest._ENDPOINT); + } + + /** + * Get the names of known OIDC groups. + * + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public List knownGroupNames() throws IOException { + return this.restTransport.performRequest(null, GetKnownGroupNamesRequest._ENDPOINT); + } + + /** + * Assign roles to OIDC group. + * + * @param groupId OIDC group ID. + * @param roleNames Role names. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public void assignRoles(String groupId, String... roleNames) throws IOException { + this.restTransport.performRequest(new AssignRolesRequest(groupId, Arrays.asList(roleNames)), + AssignRolesRequest._ENDPOINT); + } + + /** + * Revoke roles from OIDC group. + * + * @param groupId OIDC group ID. + * @param roleNames Role names. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public void revokeRoles(String groupId, String... roleNames) throws IOException { + this.restTransport.performRequest(new RevokeRolesRequest(groupId, Arrays.asList(roleNames)), + RevokeRolesRequest._ENDPOINT); + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/groups/WeaviateGroupsClientAsync.java b/src/main/java/io/weaviate/client6/v1/api/rbac/groups/WeaviateGroupsClientAsync.java new file mode 100644 index 00000000..9932a910 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/groups/WeaviateGroupsClientAsync.java @@ -0,0 +1,67 @@ +package io.weaviate.client6.v1.api.rbac.groups; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +import io.weaviate.client6.v1.api.rbac.Role; +import io.weaviate.client6.v1.internal.ObjectBuilder; +import io.weaviate.client6.v1.internal.rest.RestTransport; + +public class WeaviateGroupsClientAsync { + private final RestTransport restTransport; + + public WeaviateGroupsClientAsync(RestTransport restTransport) { + this.restTransport = restTransport; + } + + /** + * Get the roles assigned an OIDC group. + * + * @param groupId OIDC group ID. + */ + public CompletableFuture> assignedRoles(String groupId) { + return this.restTransport.performRequestAsync(GetAssignedRolesRequest.of(groupId), + GetAssignedRolesRequest._ENDPOINT); + } + + /** + * Get the roles assigned an OIDC group. + * + * @param groupId OIDC group ID. + * @param fn Lambda expression for optional parameters. + */ + public CompletableFuture> assignedRoles(String groupId, + Function> fn) { + return this.restTransport.performRequestAsync(GetAssignedRolesRequest.of(groupId, fn), + GetAssignedRolesRequest._ENDPOINT); + } + + /** Get the names of known OIDC groups. */ + public CompletableFuture> knownGroupNames() { + return this.restTransport.performRequestAsync(null, GetKnownGroupNamesRequest._ENDPOINT); + } + + /** + * Assign roles to OIDC group. + * + * @param groupId OIDC group ID. + * @param roleNames Role names. + */ + public CompletableFuture assignRoles(String groupId, String... roleNames) { + return this.restTransport.performRequestAsync(new AssignRolesRequest(groupId, Arrays.asList(roleNames)), + AssignRolesRequest._ENDPOINT); + } + + /** + * Revoke roles from OIDC group. + * + * @param groupId OIDC group ID. + * @param roleNames Role names. + */ + public CompletableFuture revokeRoles(String groupId, String... roleNames) { + return this.restTransport.performRequestAsync(new RevokeRolesRequest(groupId, Arrays.asList(roleNames)), + RevokeRolesRequest._ENDPOINT); + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/roles/AddPermissionsRequest.java b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/AddPermissionsRequest.java new file mode 100644 index 00000000..8034181d --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/AddPermissionsRequest.java @@ -0,0 +1,24 @@ +package io.weaviate.client6.v1.api.rbac.roles; + +import java.util.Collections; +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +import io.weaviate.client6.v1.api.rbac.Permission; +import io.weaviate.client6.v1.internal.json.JSON; +import io.weaviate.client6.v1.internal.rest.Endpoint; +import io.weaviate.client6.v1.internal.rest.SimpleEndpoint; +import io.weaviate.client6.v1.internal.rest.UrlEncoder; + +public record AddPermissionsRequest(String roleName, List permissions) { + public static final Endpoint _ENDPOINT = SimpleEndpoint.sideEffect( + __ -> "POST", + request -> "/authz/roles/" + UrlEncoder.encodeValue(request.roleName) + "/add-permissions", + __ -> Collections.emptyMap(), + request -> JSON.serialize(new Body(request.permissions))); + + /** Request body must be {"permissions": [...]}. */ + private static record Body(@SerializedName("permissions") List permissions) { + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/roles/CreateRoleRequest.java b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/CreateRoleRequest.java new file mode 100644 index 00000000..93502fad --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/CreateRoleRequest.java @@ -0,0 +1,16 @@ +package io.weaviate.client6.v1.api.rbac.roles; + +import java.util.Collections; + +import io.weaviate.client6.v1.api.rbac.Role; +import io.weaviate.client6.v1.internal.json.JSON; +import io.weaviate.client6.v1.internal.rest.Endpoint; +import io.weaviate.client6.v1.internal.rest.SimpleEndpoint; + +public record CreateRoleRequest(Role role) { + public static final Endpoint _ENDPOINT = SimpleEndpoint.sideEffect( + __ -> "POST", + __ -> "/authz/roles", + __ -> Collections.emptyMap(), + request -> JSON.serialize(request.role)); +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/roles/DeleteRoleRequest.java b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/DeleteRoleRequest.java new file mode 100644 index 00000000..a966c582 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/DeleteRoleRequest.java @@ -0,0 +1,14 @@ +package io.weaviate.client6.v1.api.rbac.roles; + +import java.util.Collections; + +import io.weaviate.client6.v1.internal.rest.Endpoint; +import io.weaviate.client6.v1.internal.rest.SimpleEndpoint; +import io.weaviate.client6.v1.internal.rest.UrlEncoder; + +public record DeleteRoleRequest(String roleName) { + public static final Endpoint _ENDPOINT = SimpleEndpoint.sideEffect( + __ -> "DELETE", + request -> "/authz/roles/" + UrlEncoder.encodeValue(request.roleName), + __ -> Collections.emptyMap()); +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/roles/GetAssignedUsersRequest.java b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/GetAssignedUsersRequest.java new file mode 100644 index 00000000..80e889a1 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/GetAssignedUsersRequest.java @@ -0,0 +1,21 @@ +package io.weaviate.client6.v1.api.rbac.roles; + +import java.util.Collections; +import java.util.List; + +import com.google.gson.reflect.TypeToken; + +import io.weaviate.client6.v1.internal.json.JSON; +import io.weaviate.client6.v1.internal.rest.Endpoint; +import io.weaviate.client6.v1.internal.rest.SimpleEndpoint; +import io.weaviate.client6.v1.internal.rest.UrlEncoder; + +public record GetAssignedUsersRequest(String roleName) { + @SuppressWarnings("unchecked") + public static final Endpoint> _ENDPOINT = SimpleEndpoint.noBody( + __ -> "GET", + request -> "/authz/roles/" + UrlEncoder.encodeValue(request.roleName) + "/users", + __ -> Collections.emptyMap(), + (statusCode, response) -> (List) JSON.deserialize(response, + TypeToken.getParameterized(List.class, String.class))); +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/roles/GetGroupAssignementsRequest.java b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/GetGroupAssignementsRequest.java new file mode 100644 index 00000000..71f1ffee --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/GetGroupAssignementsRequest.java @@ -0,0 +1,21 @@ +package io.weaviate.client6.v1.api.rbac.roles; + +import java.util.Collections; +import java.util.List; + +import com.google.gson.reflect.TypeToken; + +import io.weaviate.client6.v1.internal.json.JSON; +import io.weaviate.client6.v1.internal.rest.Endpoint; +import io.weaviate.client6.v1.internal.rest.SimpleEndpoint; +import io.weaviate.client6.v1.internal.rest.UrlEncoder; + +public record GetGroupAssignementsRequest(String roleName) { + @SuppressWarnings("unchecked") + public static final Endpoint> _ENDPOINT = SimpleEndpoint.noBody( + __ -> "GET", + request -> "/authz/roles/" + UrlEncoder.encodeValue(request.roleName) + "/group-assignments", + __ -> Collections.emptyMap(), + (statusCode, response) -> (List) JSON.deserialize(response, + TypeToken.getParameterized(List.class, GroupAssignment.class))); +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/roles/GetRoleRequest.java b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/GetRoleRequest.java new file mode 100644 index 00000000..a27da086 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/GetRoleRequest.java @@ -0,0 +1,18 @@ +package io.weaviate.client6.v1.api.rbac.roles; + +import java.util.Collections; +import java.util.Optional; + +import io.weaviate.client6.v1.api.rbac.Role; +import io.weaviate.client6.v1.internal.json.JSON; +import io.weaviate.client6.v1.internal.rest.Endpoint; +import io.weaviate.client6.v1.internal.rest.OptionalEndpoint; +import io.weaviate.client6.v1.internal.rest.UrlEncoder; + +public record GetRoleRequest(String roleName) { + public static final Endpoint> _ENDPOINT = OptionalEndpoint.noBodyOptional( + __ -> "GET", + request -> "/authz/roles/" + UrlEncoder.encodeValue(request.roleName), + __ -> Collections.emptyMap(), + (statusCode, response) -> JSON.deserialize(response, Role.class)); +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/roles/GetUserAssignementsRequest.java b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/GetUserAssignementsRequest.java new file mode 100644 index 00000000..501f736e --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/GetUserAssignementsRequest.java @@ -0,0 +1,21 @@ +package io.weaviate.client6.v1.api.rbac.roles; + +import java.util.Collections; +import java.util.List; + +import com.google.gson.reflect.TypeToken; + +import io.weaviate.client6.v1.internal.json.JSON; +import io.weaviate.client6.v1.internal.rest.Endpoint; +import io.weaviate.client6.v1.internal.rest.SimpleEndpoint; +import io.weaviate.client6.v1.internal.rest.UrlEncoder; + +public record GetUserAssignementsRequest(String roleName) { + @SuppressWarnings("unchecked") + public static final Endpoint> _ENDPOINT = SimpleEndpoint.noBody( + __ -> "GET", + request -> "/authz/roles/" + UrlEncoder.encodeValue(request.roleName) + "/user-assignments", + __ -> Collections.emptyMap(), + (statusCode, response) -> (List) JSON.deserialize(response, + TypeToken.getParameterized(List.class, UserAssignment.class))); +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/roles/GroupAssignment.java b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/GroupAssignment.java new file mode 100644 index 00000000..424aa6ea --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/GroupAssignment.java @@ -0,0 +1,10 @@ +package io.weaviate.client6.v1.api.rbac.roles; + +import com.google.gson.annotations.SerializedName; + +import io.weaviate.client6.v1.api.rbac.groups.GroupType; + +public record GroupAssignment( + @SerializedName("groupId") String groupId, + @SerializedName("groupType") GroupType groupType) { +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/roles/HasPermissionRequest.java b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/HasPermissionRequest.java new file mode 100644 index 00000000..32ed0ae2 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/HasPermissionRequest.java @@ -0,0 +1,18 @@ +package io.weaviate.client6.v1.api.rbac.roles; + +import java.util.Collections; + +import io.weaviate.client6.v1.api.rbac.Permission; +import io.weaviate.client6.v1.internal.json.JSON; +import io.weaviate.client6.v1.internal.rest.Endpoint; +import io.weaviate.client6.v1.internal.rest.SimpleEndpoint; +import io.weaviate.client6.v1.internal.rest.UrlEncoder; + +public record HasPermissionRequest(String roleName, Permission permission) { + public static final Endpoint _ENDPOINT = new SimpleEndpoint<>( + __ -> "POST", + request -> "/authz/roles/" + UrlEncoder.encodeValue(request.roleName) + "/has-permission", + __ -> Collections.emptyMap(), + request -> JSON.serialize(request.permission), + (statusCode, response) -> JSON.deserialize(response, Boolean.class)); +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/roles/ListRolesRequest.java b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/ListRolesRequest.java new file mode 100644 index 00000000..1ea57e1b --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/ListRolesRequest.java @@ -0,0 +1,21 @@ +package io.weaviate.client6.v1.api.rbac.roles; + +import java.util.Collections; +import java.util.List; + +import com.google.gson.reflect.TypeToken; + +import io.weaviate.client6.v1.api.rbac.Role; +import io.weaviate.client6.v1.internal.json.JSON; +import io.weaviate.client6.v1.internal.rest.Endpoint; +import io.weaviate.client6.v1.internal.rest.SimpleEndpoint; + +public record ListRolesRequest() { + @SuppressWarnings("unchecked") + public static final Endpoint> _ENDPOINT = SimpleEndpoint.noBody( + __ -> "GET", + __ -> "/authz/roles", + __ -> Collections.emptyMap(), + (statusCode, response) -> (List) JSON.deserialize(response, + TypeToken.getParameterized(List.class, Role.class))); +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/roles/RemovePermissionsRequest.java b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/RemovePermissionsRequest.java new file mode 100644 index 00000000..b40749fd --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/RemovePermissionsRequest.java @@ -0,0 +1,24 @@ +package io.weaviate.client6.v1.api.rbac.roles; + +import java.util.Collections; +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +import io.weaviate.client6.v1.api.rbac.Permission; +import io.weaviate.client6.v1.internal.json.JSON; +import io.weaviate.client6.v1.internal.rest.Endpoint; +import io.weaviate.client6.v1.internal.rest.SimpleEndpoint; +import io.weaviate.client6.v1.internal.rest.UrlEncoder; + +public record RemovePermissionsRequest(String roleName, List permissions) { + public static final Endpoint _ENDPOINT = SimpleEndpoint.sideEffect( + __ -> "POST", + request -> "/authz/roles/" + UrlEncoder.encodeValue(request.roleName) + "/remove-permissions", + __ -> Collections.emptyMap(), + request -> JSON.serialize(new Body(request.permissions))); + + /** Request body must be {"permissions": [...]}. */ + private static record Body(@SerializedName("permissions") List permissions) { + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/roles/RoleExistsRequest.java b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/RoleExistsRequest.java new file mode 100644 index 00000000..155dd681 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/RoleExistsRequest.java @@ -0,0 +1,14 @@ +package io.weaviate.client6.v1.api.rbac.roles; + +import java.util.Collections; + +import io.weaviate.client6.v1.internal.rest.BooleanEndpoint; +import io.weaviate.client6.v1.internal.rest.Endpoint; +import io.weaviate.client6.v1.internal.rest.UrlEncoder; + +public record RoleExistsRequest(String roleName) { + public static final Endpoint _ENDPOINT = BooleanEndpoint.noBody( + __ -> "GET", + request -> "/authz/roles/" + UrlEncoder.encodeValue(request.roleName), + __ -> Collections.emptyMap()); +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/roles/UserAssignment.java b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/UserAssignment.java new file mode 100644 index 00000000..ba759707 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/UserAssignment.java @@ -0,0 +1,10 @@ +package io.weaviate.client6.v1.api.rbac.roles; + +import com.google.gson.annotations.SerializedName; + +import io.weaviate.client6.v1.api.rbac.users.UserType; + +public record UserAssignment( + @SerializedName("userId") String userId, + @SerializedName("userType") UserType userType) { +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/roles/WeaviateRolesClient.java b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/WeaviateRolesClient.java new file mode 100644 index 00000000..b1120465 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/WeaviateRolesClient.java @@ -0,0 +1,187 @@ +package io.weaviate.client6.v1.api.rbac.roles; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import io.weaviate.client6.v1.api.WeaviateApiException; +import io.weaviate.client6.v1.api.rbac.Permission; +import io.weaviate.client6.v1.api.rbac.Role; +import io.weaviate.client6.v1.internal.rest.RestTransport; + +public class WeaviateRolesClient { + private final RestTransport restTransport; + + public WeaviateRolesClient(RestTransport restTransport) { + this.restTransport = restTransport; + } + + /** + * Create a new role. + * + * @param roleName Role name. + * @param permissions Permissions granted to the role. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public void create(String roleName, Permission... permissions) throws IOException { + var role = new Role(roleName, permissions); + this.restTransport.performRequest(new CreateRoleRequest(role), CreateRoleRequest._ENDPOINT); + } + + /** + * Check if a role with a given name exists. + * + * @param roleName Role name. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public boolean exists(String roleName) throws IOException { + return this.restTransport.performRequest(new RoleExistsRequest(roleName), RoleExistsRequest._ENDPOINT); + } + + /** + * Fetch role definition. + * + * @param roleName Role name. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public Optional get(String roleName) throws IOException { + return this.restTransport.performRequest(new GetRoleRequest(roleName), GetRoleRequest._ENDPOINT); + } + + /** + * List all existing roles. + * + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public List list() throws IOException { + return this.restTransport.performRequest(null, ListRolesRequest._ENDPOINT); + } + + /** + * Delete a role. + * + * @param roleName Role name. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public void delete(String roleName) throws IOException { + this.restTransport.performRequest(new DeleteRoleRequest(roleName), DeleteRoleRequest._ENDPOINT); + } + + /** + * Add permissions to a role. + * + * @param roleName Role name. + * @param permissions Permissions to add to the role. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public void addPermissions(String roleName, Permission... permissions) throws IOException { + this.restTransport.performRequest(new AddPermissionsRequest(roleName, Arrays.asList(permissions)), + AddPermissionsRequest._ENDPOINT); + } + + /** + * Remove permissions from a role. + * + * @param roleName Role name. + * @param permissions Permissions to remove from the role. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public void removePermissions(String roleName, Permission... permissions) throws IOException { + this.restTransport.performRequest(new RemovePermissionsRequest(roleName, Arrays.asList(permissions)), + RemovePermissionsRequest._ENDPOINT); + } + + /** + * Check if a role has a set of permissions. + * + * @param roleName Role name. + * @param permission Permission to check. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public boolean hasPermission(String roleName, Permission permission) throws IOException { + return this.restTransport.performRequest(new HasPermissionRequest(roleName, permission), + HasPermissionRequest._ENDPOINT); + } + + /** + * Get IDs of all users this role is assigned to. + * + * @param roleName Role name. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public List assignedUserIds(String roleName) throws IOException { + return this.restTransport.performRequest(new GetAssignedUsersRequest(roleName), GetAssignedUsersRequest._ENDPOINT); + } + + /** + * Get IDs of all users this role is assigned to along with their user type. + * + *

+ * Note that, unlike {@link #assignedUserIds}, this method MAY return multiple + * entries for the same user ID if OIDCS authentication is enabled: once with + * "db_*" and another time with "oidc" user type. + * + * @param roleName Role name. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public List userAssignments(String roleName) throws IOException { + return this.restTransport.performRequest(new GetUserAssignementsRequest(roleName), + GetUserAssignementsRequest._ENDPOINT); + } + + /** + * Get IDs of all groups this role is assigned to along with their group type. + * + * @param roleName Role name. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public List groupAssignments(String roleName) throws IOException { + return this.restTransport.performRequest(new GetGroupAssignementsRequest(roleName), + GetGroupAssignementsRequest._ENDPOINT); + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/roles/WeaviateRolesClientAsync.java b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/WeaviateRolesClientAsync.java new file mode 100644 index 00000000..64e60fc8 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/roles/WeaviateRolesClientAsync.java @@ -0,0 +1,129 @@ +package io.weaviate.client6.v1.api.rbac.roles; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import io.weaviate.client6.v1.api.rbac.Permission; +import io.weaviate.client6.v1.api.rbac.Role; +import io.weaviate.client6.v1.internal.rest.RestTransport; + +public class WeaviateRolesClientAsync { + private final RestTransport restTransport; + + public WeaviateRolesClientAsync(RestTransport restTransport) { + this.restTransport = restTransport; + } + + /** + * Create a new role. + * + * @param roleName Role name. + * @param permissions Permissions granted to the role. + */ + public CompletableFuture create(String roleName, Permission... permissions) { + var role = new Role(roleName, permissions); + return this.restTransport.performRequestAsync(new CreateRoleRequest(role), CreateRoleRequest._ENDPOINT); + } + + /** + * Check if a role with a given name exists. + * + * @param roleName Role name. + */ + public CompletableFuture exists(String roleName) { + return this.restTransport.performRequestAsync(new RoleExistsRequest(roleName), RoleExistsRequest._ENDPOINT); + } + + /** + * Fetch role definition. + * + * @param roleName Role name. + */ + public CompletableFuture> get(String roleName) { + return this.restTransport.performRequestAsync(new GetRoleRequest(roleName), GetRoleRequest._ENDPOINT); + } + + /** List all existing roles. */ + public CompletableFuture> list() { + return this.restTransport.performRequestAsync(null, ListRolesRequest._ENDPOINT); + } + + /** + * Delete a role. + * + * @param roleName Role name. + */ + public CompletableFuture delete(String roleName) { + return this.restTransport.performRequestAsync(new DeleteRoleRequest(roleName), DeleteRoleRequest._ENDPOINT); + } + + /** + * Add permissions to a role. + * + * @param roleName Role name. + * @param permissions Permissions to add to the role. + */ + public CompletableFuture addPermissions(String roleName, Permission... permissions) { + return this.restTransport.performRequestAsync(new AddPermissionsRequest(roleName, Arrays.asList(permissions)), + AddPermissionsRequest._ENDPOINT); + } + + /** + * Remove permissions from a role. + * + * @param roleName Role name. + * @param permissions Permissions to remove from the role. + */ + public CompletableFuture removePermissions(String roleName, Permission... permissions) { + return this.restTransport.performRequestAsync(new RemovePermissionsRequest(roleName, Arrays.asList(permissions)), + RemovePermissionsRequest._ENDPOINT); + } + + /** + * Check if a role has a set of permissions. + * + * @param roleName Role name. + * @param permission Permission to check. + */ + public CompletableFuture hasPermission(String roleName, Permission permission) { + return this.restTransport.performRequestAsync(new HasPermissionRequest(roleName, permission), + HasPermissionRequest._ENDPOINT); + } + + /** + * Get IDs of all users this role is assigned to. + * + * @param roleName Role name. + */ + public CompletableFuture> assignedUserIds(String roleName) { + return this.restTransport.performRequestAsync(new GetAssignedUsersRequest(roleName), + GetAssignedUsersRequest._ENDPOINT); + } + + /** + * Get IDs of all users this role is assigned to along with their user type. + * + *

+ * Note that, unlike {@link #assignedUserIds}, this method MAY return multiple + * entries for the same user ID if OIDCS authentication is enabled: once with + * "db_*" and another time with "oidc" user type. + * + * @param roleName Role name. + */ + public CompletableFuture> userAssignments(String roleName) { + return this.restTransport.performRequestAsync(new GetUserAssignementsRequest(roleName), + GetUserAssignementsRequest._ENDPOINT); + } + + /** + * Get IDs of all groups this role is assigned to along with their group type. + * + * @param roleName Role name. + */ + public CompletableFuture> groupAssignments(String roleName) { + return this.restTransport.performRequestAsync(new GetGroupAssignementsRequest(roleName), + GetGroupAssignementsRequest._ENDPOINT); + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/users/ActivateDbUserRequest.java b/src/main/java/io/weaviate/client6/v1/api/rbac/users/ActivateDbUserRequest.java new file mode 100644 index 00000000..5c7ae391 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/users/ActivateDbUserRequest.java @@ -0,0 +1,16 @@ +package io.weaviate.client6.v1.api.rbac.users; + +import java.util.Collections; + +import io.weaviate.client6.v1.internal.rest.Endpoint; +import io.weaviate.client6.v1.internal.rest.SimpleEndpoint; +import io.weaviate.client6.v1.internal.rest.UrlEncoder; + +public record ActivateDbUserRequest(String userId) { + + public static final Endpoint _ENDPOINT = SimpleEndpoint.sideEffect( + __ -> "POST", + request -> "/users/db/" + UrlEncoder.encodeValue(((ActivateDbUserRequest) request).userId) + "/activate", + request -> Collections.emptyMap()) + .allowStatus(409); +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/users/AssignRolesRequest.java b/src/main/java/io/weaviate/client6/v1/api/rbac/users/AssignRolesRequest.java new file mode 100644 index 00000000..95c3481f --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/users/AssignRolesRequest.java @@ -0,0 +1,26 @@ +package io.weaviate.client6.v1.api.rbac.users; + +import java.util.Collections; +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +import io.weaviate.client6.v1.internal.json.JSON; +import io.weaviate.client6.v1.internal.rest.Endpoint; +import io.weaviate.client6.v1.internal.rest.SimpleEndpoint; +import io.weaviate.client6.v1.internal.rest.UrlEncoder; + +public record AssignRolesRequest(String userId, UserType userType, List roleNames) { + + public static final Endpoint _ENDPOINT = SimpleEndpoint.sideEffect( + __ -> "POST", + request -> "/authz/users/" + UrlEncoder.encodeValue(request.userId) + "/assign", + request -> Collections.emptyMap(), + request -> JSON.serialize(new Body(request.roleNames, request.userType))); + + /** Request body should be {"roles": [...], "userType": ""} */ + private static record Body( + @SerializedName("roles") List roleNames, + @SerializedName("userType") UserType userType) { + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/users/CreateDbUserRequest.java b/src/main/java/io/weaviate/client6/v1/api/rbac/users/CreateDbUserRequest.java new file mode 100644 index 00000000..7b425cff --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/users/CreateDbUserRequest.java @@ -0,0 +1,17 @@ +package io.weaviate.client6.v1.api.rbac.users; + +import java.util.Collections; + +import io.weaviate.client6.v1.internal.json.JSON; +import io.weaviate.client6.v1.internal.rest.Endpoint; +import io.weaviate.client6.v1.internal.rest.SimpleEndpoint; +import io.weaviate.client6.v1.internal.rest.UrlEncoder; + +public record CreateDbUserRequest(String userId) { + + public static final Endpoint _ENDPOINT = SimpleEndpoint.noBody( + __ -> "POST", + request -> "/users/db/" + UrlEncoder.encodeValue(request.userId), + request -> Collections.emptyMap(), + (statusCode, response) -> JSON.deserialize(response, CreateDbUserResponse.class).apiKey()); +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/users/CreateDbUserResponse.java b/src/main/java/io/weaviate/client6/v1/api/rbac/users/CreateDbUserResponse.java new file mode 100644 index 00000000..73ff1071 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/users/CreateDbUserResponse.java @@ -0,0 +1,6 @@ +package io.weaviate.client6.v1.api.rbac.users; + +import com.google.gson.annotations.SerializedName; + +public record CreateDbUserResponse(@SerializedName("apikey") String apiKey) { +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/users/DbUser.java b/src/main/java/io/weaviate/client6/v1/api/rbac/users/DbUser.java new file mode 100644 index 00000000..f73313b5 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/users/DbUser.java @@ -0,0 +1,16 @@ +package io.weaviate.client6.v1.api.rbac.users; + +import java.time.OffsetDateTime; +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +public record DbUser( + @SerializedName("userId") String id, + @SerializedName("dbUserType") UserType userType, + @SerializedName("active") boolean active, + @SerializedName("roles") List roleNames, + @SerializedName("createdAt") OffsetDateTime createdAt, + @SerializedName("lastUsedAt") OffsetDateTime lastUsedAt, + @SerializedName("apiKeyFirstLetters") String apiKeyFirstLetters) { +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/users/DbUsersClient.java b/src/main/java/io/weaviate/client6/v1/api/rbac/users/DbUsersClient.java new file mode 100644 index 00000000..4a24472c --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/users/DbUsersClient.java @@ -0,0 +1,147 @@ +package io.weaviate.client6.v1.api.rbac.users; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +import io.weaviate.client6.v1.api.WeaviateApiException; +import io.weaviate.client6.v1.internal.ObjectBuilder; +import io.weaviate.client6.v1.internal.rest.RestTransport; + +public class DbUsersClient extends NamespacedUsersClient { + + public DbUsersClient(RestTransport restTransport) { + super(restTransport, UserType.DB_USER); + } + + /** + * Create a new "db" user. + * + * @param userId User ID. + * @return API key for the created user. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public String create(String userId) throws IOException { + return this.restTransport.performRequest(new CreateDbUserRequest(userId), CreateDbUserRequest._ENDPOINT); + } + + /** + * Delete a "db" user. + * + * @param userId User ID. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public void delete(String userId) throws IOException { + this.restTransport.performRequest(new DeleteDbUserRequest(userId), DeleteDbUserRequest._ENDPOINT); + } + + /** + * Activate a "db" user. + * + * @param userId User ID. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public void activate(String userId) throws IOException { + this.restTransport.performRequest(new ActivateDbUserRequest(userId), ActivateDbUserRequest._ENDPOINT); + } + + /** + * Deactivate a "db" user. + * + * @param userId User ID. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public void deactivate(String userId) throws IOException { + this.restTransport.performRequest(new DeactivateDbUserRequest(userId), DeactivateDbUserRequest._ENDPOINT); + } + + /** + * Rotate API key of the "db" user. + * + * @param userId User ID. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public String rotateKey(String userId) throws IOException { + return this.restTransport.performRequest(new RotateDbUserKeyRequest(userId), RotateDbUserKeyRequest._ENDPOINT); + } + + /** + * Fetch "db" user info. + * + * @param userId User ID. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public Optional byName(String userId) throws IOException { + return this.restTransport.performRequest(GetDbUserRequest.of(userId), GetDbUserRequest._ENDPOINT); + } + + /** + * Fetch "db" user info. + * + * @param userId User ID. + * @param fn Lambda expression for optional parameters. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public Optional byName(String userId, Function> fn) + throws IOException { + return this.restTransport.performRequest(GetDbUserRequest.of(userId, fn), GetDbUserRequest._ENDPOINT); + } + + /** + * List all "db" users. + * + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public List list() + throws IOException { + return this.restTransport.performRequest(ListDbUsersRequest.of(), ListDbUsersRequest._ENDPOINT); + } + + /** + * List all "db" users. + * + * @param fn Lambda expression for optional parameters. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public List list(Function> fn) + throws IOException { + return this.restTransport.performRequest(ListDbUsersRequest.of(fn), ListDbUsersRequest._ENDPOINT); + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/users/DbUsersClientAsync.java b/src/main/java/io/weaviate/client6/v1/api/rbac/users/DbUsersClientAsync.java new file mode 100644 index 00000000..a315656c --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/users/DbUsersClientAsync.java @@ -0,0 +1,102 @@ +package io.weaviate.client6.v1.api.rbac.users; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +import io.weaviate.client6.v1.internal.ObjectBuilder; +import io.weaviate.client6.v1.internal.rest.RestTransport; + +public class DbUsersClientAsync extends NamespacedUsersClientAsync { + + public DbUsersClientAsync(RestTransport restTransport) { + super(restTransport, UserType.DB_USER); + } + + /** + * Create a new "db" user. + * + * @param userId User ID. + * @return API key for the created user. + */ + public CompletableFuture create(String userId) throws IOException { + return this.restTransport.performRequestAsync(new CreateDbUserRequest(userId), CreateDbUserRequest._ENDPOINT); + } + + /** + * Delete a "db" user. + * + * @param userId User ID. + */ + public CompletableFuture delete(String userId) throws IOException { + return this.restTransport.performRequestAsync(new DeleteDbUserRequest(userId), DeleteDbUserRequest._ENDPOINT); + } + + /** + * Activate a "db" user. + * + * @param userId User ID. + */ + public CompletableFuture activate(String userId) throws IOException { + return this.restTransport.performRequestAsync(new ActivateDbUserRequest(userId), ActivateDbUserRequest._ENDPOINT); + } + + /** + * Deactivate a "db" user. + * + * @param userId User ID. + */ + public CompletableFuture deactivate(String userId) throws IOException { + return this.restTransport.performRequestAsync(new DeactivateDbUserRequest(userId), + DeactivateDbUserRequest._ENDPOINT); + } + + /** + * Rotate API key of the "db" user. + * + * @param userId User ID. + */ + public CompletableFuture rotateKey(String userId) throws IOException { + return this.restTransport.performRequestAsync(new RotateDbUserKeyRequest(userId), RotateDbUserKeyRequest._ENDPOINT); + } + + /** + * Fetch "db" user info. + * + * @param userId User ID. + */ + public CompletableFuture> byName(String userId) throws IOException { + return this.restTransport.performRequestAsync(GetDbUserRequest.of(userId), GetDbUserRequest._ENDPOINT); + } + + /** + * Fetch "db" user info. + * + * @param userId User ID. + * @param fn Lambda expression for optional parameters. + */ + public CompletableFuture> byName(String userId, + Function> fn) + throws IOException { + return this.restTransport.performRequestAsync(GetDbUserRequest.of(userId, fn), GetDbUserRequest._ENDPOINT); + } + + /** List all "db" users. */ + public CompletableFuture> list() + throws IOException { + return this.restTransport.performRequestAsync(ListDbUsersRequest.of(), ListDbUsersRequest._ENDPOINT); + } + + /** + * List all "db" users. + * + * @param fn Lambda expression for optional parameters. + */ + public CompletableFuture> list( + Function> fn) + throws IOException { + return this.restTransport.performRequestAsync(ListDbUsersRequest.of(fn), ListDbUsersRequest._ENDPOINT); + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/users/DeactivateDbUserRequest.java b/src/main/java/io/weaviate/client6/v1/api/rbac/users/DeactivateDbUserRequest.java new file mode 100644 index 00000000..dc5ec489 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/users/DeactivateDbUserRequest.java @@ -0,0 +1,16 @@ +package io.weaviate.client6.v1.api.rbac.users; + +import java.util.Collections; + +import io.weaviate.client6.v1.internal.rest.Endpoint; +import io.weaviate.client6.v1.internal.rest.SimpleEndpoint; +import io.weaviate.client6.v1.internal.rest.UrlEncoder; + +public record DeactivateDbUserRequest(String userId) { + + public static final Endpoint _ENDPOINT = SimpleEndpoint.sideEffect( + __ -> "POST", + request -> "/users/db/" + UrlEncoder.encodeValue(((DeactivateDbUserRequest) request).userId) + "/deactivate", + request -> Collections.emptyMap()) + .allowStatus(409); +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/users/DeleteDbUserRequest.java b/src/main/java/io/weaviate/client6/v1/api/rbac/users/DeleteDbUserRequest.java new file mode 100644 index 00000000..d34636d5 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/users/DeleteDbUserRequest.java @@ -0,0 +1,15 @@ +package io.weaviate.client6.v1.api.rbac.users; + +import java.util.Collections; + +import io.weaviate.client6.v1.internal.rest.Endpoint; +import io.weaviate.client6.v1.internal.rest.SimpleEndpoint; +import io.weaviate.client6.v1.internal.rest.UrlEncoder; + +public record DeleteDbUserRequest(String userId) { + + public static final Endpoint _ENDPOINT = SimpleEndpoint.sideEffect( + __ -> "DELETE", + request -> "/users/db/" + UrlEncoder.encodeValue(request.userId), + request -> Collections.emptyMap()); +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/users/GetAssignedRolesRequest.java b/src/main/java/io/weaviate/client6/v1/api/rbac/users/GetAssignedRolesRequest.java new file mode 100644 index 00000000..05644046 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/users/GetAssignedRolesRequest.java @@ -0,0 +1,61 @@ +package io.weaviate.client6.v1.api.rbac.users; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import com.google.gson.reflect.TypeToken; + +import io.weaviate.client6.v1.api.rbac.Role; +import io.weaviate.client6.v1.internal.ObjectBuilder; +import io.weaviate.client6.v1.internal.json.JSON; +import io.weaviate.client6.v1.internal.rest.Endpoint; +import io.weaviate.client6.v1.internal.rest.SimpleEndpoint; +import io.weaviate.client6.v1.internal.rest.UrlEncoder; + +public record GetAssignedRolesRequest(String userId, UserType userType, Boolean includePermissions) { + + @SuppressWarnings("unchecked") + public static final Endpoint> _ENDPOINT = SimpleEndpoint.noBody( + __ -> "GET", + request -> "/authz/users/" + UrlEncoder.encodeValue(request.userId) + "/roles/" + request.userType.jsonValue(), + request -> request.includePermissions != null ? Map.of("includePermissions", request.includePermissions) + : Collections.emptyMap(), + (statusCode, + response) -> (List) JSON.deserialize(response, TypeToken.getParameterized(List.class, Role.class))); + + public static GetAssignedRolesRequest of(String userId, UserType userType) { + return of(userId, userType, ObjectBuilder.identity()); + } + + public static GetAssignedRolesRequest of(String userId, UserType userType, + Function> fn) { + return fn.apply(new Builder(userId, userType)).build(); + } + + public GetAssignedRolesRequest(Builder builder) { + this(builder.userId, builder.userType, builder.includePermissions); + } + + public static class Builder implements ObjectBuilder { + private final String userId; + private final UserType userType; + private Boolean includePermissions; + + public Builder(String userId, UserType userType) { + this.userId = userId; + this.userType = userType; + } + + public Builder includePermissions(boolean includePermissions) { + this.includePermissions = includePermissions; + return this; + } + + @Override + public GetAssignedRolesRequest build() { + return new GetAssignedRolesRequest(this); + } + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/users/GetDbUserRequest.java b/src/main/java/io/weaviate/client6/v1/api/rbac/users/GetDbUserRequest.java new file mode 100644 index 00000000..032ac8d1 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/users/GetDbUserRequest.java @@ -0,0 +1,54 @@ +package io.weaviate.client6.v1.api.rbac.users; + +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +import io.weaviate.client6.v1.internal.ObjectBuilder; +import io.weaviate.client6.v1.internal.json.JSON; +import io.weaviate.client6.v1.internal.rest.Endpoint; +import io.weaviate.client6.v1.internal.rest.OptionalEndpoint; +import io.weaviate.client6.v1.internal.rest.UrlEncoder; + +public record GetDbUserRequest(String userId, Boolean includeLastUsedAt) { + + public static final Endpoint> _ENDPOINT = OptionalEndpoint.noBodyOptional( + __ -> "GET", + request -> "/users/db/" + UrlEncoder.encodeValue(request.userId), + request -> request.includeLastUsedAt != null + ? Map.of("includeLastUsedTime", request.includeLastUsedAt) + : Collections.emptyMap(), + (statusCode, response) -> JSON.deserialize(response, DbUser.class)); + + public static GetDbUserRequest of(String userId) { + return of(userId, ObjectBuilder.identity()); + } + + public static GetDbUserRequest of(String userId, Function> fn) { + return fn.apply(new Builder(userId)).build(); + } + + public GetDbUserRequest(Builder builder) { + this(builder.userId, builder.includeLastUsedAt); + } + + public static class Builder implements ObjectBuilder { + private final String userId; + private Boolean includeLastUsedAt; + + public Builder(String userId) { + this.userId = userId; + } + + public Builder includeLastUsedAt(boolean includeLastUsedAt) { + this.includeLastUsedAt = includeLastUsedAt; + return this; + } + + @Override + public GetDbUserRequest build() { + return new GetDbUserRequest(this); + } + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/users/GetMyUserRequest.java b/src/main/java/io/weaviate/client6/v1/api/rbac/users/GetMyUserRequest.java new file mode 100644 index 00000000..e467e17f --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/users/GetMyUserRequest.java @@ -0,0 +1,16 @@ +package io.weaviate.client6.v1.api.rbac.users; + +import java.util.Collections; + +import io.weaviate.client6.v1.internal.json.JSON; +import io.weaviate.client6.v1.internal.rest.Endpoint; +import io.weaviate.client6.v1.internal.rest.SimpleEndpoint; + +public record GetMyUserRequest() { + + public static final Endpoint _ENDPOINT = SimpleEndpoint.noBody( + __ -> "GET", + __ -> "/users/own-info", + __ -> Collections.emptyMap(), + (statusCode, response) -> JSON.deserialize(response, User.class)); +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/users/ListDbUsersRequest.java b/src/main/java/io/weaviate/client6/v1/api/rbac/users/ListDbUsersRequest.java new file mode 100644 index 00000000..72fe2817 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/users/ListDbUsersRequest.java @@ -0,0 +1,53 @@ +package io.weaviate.client6.v1.api.rbac.users; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import com.google.gson.reflect.TypeToken; + +import io.weaviate.client6.v1.internal.ObjectBuilder; +import io.weaviate.client6.v1.internal.json.JSON; +import io.weaviate.client6.v1.internal.rest.Endpoint; +import io.weaviate.client6.v1.internal.rest.SimpleEndpoint; + +public record ListDbUsersRequest(Boolean includeLastUsedAt) { + + @SuppressWarnings("unchecked") + public static final Endpoint> _ENDPOINT = SimpleEndpoint.noBody( + __ -> "GET", + request -> "/users/db", + request -> request.includeLastUsedAt != null + ? Map.of("includeLastUsedTime", request.includeLastUsedAt) + : Collections.emptyMap(), + (statusCode, + response) -> (List) JSON.deserialize(response, + TypeToken.getParameterized(List.class, DbUser.class))); + + public static ListDbUsersRequest of() { + return of(ObjectBuilder.identity()); + } + + public static ListDbUsersRequest of(Function> fn) { + return fn.apply(new Builder()).build(); + } + + public ListDbUsersRequest(Builder builder) { + this(builder.includeLastUsedAt); + } + + public static class Builder implements ObjectBuilder { + private Boolean includeLastUsedAt; + + public Builder includeLastUsedAt(boolean includeLastUsedAt) { + this.includeLastUsedAt = includeLastUsedAt; + return this; + } + + @Override + public ListDbUsersRequest build() { + return new ListDbUsersRequest(this); + } + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/users/NamespacedUsersClient.java b/src/main/java/io/weaviate/client6/v1/api/rbac/users/NamespacedUsersClient.java new file mode 100644 index 00000000..ff3d311b --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/users/NamespacedUsersClient.java @@ -0,0 +1,85 @@ +package io.weaviate.client6.v1.api.rbac.users; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; + +import io.weaviate.client6.v1.api.WeaviateApiException; +import io.weaviate.client6.v1.api.rbac.Role; +import io.weaviate.client6.v1.internal.ObjectBuilder; +import io.weaviate.client6.v1.internal.rest.RestTransport; + +public abstract class NamespacedUsersClient { + protected final RestTransport restTransport; + private final UserType userType; + + public NamespacedUsersClient(RestTransport restTransport, UserType userType) { + this.restTransport = restTransport; + this.userType = userType; + } + + /** + * Get the roles assigned a user with type {@link #userType}. + * + * @param userId OIDC group ID. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public List assignedRoles(String userId) throws IOException { + return this.restTransport.performRequest(GetAssignedRolesRequest.of(userId, userType), + GetAssignedRolesRequest._ENDPOINT); + } + + /** + * Get the roles assigned a user with type {@link #userType}. + * + * @param userId OIDC group ID. + * @param fn Lambda expression for optional parameters. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public List assignedRoles(String userId, + Function> fn) throws IOException { + return this.restTransport.performRequest(GetAssignedRolesRequest.of(userId, userType, fn), + GetAssignedRolesRequest._ENDPOINT); + } + + /** + * Assing roles to a user with type {@link #userType}. + * + * @param userId User ID. + * @param roleNames Role names. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public void assignRoles(String userId, String... roleNames) throws IOException { + this.restTransport.performRequest(new AssignRolesRequest(userId, userType, Arrays.asList(roleNames)), + AssignRolesRequest._ENDPOINT); + } + + /** + * Revoke roles from a user with type {@link #userType}. + * + * @param userId User ID. + * @param roleNames Role names. + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public void revokeRoles(String userId, String... roleNames) throws IOException { + this.restTransport.performRequest(new RevokeRolesRequest(userId, userType, Arrays.asList(roleNames)), + RevokeRolesRequest._ENDPOINT); + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/users/NamespacedUsersClientAsync.java b/src/main/java/io/weaviate/client6/v1/api/rbac/users/NamespacedUsersClientAsync.java new file mode 100644 index 00000000..7053b5ab --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/users/NamespacedUsersClientAsync.java @@ -0,0 +1,64 @@ +package io.weaviate.client6.v1.api.rbac.users; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +import io.weaviate.client6.v1.api.rbac.Role; +import io.weaviate.client6.v1.internal.ObjectBuilder; +import io.weaviate.client6.v1.internal.rest.RestTransport; + +public abstract class NamespacedUsersClientAsync { + protected final RestTransport restTransport; + private final UserType userType; + + public NamespacedUsersClientAsync(RestTransport restTransport, UserType userType) { + this.restTransport = restTransport; + this.userType = userType; + } + + /** + * Get the roles assigned a user with type {@link #userType}. + * + * @param userId OIDC group ID. + */ + public CompletableFuture> assignedRoles(String userId) { + return this.restTransport.performRequestAsync(GetAssignedRolesRequest.of(userId, userType), + GetAssignedRolesRequest._ENDPOINT); + } + + /** + * Get the roles assigned a user with type {@link #userType}. + * + * @param userId OIDC group ID. + * @param fn Lambda expression for optional parameters. + */ + public CompletableFuture> assignedRoles(String userId, + Function> fn) { + return this.restTransport.performRequestAsync(GetAssignedRolesRequest.of(userId, userType, fn), + GetAssignedRolesRequest._ENDPOINT); + } + + /** + * Assing roles to a user with type {@link #userType}. + * + * @param userId User ID. + * @param roleNames Role names. + */ + public CompletableFuture assignRoles(String userId, String... roleNames) { + return this.restTransport.performRequestAsync(new AssignRolesRequest(userId, userType, Arrays.asList(roleNames)), + AssignRolesRequest._ENDPOINT); + } + + /** + * Revoke roles from a user with type {@link #userType}. + * + * @param userId User ID. + * @param roleNames Role names. + */ + public CompletableFuture revokeRoles(String userId, String... roleNames) { + return this.restTransport.performRequestAsync(new RevokeRolesRequest(userId, userType, Arrays.asList(roleNames)), + RevokeRolesRequest._ENDPOINT); + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/users/OidcUsersClient.java b/src/main/java/io/weaviate/client6/v1/api/rbac/users/OidcUsersClient.java new file mode 100644 index 00000000..1159c8c0 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/users/OidcUsersClient.java @@ -0,0 +1,10 @@ +package io.weaviate.client6.v1.api.rbac.users; + +import io.weaviate.client6.v1.internal.rest.RestTransport; + +public class OidcUsersClient extends NamespacedUsersClient { + + public OidcUsersClient(RestTransport restTransport) { + super(restTransport, UserType.OIDC); + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/users/OidcUsersClientAsync.java b/src/main/java/io/weaviate/client6/v1/api/rbac/users/OidcUsersClientAsync.java new file mode 100644 index 00000000..7d587005 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/users/OidcUsersClientAsync.java @@ -0,0 +1,10 @@ +package io.weaviate.client6.v1.api.rbac.users; + +import io.weaviate.client6.v1.internal.rest.RestTransport; + +public class OidcUsersClientAsync extends NamespacedUsersClientAsync { + + public OidcUsersClientAsync(RestTransport restTransport) { + super(restTransport, UserType.OIDC); + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/users/RevokeRolesRequest.java b/src/main/java/io/weaviate/client6/v1/api/rbac/users/RevokeRolesRequest.java new file mode 100644 index 00000000..d9e6f2c3 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/users/RevokeRolesRequest.java @@ -0,0 +1,26 @@ +package io.weaviate.client6.v1.api.rbac.users; + +import java.util.Collections; +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +import io.weaviate.client6.v1.internal.json.JSON; +import io.weaviate.client6.v1.internal.rest.Endpoint; +import io.weaviate.client6.v1.internal.rest.SimpleEndpoint; +import io.weaviate.client6.v1.internal.rest.UrlEncoder; + +public record RevokeRolesRequest(String userId, UserType userType, List roleNames) { + + public static final Endpoint _ENDPOINT = SimpleEndpoint.sideEffect( + __ -> "POST", + request -> "/authz/users/" + UrlEncoder.encodeValue(request.userId) + "/revoke", + request -> Collections.emptyMap(), + request -> JSON.serialize(new Body(request.roleNames, request.userType))); + + /** Request body should be {"roles": [...], "userType": ""} */ + private static record Body( + @SerializedName("roles") List roleNames, + @SerializedName("userType") UserType userType) { + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/users/RotateDbUserKeyRequest.java b/src/main/java/io/weaviate/client6/v1/api/rbac/users/RotateDbUserKeyRequest.java new file mode 100644 index 00000000..9d394f54 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/users/RotateDbUserKeyRequest.java @@ -0,0 +1,17 @@ +package io.weaviate.client6.v1.api.rbac.users; + +import java.util.Collections; + +import io.weaviate.client6.v1.internal.json.JSON; +import io.weaviate.client6.v1.internal.rest.Endpoint; +import io.weaviate.client6.v1.internal.rest.SimpleEndpoint; +import io.weaviate.client6.v1.internal.rest.UrlEncoder; + +public record RotateDbUserKeyRequest(String userId) { + + public static final Endpoint _ENDPOINT = SimpleEndpoint.noBody( + __ -> "POST", + request -> "/users/db/" + UrlEncoder.encodeValue(request.userId) + "/rotate-key", + request -> Collections.emptyMap(), + (statusCode, response) -> JSON.deserialize(response, RotateDbUserKeyResponse.class).apiKey()); +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/users/RotateDbUserKeyResponse.java b/src/main/java/io/weaviate/client6/v1/api/rbac/users/RotateDbUserKeyResponse.java new file mode 100644 index 00000000..6099dd76 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/users/RotateDbUserKeyResponse.java @@ -0,0 +1,6 @@ +package io.weaviate.client6.v1.api.rbac.users; + +import com.google.gson.annotations.SerializedName; + +public record RotateDbUserKeyResponse(@SerializedName("apikey") String apiKey) { +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/users/User.java b/src/main/java/io/weaviate/client6/v1/api/rbac/users/User.java new file mode 100644 index 00000000..d31f66dc --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/users/User.java @@ -0,0 +1,12 @@ +package io.weaviate.client6.v1.api.rbac.users; + +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +import io.weaviate.client6.v1.api.rbac.Role; + +public record User( + @SerializedName("username") String id, + @SerializedName("roles") List roles) { +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/users/UserType.java b/src/main/java/io/weaviate/client6/v1/api/rbac/users/UserType.java new file mode 100644 index 00000000..5aefa28b --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/users/UserType.java @@ -0,0 +1,22 @@ +package io.weaviate.client6.v1.api.rbac.users; + +import com.google.gson.annotations.SerializedName; + +public enum UserType { + @SerializedName(value = "db", alternate = "db_user") + DB_USER("db"), + @SerializedName(value = "db", alternate = "db_env_user") + DB_ENV_USER("db"), + @SerializedName("oidc") + OIDC("oidc"); + + private final String jsonValue; + + private UserType(String jsonValue) { + this.jsonValue = jsonValue; + } + + public String jsonValue() { + return jsonValue; + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/users/WeaviateUsersClient.java b/src/main/java/io/weaviate/client6/v1/api/rbac/users/WeaviateUsersClient.java new file mode 100644 index 00000000..300843d4 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/users/WeaviateUsersClient.java @@ -0,0 +1,38 @@ +package io.weaviate.client6.v1.api.rbac.users; + +import java.io.IOException; + +import io.weaviate.client6.v1.api.WeaviateApiException; +import io.weaviate.client6.v1.internal.rest.RestTransport; + +public class WeaviateUsersClient { + private final RestTransport restTransport; + + /** + * Client for managing {@link UserType#DB_USER} and {@link UserType#DB_ENV_USER} + * users. + */ + public final DbUsersClient db; + + /** Client for managing {@link UserType#OIDC} users. */ + public final OidcUsersClient oidc; + + public WeaviateUsersClient(RestTransport restTransport) { + this.restTransport = restTransport; + this.db = new DbUsersClient(restTransport); + this.oidc = new OidcUsersClient(restTransport); + } + + /** + * Get my user info. + * + * @throws WeaviateApiException in case the server returned with an + * error status code. + * @throws IOException in case the request was not sent successfully + * due to a malformed request, a networking error + * or the server being unavailable. + */ + public User myUser() throws IOException { + return this.restTransport.performRequest(null, GetMyUserRequest._ENDPOINT); + } +} diff --git a/src/main/java/io/weaviate/client6/v1/api/rbac/users/WeaviateUsersClientAsync.java b/src/main/java/io/weaviate/client6/v1/api/rbac/users/WeaviateUsersClientAsync.java new file mode 100644 index 00000000..8104a867 --- /dev/null +++ b/src/main/java/io/weaviate/client6/v1/api/rbac/users/WeaviateUsersClientAsync.java @@ -0,0 +1,29 @@ +package io.weaviate.client6.v1.api.rbac.users; + +import java.util.concurrent.CompletableFuture; + +import io.weaviate.client6.v1.internal.rest.RestTransport; + +public class WeaviateUsersClientAsync { + private final RestTransport restTransport; + + /** + * Client for managing {@link UserType#DB_USER} and {@link UserType#DB_ENV_USER} + * users. + */ + public final DbUsersClientAsync db; + + /** Client for managing {@link UserType#OIDC} users. */ + public final OidcUsersClientAsync oidc; + + public WeaviateUsersClientAsync(RestTransport restTransport) { + this.restTransport = restTransport; + this.db = new DbUsersClientAsync(restTransport); + this.oidc = new OidcUsersClientAsync(restTransport); + } + + /** Get my user info. */ + public CompletableFuture myUser() { + return this.restTransport.performRequestAsync(null, GetMyUserRequest._ENDPOINT); + } +} diff --git a/src/main/java/io/weaviate/client6/v1/internal/grpc/GrpcTransport.java b/src/main/java/io/weaviate/client6/v1/internal/grpc/GrpcTransport.java index 952bfc33..cad58257 100644 --- a/src/main/java/io/weaviate/client6/v1/internal/grpc/GrpcTransport.java +++ b/src/main/java/io/weaviate/client6/v1/internal/grpc/GrpcTransport.java @@ -1,12 +1,11 @@ package io.weaviate.client6.v1.internal.grpc; -import java.io.Closeable; import java.util.concurrent.CompletableFuture; public interface GrpcTransport extends AutoCloseable { - ResponseT performRequest(RequestT request, - Rpc rpc); + ResponseT performRequest(RequestT request, + Rpc rpc); - CompletableFuture performRequestAsync(RequestT request, - Rpc rpc); + CompletableFuture performRequestAsync(RequestT request, + Rpc rpc); } diff --git a/src/main/java/io/weaviate/client6/v1/internal/json/JSON.java b/src/main/java/io/weaviate/client6/v1/internal/json/JSON.java index 3d2e0008..b0b60140 100644 --- a/src/main/java/io/weaviate/client6/v1/internal/json/JSON.java +++ b/src/main/java/io/weaviate/client6/v1/internal/json/JSON.java @@ -13,6 +13,10 @@ public final class JSON { var gsonBuilder = new GsonBuilder(); // TypeAdapterFactories --------------------------------------------------- + gsonBuilder.registerTypeAdapterFactory( + io.weaviate.client6.v1.api.rbac.Permission.CustomTypeAdapterFactory.INSTANCE); + gsonBuilder.registerTypeAdapterFactory( + io.weaviate.client6.v1.api.rbac.Role.CustomTypeAdapterFactory.INSTANCE); gsonBuilder.registerTypeAdapterFactory( io.weaviate.client6.v1.api.collections.WeaviateObject.CustomTypeAdapterFactory.INSTANCE); gsonBuilder.registerTypeAdapterFactory( diff --git a/src/main/java/io/weaviate/client6/v1/internal/rest/DefaultRestTransport.java b/src/main/java/io/weaviate/client6/v1/internal/rest/DefaultRestTransport.java index 2e6323be..24357b1a 100644 --- a/src/main/java/io/weaviate/client6/v1/internal/rest/DefaultRestTransport.java +++ b/src/main/java/io/weaviate/client6/v1/internal/rest/DefaultRestTransport.java @@ -110,6 +110,7 @@ private ResponseT handleResponse(Endpoint var body = httpResponse.getEntity() != null ? EntityUtils.toString(httpResponse.getEntity()) : ""; + return _handleResponse(endpoint, method, url, statusCode, body); } @@ -169,6 +170,7 @@ private ResponseT handleResponseAsync( @SuppressWarnings("unchecked") private ResponseT _handleResponse(Endpoint endpoint, String method, String url, int statusCode, String body) { + if (endpoint.isError(statusCode)) { var message = endpoint.deserializeError(statusCode, body); throw WeaviateApiException.http(method, url, statusCode, message); diff --git a/src/main/java/io/weaviate/client6/v1/internal/rest/EndpointBase.java b/src/main/java/io/weaviate/client6/v1/internal/rest/EndpointBase.java index 29cf803d..de57747b 100644 --- a/src/main/java/io/weaviate/client6/v1/internal/rest/EndpointBase.java +++ b/src/main/java/io/weaviate/client6/v1/internal/rest/EndpointBase.java @@ -1,7 +1,10 @@ package io.weaviate.client6.v1.internal.rest; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Function; import com.google.gson.annotations.SerializedName; @@ -16,6 +19,8 @@ public abstract class EndpointBase implements Endpoint body; protected final Function> queryParameters; + private final Set acceptStatusCodes = new HashSet<>(); + @SuppressWarnings("unchecked") protected static Function nullBody() { return (Function) NULL_BODY; @@ -54,7 +59,11 @@ public String body(RequestT request) { @Override public boolean isError(int statusCode) { - return statusCode >= 400; + return statusCode >= 400 && !acceptStatusCodes.contains(statusCode); + } + + protected void _allowStatusCodes(Integer... statusCodes) { + acceptStatusCodes.addAll(Arrays.asList(statusCodes)); } @Override diff --git a/src/main/java/io/weaviate/client6/v1/internal/rest/SimpleEndpoint.java b/src/main/java/io/weaviate/client6/v1/internal/rest/SimpleEndpoint.java index c9b8d441..b65dc613 100644 --- a/src/main/java/io/weaviate/client6/v1/internal/rest/SimpleEndpoint.java +++ b/src/main/java/io/weaviate/client6/v1/internal/rest/SimpleEndpoint.java @@ -61,6 +61,12 @@ public SimpleEndpoint( this.deserializeResponse = deserializeResponse; } + @SuppressWarnings("unchecked") + public SimpleEndpoint allowStatus(Integer... statusCodes) { + super._allowStatusCodes(statusCodes); + return (SimpleEndpoint) this; + } + @Override public ResponseT deserializeResponse(int statusCode, String responseBody) { return deserializeResponse.apply(statusCode, responseBody); diff --git a/src/main/java/io/weaviate/client6/v1/internal/rest/UrlEncoder.java b/src/main/java/io/weaviate/client6/v1/internal/rest/UrlEncoder.java index b4a9d2b6..2b53f1c8 100644 --- a/src/main/java/io/weaviate/client6/v1/internal/rest/UrlEncoder.java +++ b/src/main/java/io/weaviate/client6/v1/internal/rest/UrlEncoder.java @@ -6,9 +6,9 @@ import java.util.Map; import java.util.stream.Collectors; -final class UrlEncoder { +public final class UrlEncoder { - private static String encodeValue(Object value) { + public static String encodeValue(Object value) { try { return URLEncoder.encode(value.toString(), StandardCharsets.UTF_8.toString()); } catch (UnsupportedEncodingException e) { diff --git a/src/test/java/io/weaviate/client6/v1/internal/json/JSONTest.java b/src/test/java/io/weaviate/client6/v1/internal/json/JSONTest.java index 9a682594..86f4249f 100644 --- a/src/test/java/io/weaviate/client6/v1/internal/json/JSONTest.java +++ b/src/test/java/io/weaviate/client6/v1/internal/json/JSONTest.java @@ -37,6 +37,21 @@ import io.weaviate.client6.v1.api.collections.vectorizers.SelfProvidedVectorizer; import io.weaviate.client6.v1.api.collections.vectorizers.Text2VecContextionaryVectorizer; import io.weaviate.client6.v1.api.collections.vectorizers.Text2VecWeaviateVectorizer; +import io.weaviate.client6.v1.api.rbac.AliasesPermission; +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.NodesPermission.Verbosity; +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.groups.GroupType; /** Unit tests for custom POJO-to-JSON serialization. */ @RunWith(JParamsTestRunner.class) @@ -337,6 +352,7 @@ public static Object[][] testCases() { """, }, + // Generative.CustomTypeAdapterFactory { Generative.class, Generative.cohere(generate -> generate @@ -359,6 +375,8 @@ public static Object[][] testCases() { } """, }, + + // BatchReference.CustomTypeAdapterFactory { BatchReference.class, new BatchReference("FromCollection", "fromProperty", "from-uuid", @@ -370,6 +388,438 @@ public static Object[][] testCases() { } """, }, + + // Role.CustomTypeAdapterFactory & Permission.CustomTypeAdapterFactory + { + Role.class, + new Role( + "rock-n-role", + List.of( + new AliasesPermission( + "CollectionAlias", + "Collection", + List.of( + AliasesPermission.Action.CREATE, + AliasesPermission.Action.READ, + AliasesPermission.Action.UPDATE, + AliasesPermission.Action.DELETE)))), + """ + { + "name": "rock-n-role", + "permissions": [ + { + "action": "create_aliases", + "aliases": { + "alias": "CollectionAlias", + "collection": "Collection" + } + }, + { + "action": "read_aliases", + "aliases": { + "alias": "CollectionAlias", + "collection": "Collection" + } + }, + { + "action": "update_aliases", + "aliases": { + "alias": "CollectionAlias", + "collection": "Collection" + } + }, + { + "action": "delete_aliases", + "aliases": { + "alias": "CollectionAlias", + "collection": "Collection" + } + } + ] + } + """ + }, + { + Role.class, + new Role( + "rock-n-role", + List.of( + new BackupsPermission( + "Collection", + List.of(BackupsPermission.Action.MANAGE)))), + """ + { + "name": "rock-n-role", + "permissions": [ + { + "action": "manage_backups", + "backups": { + "collection": "Collection" + } + } + ] + } + """ + }, + { + Role.class, + new Role( + "rock-n-role", + List.of( + new ClusterPermission( + List.of(ClusterPermission.Action.READ)))), + """ + { + "name": "rock-n-role", + "permissions": [ + { "action": "read_cluster" } + ] + } + """ + }, + { + Role.class, + new Role( + "rock-n-role", + List.of( + new CollectionsPermission( + "Collection", + List.of( + CollectionsPermission.Action.CREATE, + CollectionsPermission.Action.READ, + CollectionsPermission.Action.UPDATE, + CollectionsPermission.Action.DELETE)))), + """ + { + "name": "rock-n-role", + "permissions": [ + { + "action": "create_collections", + "collections": { + "collection": "Collection" + } + }, + { + "action": "read_collections", + "collections": { + "collection": "Collection" + } + }, + { + "action": "update_collections", + "collections": { + "collection": "Collection" + } + }, + { + "action": "delete_collections", + "collections": { + "collection": "Collection" + } + } + ] + } + """ + }, + { + Role.class, + new Role( + "rock-n-role", + List.of( + new DataPermission( + "Collection", + List.of( + DataPermission.Action.CREATE, + DataPermission.Action.READ, + DataPermission.Action.UPDATE, + DataPermission.Action.DELETE)))), + """ + { + "name": "rock-n-role", + "permissions": [ + { + "action": "create_data", + "data": { + "collection": "Collection" + } + }, + { + "action": "read_data", + "data": { + "collection": "Collection" + } + }, + { + "action": "update_data", + "data": { + "collection": "Collection" + } + }, + { + "action": "delete_data", + "data": { + "collection": "Collection" + } + } + ] + } + """ + }, + { + Role.class, + new Role( + "rock-n-role", + List.of( + new GroupsPermission( + "friend-group", + GroupType.OIDC, + List.of( + GroupsPermission.Action.READ, + GroupsPermission.Action.ASSIGN_AND_REVOKE)))), + """ + { + "name": "rock-n-role", + "permissions": [ + { + "action": "read_groups", + "groups": { + "group": "friend-group", + "groupType": "oidc" + } + }, + { + "action": "assign_and_revoke_groups", + "groups": { + "group": "friend-group", + "groupType": "oidc" + } + } + ] + } + """ + }, + { + Role.class, + new Role( + "rock-n-role", + List.of( + new NodesPermission( + "Collection", + Verbosity.MINIMAL, + List.of(NodesPermission.Action.READ)))), + """ + { + "name": "rock-n-role", + "permissions": [ + { + "action": "read_nodes", + "nodes": { + "collection": "Collection", + "verbosity": "minimal" + } + } + ] + } + """ + }, + { + Role.class, + new Role( + "rock-n-role", + List.of( + new ReplicatePermission( + "Collection", + "shard-123", + List.of( + ReplicatePermission.Action.CREATE, + ReplicatePermission.Action.READ, + ReplicatePermission.Action.UPDATE, + ReplicatePermission.Action.DELETE)))), + """ + { + "name": "rock-n-role", + "permissions": [ + { + "action": "create_replicate", + "replicate": { + "collection": "Collection", + "shard": "shard-123" + } + }, + { + "action": "read_replicate", + "replicate": { + "collection": "Collection", + "shard": "shard-123" + } + }, + { + "action": "update_replicate", + "replicate": { + "collection": "Collection", + "shard": "shard-123" + } + }, + { + "action": "delete_replicate", + "replicate": { + "collection": "Collection", + "shard": "shard-123" + } + } + ] + } + """ + }, + { + Role.class, + new Role( + "rock-n-role", + List.of( + new RolesPermission( + "rock-n-role", + Scope.MATCH, + List.of( + RolesPermission.Action.CREATE, + RolesPermission.Action.READ, + RolesPermission.Action.UPDATE, + RolesPermission.Action.DELETE)))), + """ + { + "name": "rock-n-role", + "permissions": [ + { + "action": "create_roles", + "roles": { + "role": "rock-n-role", + "scope": "match" + } + }, + { + "action": "read_roles", + "roles": { + "role": "rock-n-role", + "scope": "match" + } + }, + { + "action": "update_roles", + "roles": { + "role": "rock-n-role", + "scope": "match" + } + }, + { + "action": "delete_roles", + "roles": { + "role": "rock-n-role", + "scope": "match" + } + } + ] + } + """ + }, + { + Role.class, + new Role( + "rock-n-role", + List.of( + new TenantsPermission( + "Collection", + "TeenageMutenantNinjaTurtles", + List.of( + TenantsPermission.Action.CREATE, + TenantsPermission.Action.READ, + TenantsPermission.Action.UPDATE, + TenantsPermission.Action.DELETE)))), + """ + { + "name": "rock-n-role", + "permissions": [ + { + "action": "create_tenants", + "tenants": { + "collection": "Collection", + "tenant": "TeenageMutenantNinjaTurtles" + } + }, + { + "action": "read_tenants", + "tenants": { + "collection": "Collection", + "tenant": "TeenageMutenantNinjaTurtles" + } + }, + { + "action": "update_tenants", + "tenants": { + "collection": "Collection", + "tenant": "TeenageMutenantNinjaTurtles" + } + }, + { + "action": "delete_tenants", + "tenants": { + "collection": "Collection", + "tenant": "TeenageMutenantNinjaTurtles" + } + } + ] + } + """ + }, + { + Role.class, + new Role( + "rock-n-role", + List.of( + new UsersPermission( + "john-doe", + List.of( + UsersPermission.Action.CREATE, + UsersPermission.Action.READ, + UsersPermission.Action.UPDATE, + UsersPermission.Action.DELETE, + UsersPermission.Action.ASSIGN_AND_REVOKE)))), + """ + { + "name": "rock-n-role", + "permissions": [ + { + "action": "create_users", + "users": { + "users": "john-doe" + } + }, + { + "action": "read_users", + "users": { + "users": "john-doe" + } + }, + { + "action": "update_users", + "users": { + "users": "john-doe" + } + }, + { + "action": "delete_users", + "users": { + "users": "john-doe" + } + }, + { + "action": "assign_and_revoke_users", + "users": { + "users": "john-doe" + } + } + ] + } + """ + }, }; }