From 40d9aa49504917ffba02d4684deece0f2d9af8af Mon Sep 17 00:00:00 2001 From: Maria Ines Parnisari Date: Tue, 26 Aug 2025 14:47:58 -0700 Subject: [PATCH 1/4] add code snippet for Watch API --- README.md | 40 ++++++------ examples/v1/{App.java => CallingCheck.java} | 0 examples/v1/CallingWatch.java | 72 +++++++++++++++++++++ 3 files changed, 90 insertions(+), 22 deletions(-) rename examples/v1/{App.java => CallingCheck.java} (100%) create mode 100644 examples/v1/CallingWatch.java diff --git a/README.md b/README.md index 83337646..ac2bfc75 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,10 @@ [![Discord Server](https://img.shields.io/discord/844600078504951838?color=7289da&logo=discord "Discord Server")](https://discord.gg/jTysUaxXzM) [![Twitter](https://img.shields.io/twitter/follow/authzed?color=%23179CF0&logo=twitter&style=flat-square)](https://twitter.com/authzed) -This repository houses the Java client library for [SpiceDB]. - -[SpiceDB] is a database and service that stores, computes, and validates your application's permissions. +This repository houses the Java client library for [SpiceDB], a database and service that can store your application's permissions. Developers create a schema that models their permissions requirements and use a client library, such as this one, to apply the schema to the database, insert data into the database, and query the data to efficiently check permissions in their applications. -Supported client API versions: -- [v1](https://authzed.com/docs/reference/api#authzedapiv1) - You can find more info on each API on the [SpiceDB API reference documentation]. Additionally, Protobuf API documentation can be found on the [Buf Registry SpiceDB API repository]. Documentation for the latest Java client release is available as [Javadoc]. @@ -23,21 +18,16 @@ See [CONTRIBUTING.md] for instructions on contributing and performing common tas [Authzed]: https://authzed.com [SpiceDB]: https://github.com/authzed/spicedb -[SpiceDB API Reference documentation]: https://authzed.com/docs/reference/api +[SpiceDB API Reference documentation]: https://authzed.com/docs/spicedb/api/http-api [Buf Registry SpiceDB API repository]: https://buf.build/authzed/api/docs/main [CONTRIBUTING.md]: CONTRIBUTING.md [Javadoc]: https://authzed.github.io/authzed-java/index.html ## Getting Started -We highly recommend following the **[Protecting Your First App]** guide to learn the latest best practice to integrate an application with SpiceDB. - -If you're interested in examples for a specific API version, they can be found in their respective folders in the [examples directory]. +We highly recommend following the **[Protecting Your First App]** guide to learn the latest best practices to integrate an application with SpiceDB. [Protecting Your First App]: https://authzed.com/docs/guides/first-app -[examples directory]: /examples - -## Basic Usage ### Installation @@ -70,7 +60,7 @@ If you are using [Gradle] then add the following to your `build.gradle` file: ```groovy dependencies { - implementation "com.authzed.api:authzed:v1.0.0" + implementation "com.authzed.api:authzed:v1.3.0" implementation 'io.grpc:grpc-api:1.72.0' implementation 'io.grpc:grpc-stub:1.72.0' } @@ -83,11 +73,6 @@ dependencies { ### Initializing a client -Because of how [grpc-java] is designed, there is little in terms of abstraction over the gRPC APIs underpinning Authzed. -A `ManagedChannel` will establish a connection to Authzed that can be shared with _stubs_ for each gRPC service. -To successfully authenticate with the API, you will have to provide a [Bearer Token] with your own API Token -from the [Authzed dashboard] or your local SpiceDB instance in place of `t_your_token_here_1234567deadbeef` as -`CallCredentials` for each stub: ```java package org.example; @@ -99,12 +84,16 @@ import io.grpc.ManagedChannelBuilder; public class PermissionServiceExample { public static void main(String[] args) { + // establish a connection to Authzed ManagedChannel channel = ManagedChannelBuilder .forTarget("grpc.authzed.com:443") .useTransportSecurity() .build(); + // get the bearer token from your Authzed dashboard (or from https://app.authzed.cloud) BearerToken bearerToken = new BearerToken("t_your_token_here_1234567deadbeef"); + + // create the client PermissionsServiceGrpc.PermissionsServiceBlockingStub permissionsService = PermissionsServiceGrpc .newBlockingStub(channel) .withCallCredentials(bearerToken); @@ -123,16 +112,16 @@ ManagedChannel channel = ManagedChannelBuilder [grpc-java]: https://github.com/grpc/grpc-java [Bearer Token]: https://authzed.com/docs/reference/api#authentication -[Authzed dashboard]: https://app.authzed.com/ +[Authzed dashboard]: https://app.authzed.cloud/ -### Performing an API call +### Calling `CheckPermission` Request and Response types are located in their respective gRPC Service packages and common types can be found in the Core package. Referring to the [Authzed ProtoBuf Documentation] is useful for discovering these APIs. Because of the verbosity of these types, we recommend writing your own functions/methods to create these types from your existing application's models. -The following example initializes a permission client, performs a `CheckPermission` call and prints the result +The following example initializes a permission client, performs a `CheckPermission` call and prints the result. [Authzed Protobuf Documentation]: https://buf.build/authzed/api/docs/main @@ -188,3 +177,10 @@ public class ClientExample { } } ``` + + +### More examples + +See the [examples directory] for more code snippets. + +[examples directory]: /examples \ No newline at end of file diff --git a/examples/v1/App.java b/examples/v1/CallingCheck.java similarity index 100% rename from examples/v1/App.java rename to examples/v1/CallingCheck.java diff --git a/examples/v1/CallingWatch.java b/examples/v1/CallingWatch.java new file mode 100644 index 00000000..f173c772 --- /dev/null +++ b/examples/v1/CallingWatch.java @@ -0,0 +1,72 @@ +/* + * Authzed API examples + */ +package v1; + +import com.authzed.api.v1.*; +import com.authzed.grpcutil.BearerToken; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import java.util.Iterator; + +// Installation +// https://search.maven.org/artifact/com.authzed.api/authzed + +public class App { + private static final Logger logger = Logger.getLogger(App.class.getName()); + private static final String target = "grpc.authzed.com:443"; + private static final String token = "tc_test_def_token"; + + private final SchemaServiceGrpc.SchemaServiceBlockingStub schemaService; + private final PermissionsServiceGrpc.PermissionsServiceBlockingStub permissionsService; + + public App(Channel channel) { + BearerToken bearerToken = new BearerToken(token); + WatchServiceGrpc.WatchServiceBlockingStub watchClient = WatchServiceGrpc + .newBlockingStub(channel) + .withCallCredentials(bearerToken); + } + + public static void main(String[] args) { + ManagedChannel channel = ManagedChannelBuilder + .forTarget(target) + .useTransportSecurity() // if not using TLS, replace with .usePlaintext() + .build(); + + ZedToken lastZedToken = ZedToken.newBuilder().setToken("").build(); + + while(true) { + try { + WatchRequest.Builder builder = WatchRequest.newBuilder(); + + if (!lastZedToken.getToken().isEmpty()) { + builder.setOptionalStartCursor(lastZedToken); + } + + WatchRequest request = builder.build(); + + Iterator watchStream = watchClient.watch(request); + + while (watchStream.hasNext()) { + WatchResponse msg = watchStream.next(); + System.out.println("Received watch response: " + msg); + + if (!msg.getChangesThrough().getToken().isEmpty()) { + lastZedToken = msg.getChangesThrough(); + } + } + + } catch (Exception e) { + if (e instanceof StatusRuntimeException sre && (sre.getStatus().getCode().equals(Status.UNAVAILABLE.getCode()) || + sre.getStatus().getCode().equals(Status.INTERNAL.getCode()))) { + // Stream probably got disconnected after inactivity. Retry + } else { + System.out.println("Error calling watch: " + e.getMessage()); + return; + } + } + } + } +} From ac9c71a0be1b4ddff74ba2d03d322a128a1c002e Mon Sep 17 00:00:00 2001 From: Maria Ines Parnisari Date: Wed, 27 Aug 2025 12:03:02 -0700 Subject: [PATCH 2/4] use grpc retries instead of hand-written retries --- examples/v1/CallingWatch.java | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/examples/v1/CallingWatch.java b/examples/v1/CallingWatch.java index f173c772..6e49cf19 100644 --- a/examples/v1/CallingWatch.java +++ b/examples/v1/CallingWatch.java @@ -10,6 +10,8 @@ import io.grpc.Status; import io.grpc.StatusRuntimeException; import java.util.Iterator; +import java.util.List; +import java.util.Map; // Installation // https://search.maven.org/artifact/com.authzed.api/authzed @@ -33,6 +35,26 @@ public static void main(String[] args) { ManagedChannel channel = ManagedChannelBuilder .forTarget(target) .useTransportSecurity() // if not using TLS, replace with .usePlaintext() + .disableServiceConfigLookUp() + .defaultServiceConfig(Map.of( + "methodConfig", List.of( + Map.of( + "name", List.of( + Map.of( + "service", "authzed.api.v1.WatchService", + "method", "Watch" + ) + ), + "retryPolicy", Map.of( + "maxAttempts", "5", + "initialBackoff", "1s", + "backoffMultiplier", "4.0", + "maxBackoff", "30s", + "retryableStatusCodes", List.of("UNAVAILABLE", "INTERNAL") + ) + ) + ) + )) .build(); ZedToken lastZedToken = ZedToken.newBuilder().setToken("").build(); From f2394826e4a2f95b716e2a7899739425bd958ae8 Mon Sep 17 00:00:00 2001 From: Maria Ines Parnisari Date: Wed, 27 Aug 2025 16:32:36 -0700 Subject: [PATCH 3/4] don't use retries, include checkpoints --- README.md | 2 +- examples/v1/CallingWatch.java | 62 ++++++++--------------------------- 2 files changed, 15 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index ac2bfc75..66b09374 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ If you are using [Gradle] then add the following to your `build.gradle` file: ```groovy dependencies { - implementation "com.authzed.api:authzed:v1.3.0" + implementation "com.authzed.api:authzed:1.4.1" implementation 'io.grpc:grpc-api:1.72.0' implementation 'io.grpc:grpc-stub:1.72.0' } diff --git a/examples/v1/CallingWatch.java b/examples/v1/CallingWatch.java index 6e49cf19..2ea00a2e 100644 --- a/examples/v1/CallingWatch.java +++ b/examples/v1/CallingWatch.java @@ -10,8 +10,6 @@ import io.grpc.Status; import io.grpc.StatusRuntimeException; import java.util.Iterator; -import java.util.List; -import java.util.Map; // Installation // https://search.maven.org/artifact/com.authzed.api/authzed @@ -35,60 +33,28 @@ public static void main(String[] args) { ManagedChannel channel = ManagedChannelBuilder .forTarget(target) .useTransportSecurity() // if not using TLS, replace with .usePlaintext() - .disableServiceConfigLookUp() - .defaultServiceConfig(Map.of( - "methodConfig", List.of( - Map.of( - "name", List.of( - Map.of( - "service", "authzed.api.v1.WatchService", - "method", "Watch" - ) - ), - "retryPolicy", Map.of( - "maxAttempts", "5", - "initialBackoff", "1s", - "backoffMultiplier", "4.0", - "maxBackoff", "30s", - "retryableStatusCodes", List.of("UNAVAILABLE", "INTERNAL") - ) - ) - ) - )) .build(); - ZedToken lastZedToken = ZedToken.newBuilder().setToken("").build(); + try { + WatchRequest request = WatchRequest. + newBuilder() + .addOptionalUpdateKinds(com.authzed.api.v1.WatchKind.WATCH_KIND_INCLUDE_CHECKPOINTS) + .build(); - while(true) { - try { - WatchRequest.Builder builder = WatchRequest.newBuilder(); + Iterator watchStream = watchClient.watch(request); - if (!lastZedToken.getToken().isEmpty()) { - builder.setOptionalStartCursor(lastZedToken); - } - - WatchRequest request = builder.build(); - - Iterator watchStream = watchClient.watch(request); - - while (watchStream.hasNext()) { - WatchResponse msg = watchStream.next(); - System.out.println("Received watch response: " + msg); - - if (!msg.getChangesThrough().getToken().isEmpty()) { - lastZedToken = msg.getChangesThrough(); + while (watchStream.hasNext()) { + WatchResponse msg = watchStream.next(); + if (msg.getUpdatesCount() > 0) { + for (var update : msg.getUpdatesList()) { + System.out.println("Received update: " + update); } - } - - } catch (Exception e) { - if (e instanceof StatusRuntimeException sre && (sre.getStatus().getCode().equals(Status.UNAVAILABLE.getCode()) || - sre.getStatus().getCode().equals(Status.INTERNAL.getCode()))) { - // Stream probably got disconnected after inactivity. Retry } else { - System.out.println("Error calling watch: " + e.getMessage()); - return; + System.out.println("No changes made in SpiceDB"); } } + } catch (Exception e) { + System.out.println("Error calling watch: " + e.getMessage()); } } } From 599bdf2291d34e6b0688815e1ddaff6e6ac31136 Mon Sep 17 00:00:00 2001 From: Maria Ines Parnisari Date: Thu, 28 Aug 2025 08:37:28 -0700 Subject: [PATCH 4/4] no retry config, retry on specific errors --- examples/v1/CallingWatch.java | 53 ++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/examples/v1/CallingWatch.java b/examples/v1/CallingWatch.java index 2ea00a2e..d172b5fb 100644 --- a/examples/v1/CallingWatch.java +++ b/examples/v1/CallingWatch.java @@ -35,26 +35,47 @@ public static void main(String[] args) { .useTransportSecurity() // if not using TLS, replace with .usePlaintext() .build(); - try { - WatchRequest request = WatchRequest. - newBuilder() - .addOptionalUpdateKinds(com.authzed.api.v1.WatchKind.WATCH_KIND_INCLUDE_CHECKPOINTS) - .build(); - - Iterator watchStream = watchClient.watch(request); - - while (watchStream.hasNext()) { - WatchResponse msg = watchStream.next(); - if (msg.getUpdatesCount() > 0) { - for (var update : msg.getUpdatesList()) { - System.out.println("Received update: " + update); + ZedToken lastZedToken = ZedToken.newBuilder().setToken("").build(); + + while(true) { + try { + WatchRequest.Builder builder = WatchRequest.newBuilder() + .addOptionalUpdateKinds(com.authzed.api.v1.WatchKind.WATCH_KIND_INCLUDE_CHECKPOINTS); + + if (!lastZedToken.getToken().isEmpty()) { + System.out.println("Resuming watch from token: " + lastZedToken.getToken()); + builder.setOptionalStartCursor(lastZedToken); + } + + WatchRequest request = builder.build(); + + Iterator watchStream = watchClient.watch(request); + + while (watchStream.hasNext()) { + WatchResponse msg = watchStream.next(); + + if (msg.getUpdatesCount() > 0) { + for (var update : msg.getUpdatesList()) { + System.out.println("Received update: " + update); + } + } else { + System.out.println("No changes made in SpiceDB"); + } + + if (!msg.getChangesThrough().getToken().isEmpty()) { + lastZedToken = msg.getChangesThrough(); } + } + + } catch (Exception e) { + if (e instanceof StatusRuntimeException sre && (sre.getStatus().getCode().equals(Status.UNAVAILABLE.getCode()) || + (sre.getStatus().getCode().equals(Status.INTERNAL.getCode())) && sre.getMessage().contains("stream timeout"))) { + // Probably a server restart. Retry. } else { - System.out.println("No changes made in SpiceDB"); + System.out.println("Error calling watch: " + e.getMessage()); + return; } } - } catch (Exception e) { - System.out.println("Error calling watch: " + e.getMessage()); } } }