diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 8d846888..56a1de23 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -61,4 +61,4 @@ By submitting an issue to this repository, you agree to the terms within the [Op > A clear and concise description of what you expected to happen. ### Additional context -> Add any other context about the problem here. \ No newline at end of file +> Add any other context about the problem here. diff --git a/.openapi-generator/FILES b/.openapi-generator/FILES index c0db22a1..dfec80fc 100644 --- a/.openapi-generator/FILES +++ b/.openapi-generator/FILES @@ -81,6 +81,18 @@ docs/WriteAuthorizationModelResponse.md docs/WriteRequest.md docs/WriteRequestDeletes.md docs/WriteRequestWrites.md +example/Makefile +example/README.md +example/example1/README.md +example/example1/build.gradle +example/example1/gradle.properties +example/example1/gradle/wrapper/gradle-wrapper.jar +example/example1/gradle/wrapper/gradle-wrapper.properties +example/example1/gradlew +example/example1/settings.gradle +example/example1/src/main/java/dev/openfga/sdk/example/Example1.java +example/example1/src/main/kotlin/dev/openfga/sdk/example/KotlinExample1.kt +example/example1/src/main/resources/example1-auth-model.json gradle.properties gradle/wrapper/gradle-wrapper.jar gradle/wrapper/gradle-wrapper.properties @@ -235,8 +247,11 @@ src/main/java/dev/openfga/sdk/util/StringUtil.java src/main/java/dev/openfga/sdk/util/Validation.java src/test-integration/java/dev/openfga/sdk/api/OpenFgaApiIntegrationTest.java src/test-integration/java/dev/openfga/sdk/api/client/OpenFgaClientIntegrationTest.java +src/test-integration/java/dev/openfga/sdk/example/Example1.java +src/test-integration/java/dev/openfga/sdk/example/ExampleTest.java src/test-integration/java/package-info.java src/test-integration/resources/auth-model.json +src/test-integration/resources/example1-auth-model.json src/test/java/dev/openfga/sdk/api/OpenFgaApiTest.java src/test/java/dev/openfga/sdk/api/OpenFgaApiTest.java src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java diff --git a/docs/GetStoreResponse.md b/docs/GetStoreResponse.md index 2e7bd479..362f334a 100644 --- a/docs/GetStoreResponse.md +++ b/docs/GetStoreResponse.md @@ -11,6 +11,7 @@ |**name** | **String** | | | |**createdAt** | **OffsetDateTime** | | | |**updatedAt** | **OffsetDateTime** | | | +|**deletedAt** | **OffsetDateTime** | | [optional] | diff --git a/docs/Store.md b/docs/Store.md index 5db4816f..4b8ebe9f 100644 --- a/docs/Store.md +++ b/docs/Store.md @@ -11,7 +11,7 @@ |**name** | **String** | | | |**createdAt** | **OffsetDateTime** | | | |**updatedAt** | **OffsetDateTime** | | | -|**deletedAt** | **OffsetDateTime** | | | +|**deletedAt** | **OffsetDateTime** | | [optional] | diff --git a/example/Makefile b/example/Makefile new file mode 100644 index 00000000..c5a01e32 --- /dev/null +++ b/example/Makefile @@ -0,0 +1,17 @@ +all: build + +project_name=example1 +openfga_version=latest +language=java + +build: + cd "${project_name}" && \ + ./gradlew -P language=$(language) build + +run: + cd "${project_name}" && \ + ./gradlew -P language=$(language) run + +run-openfga: + docker pull docker.io/openfga/openfga:${openfga_version} && \ + docker run -p 8080:8080 docker.io/openfga/openfga:${openfga_version} run diff --git a/example/README.md b/example/README.md new file mode 100644 index 00000000..023c55c3 --- /dev/null +++ b/example/README.md @@ -0,0 +1,49 @@ +## Examples of using the OpenFGA Java SDK + +A set of Examples on how to call the OpenFGA Java SDK + +### Examples +Example 1: +A bare-bones example. It creates a store, and runs a set of calls against it including creating a model, writing tuples and checking for access. +This example is implemented in both Java and Kotlin. + + +### Running the Examples + +Prerequisites: +- `docker` +- `make` +- A Java Runtime Environment (JRE) + +#### Run using a published SDK + +Steps +1. Clone/Copy the example folder +2. Run `make` to build the project +3. If you have an OpenFGA server running, you can use it, otherwise run `make run-openfga` to spin up an instance (you'll need to switch to a different terminal after - don't forget to close it when done) +4. Run `make run` to run the example + +#### Run using a local unpublished SDK build + +Steps +1. Build the SDK +2. In the Example project file (e.g. `build.gradle`), comment out the part that specifies the remote SDK, e.g. +```groovy +dependencies { + implementation("dev.openfga:openfga-sdk:0.3.+") + + // ...etc +} +``` +and replace it with one pointing to the local gradle project, e.g. +```groovy +dependencies { + // implementation("dev.openfga:openfga-sdk:0.3.+") + implementation project(path: ':') + + // ...etc +} +``` +3. Run `make` to build the project +4. If you have an OpenFGA server running, you can use it, otherwise run `make run-openfga` to spin up an instance (you'll need to switch to a different terminal after - don't forget to close it when done) +5. Run `make run` to run the example diff --git a/example/example1/README.md b/example/example1/README.md new file mode 100644 index 00000000..b4f70a4e --- /dev/null +++ b/example/example1/README.md @@ -0,0 +1,50 @@ +## Examples of using the OpenFGA Java SDK + +A set of Examples on how to call the OpenFGA Java SDK + +### Examples +Example 1: +A bare-bones example. It creates a store, and runs a set of calls against it including creating a model, writing tuples and checking for access. +This example is implemented in both Java and Kotlin. + + +### Running the Examples + +Prerequisites: +- `docker` +- `make` +- A Java Runtime Environment (JRE) + +#### Run using a published SDK + +Steps +1. Clone/Copy the example folder +2. Run `make` to build the project +3. If you have an OpenFGA server running, you can use it, otherwise run `make run-openfga` to spin up an instance (you'll need to switch to a different terminal after - don't forget to close it when done) +4. Run `make run` to run the example + * This should run a Java example by default. Where implemented, it's possible to specify an alternate JVM language too, like `make run language=kotlin`. + +#### Run using a local unpublished SDK build + +Steps +1. Build the SDK +2. In the Example project file (e.g. `build.gradle`), comment out the part that specifies the remote SDK, e.g. +```groovy +dependencies { + implementation("dev.openfga:openfga-sdk:0.3.+") + + // ...etc +} +``` +and replace it with one pointing to the local gradle project, e.g. +```groovy +dependencies { + // implementation("dev.openfga:openfga-sdk:0.3.+") + implementation project(path: ':') + + // ...etc +} +``` +3. Run `make` to build the project +4. If you have an OpenFGA server running, you can use it, otherwise run `make run-openfga` to spin up an instance (you'll need to switch to a different terminal after - don't forget to close it when done) +5. Run `make run` to run the example diff --git a/example/example1/build.gradle b/example/example1/build.gradle new file mode 100644 index 00000000..793cd3ee --- /dev/null +++ b/example/example1/build.gradle @@ -0,0 +1,77 @@ +plugins { + id 'application' + id 'com.diffplug.spotless' version '6.23.3' + id 'org.jetbrains.kotlin.jvm' version '2.0.0-Beta2' +} + +application { + switch (language) { + case 'kotlin': + mainClass = 'dev.openfga.sdk.example.KotlinExample1' + break + default: + mainClass = 'dev.openfga.sdk.example.Example1' + } +} + +repositories { + mavenCentral() +} + +ext { + jacksonVersion = "2.16.0" +} + +dependencies { + implementation("dev.openfga:openfga-sdk:0.3.+") + + // Serialization + implementation("com.fasterxml.jackson.core:jackson-core:$jacksonVersion") + implementation("com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion") + implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion") + implementation("org.openapitools:jackson-databind-nullable:0.2.+") + + // Kotlin + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" +} + +// Use spotless plugin to automatically format code, remove unused import, etc +// To apply changes directly to the file, run `gradlew spotlessApply` +// Ref: https://github.com/diffplug/spotless/tree/main/plugin-gradle +spotless { + // comment out below to run spotless as part of the `check` task + enforceCheck false + format 'misc', { + // define the files (e.g. '*.gradle', '*.md') to apply `misc` to + target '.gitignore' + // define the steps to apply to those files + trimTrailingWhitespace() + indentWithSpaces() // Takes an integer argument if you don't like 4 + endWithNewline() + } + java { + palantirJavaFormat() + removeUnusedImports() + importOrder() + } +} + +// Use spotless plugin to automatically format code, remove unused import, etc +// To apply changes directly to the file, run `gradlew spotlessApply` +// Ref: https://github.com/diffplug/spotless/tree/main/plugin-gradle +tasks.register('fmt') { + dependsOn 'spotlessApply' +} + +compileKotlin { + kotlinOptions { + jvmTarget = "17" + } +} + +compileTestKotlin { + kotlinOptions { + jvmTarget = "17" + } +} diff --git a/example/example1/gradle.properties b/example/example1/gradle.properties new file mode 100644 index 00000000..5f544a8e --- /dev/null +++ b/example/example1/gradle.properties @@ -0,0 +1 @@ +language=java \ No newline at end of file diff --git a/example/example1/gradle/wrapper/gradle-wrapper.jar b/example/example1/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..ccebba77 Binary files /dev/null and b/example/example1/gradle/wrapper/gradle-wrapper.jar differ diff --git a/example/example1/gradle/wrapper/gradle-wrapper.properties b/example/example1/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..42defcc9 --- /dev/null +++ b/example/example1/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/example/example1/gradlew b/example/example1/gradlew new file mode 100644 index 00000000..79a61d42 --- /dev/null +++ b/example/example1/gradlew @@ -0,0 +1,244 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/example/example1/settings.gradle b/example/example1/settings.gradle new file mode 100644 index 00000000..ff41744a --- /dev/null +++ b/example/example1/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'example1' \ No newline at end of file diff --git a/example/example1/src/main/java/dev/openfga/sdk/example/Example1.java b/example/example1/src/main/java/dev/openfga/sdk/example/Example1.java new file mode 100644 index 00000000..4a5b1ee3 --- /dev/null +++ b/example/example1/src/main/java/dev/openfga/sdk/example/Example1.java @@ -0,0 +1,199 @@ +package dev.openfga.sdk.example; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.openfga.sdk.api.client.ClientAssertion; +import dev.openfga.sdk.api.client.OpenFgaClient; +import dev.openfga.sdk.api.client.model.*; +import dev.openfga.sdk.api.configuration.*; +import dev.openfga.sdk.api.model.*; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; + +class Example1 { + public void run() throws Exception { + var credentials = new Credentials(); + if (System.getenv("FGA_CLIENT_ID") != null) { + credentials = new Credentials(new ClientCredentials() + .apiAudience(System.getenv("FGA_API_AUDIENCE")) + .apiTokenIssuer(System.getenv("FGA_TOKEN_ISSUER")) + .clientId("FGA_CLIENT_ID") + .clientSecret("FGA_CLIENT_SECRET")); + } else { + System.out.println("Proceeding with no credentials (expecting localhost)"); + } + + var configuration = new ClientConfiguration() + .apiUrl(System.getenv("FGA_API_URL")) // required, e.g. https://api.fga.example + .storeId(System.getenv("FGA_STORE_ID")) // not needed when calling `CreateStore` or `ListStores` + .authorizationModelId( + System.getenv("FGA_AUTHORIZATION_MODEL_ID")) // Optional, can be overridden per request + .credentials(credentials); + var fgaClient = new OpenFgaClient(configuration); + + // ListStores + System.out.println("Listing Stores"); + var stores1 = fgaClient.listStores().get(); + System.out.println("Stores Count: " + stores1.getStores().size()); + + // CreateStore + System.out.println("Creating Test Store"); + var store = fgaClient + .createStore(new CreateStoreRequest().name("Test Store")) + .get(); + System.out.println("Test Store ID: " + store.getId()); + + // Set the store id + fgaClient.setStoreId(store.getId()); + + // ListStores after Create + System.out.println("Listing Stores"); + var stores = fgaClient.listStores().get(); + System.out.println("Stores Count: " + stores.getStores().size()); + + // GetStore + System.out.println("Getting Current Store"); + var currentStore = fgaClient.getStore().get(); + System.out.println("Current Store Name: " + currentStore.getName()); + + // ReadAuthorizationModels + System.out.println("Reading Authorization Models"); + var models = fgaClient.readAuthorizationModels().get(); + System.out.println("Models Count: " + models.getAuthorizationModels().size()); + + // ReadLatestAuthorizationModel + try { + var latestAuthorizationModel = fgaClient + .readLatestAuthorizationModel() + .get(); // TODO: Should this really return null? Optional<...Response>? + System.out.println("Latest Authorization Model ID " + + latestAuthorizationModel.getAuthorizationModel().getId()); + } catch (Exception e) { + System.out.println("Latest Authorization Model not found"); + } + + var mapper = new ObjectMapper().findAndRegisterModules(); + + // WriteAuthorizationModel + var authModelJson = loadResource("example1-auth-model.json"); + var authorizationModel = fgaClient + .writeAuthorizationModel(mapper.readValue(authModelJson, WriteAuthorizationModelRequest.class)) + .get(); + System.out.println("Authorization Model ID " + authorizationModel.getAuthorizationModelId()); + + // ReadAuthorizationModels - after Write + System.out.println("Reading Authorization Models"); + models = fgaClient.readAuthorizationModels().get(); + System.out.println("Models Count: " + models.getAuthorizationModels().size()); + + // ReadLatestAuthorizationModel - after Write + var latestAuthorizationModel = fgaClient.readLatestAuthorizationModel().get(); + System.out.println("Latest Authorization Model ID " + + latestAuthorizationModel.getAuthorizationModel().getId()); + + // Set the model ID + fgaClient.setAuthorizationModelId( + latestAuthorizationModel.getAuthorizationModel().getId()); + + // Write + System.out.println("Writing Tuples"); + fgaClient + .write( + new ClientWriteRequest() + .writes(List.of(new ClientTupleKey() + .user("user:anne") + .relation("writer") + ._object("document:roadmap"))), + new ClientWriteOptions() + .disableTransactions(true) + .authorizationModelId(authorizationModel.getAuthorizationModelId())) + .get(); + System.out.println("Done Writing Tuples"); + + // Read + System.out.println("Reading Tuples"); + var readTuples = fgaClient.read(new ClientReadRequest()).get(); + System.out.println("Read Tuples" + mapper.writeValueAsString(readTuples)); + + // ReadChanges + System.out.println("Reading Tuple Changess"); + var readChangesTuples = + fgaClient.readChanges(new ClientReadChangesRequest()).get(); + System.out.println("Read Changes Tuples" + mapper.writeValueAsString(readChangesTuples)); + + // Check + System.out.println("Checking for access"); + try { + var failingCheckResponse = fgaClient + .check(new ClientCheckRequest() + .user("user:anne") + .relation("reader") + ._object("document:roadmap")) + .get(); + System.out.println("Allowed: " + failingCheckResponse.getAllowed()); + } catch (Exception e) { + System.out.println("Failed due to: " + e.getMessage()); + } + + // Checking for access with context + // TODO: Add ClientCheckRequest.context + // System.out.println("Checking for access with context"); + // var checkResponse = fgaClient + // .check(new ClientCheckRequest() + // .user("user:anne") + // .relation("reader") + // ._object("document:roadmap") + // .context(Map.of("ViewCount", 100))) + // .get(); + // System.out.println("Allowed: " + checkResponse.getAllowed()); + + // WriteAssertions + fgaClient + .writeAssertions(List.of( + new ClientAssertion() + .user("user:carl") + .relation("writer") + ._object("document:budget") + .expectation(true), + new ClientAssertion() + .user("user:anne") + .relation("reader") + ._object("document:roadmap") + .expectation(false))) + .get(); + System.out.println("Assertions updated"); + + // ReadAssertions + System.out.println("Reading Assertions"); + var assertions = fgaClient.readAssertions().get(); + System.out.println("Assertions " + mapper.writeValueAsString(assertions)); + + // DeleteStore + System.out.println("Deleting Current Store"); + fgaClient.deleteStore().get(); + System.out.println("Deleted Store: " + currentStore.getName()); + } + + public static void main(String[] args) { + System.out.println("=== Example 1 (Java) ==="); + try { + new Example1().run(); + } catch (Exception e) { + System.err.printf("ERROR: %s%n", e); + } + } + + String module = "main"; // Used only for integration testing + + // Small helper function to load resource files relative to this class. + private String loadResource(String filename) { + try { + var filepath = Paths.get("src", module, "resources", filename); + return Files.readString(filepath, StandardCharsets.UTF_8); + } catch (IOException cause) { + throw new RuntimeException("Unable to load resource: " + filename, cause); + } + } +} diff --git a/example/example1/src/main/kotlin/dev/openfga/sdk/example/KotlinExample1.kt b/example/example1/src/main/kotlin/dev/openfga/sdk/example/KotlinExample1.kt new file mode 100644 index 00000000..8dd22de6 --- /dev/null +++ b/example/example1/src/main/kotlin/dev/openfga/sdk/example/KotlinExample1.kt @@ -0,0 +1,210 @@ +package dev.openfga.sdk.example + +import com.fasterxml.jackson.databind.ObjectMapper +import dev.openfga.sdk.api.client.ClientAssertion +import dev.openfga.sdk.api.client.OpenFgaClient +import dev.openfga.sdk.api.client.model.* +import dev.openfga.sdk.api.configuration.ClientConfiguration +import dev.openfga.sdk.api.configuration.ClientCredentials +import dev.openfga.sdk.api.configuration.ClientWriteOptions +import dev.openfga.sdk.api.configuration.Credentials +import dev.openfga.sdk.api.model.CreateStoreRequest +import dev.openfga.sdk.api.model.WriteAuthorizationModelRequest + +internal class KotlinExample1 { + @Throws(Exception::class) + fun run() { + var credentials = Credentials() + if (System.getenv("FGA_CLIENT_ID") != null) { + credentials = Credentials( + ClientCredentials() + .apiAudience(System.getenv("FGA_API_AUDIENCE")) + .apiTokenIssuer(System.getenv("FGA_TOKEN_ISSUER")) + .clientId("FGA_CLIENT_ID") + .clientSecret("FGA_CLIENT_SECRET") + ) + } else { + println("Proceeding with no credentials (expecting localhost)") + } + val configuration = ClientConfiguration() + .apiUrl(System.getenv("FGA_API_URL")) // required, e.g. https://api.fga.example + .storeId(System.getenv("FGA_STORE_ID")) // not needed when calling `CreateStore` or `ListStores` + .authorizationModelId( + System.getenv("FGA_AUTHORIZATION_MODEL_ID") + ) // Optional, can be overridden per request + .credentials(credentials) + val fgaClient = OpenFgaClient(configuration) + + // ListStores + println("Listing Stores") + val stores1 = fgaClient.listStores().get() + println("Stores Count: " + stores1.stores.size) + + // CreateStore + println("Creating Test Store") + val store = fgaClient + .createStore(CreateStoreRequest().name("Test Store")) + .get() + println("Test Store ID: " + store.id) + + // Set the store id + fgaClient.setStoreId(store.id) + + // ListStores after Create + println("Listing Stores") + val stores = fgaClient.listStores().get() + println("Stores Count: " + stores.stores.size) + + // GetStore + println("Getting Current Store") + val currentStore = fgaClient.store.get() + println("Current Store Name: " + currentStore.name) + + // ReadAuthorizationModels + println("Reading Authorization Models") + var models = fgaClient.readAuthorizationModels().get() + println("Models Count: " + models.authorizationModels.size) + + // ReadLatestAuthorizationModel + try { + val latestAuthorizationModel = fgaClient + .readLatestAuthorizationModel() + .get() // TODO: Should this really return null? Optional<...Response>? + println( + "Latest Authorization Model ID " + + latestAuthorizationModel.authorizationModel!!.id + ) + } catch (e: Exception) { + println("Latest Authorization Model not found") + } + val mapper = ObjectMapper().findAndRegisterModules() + + // WriteAuthorizationModel + val authModelJson = loadResource("example1-auth-model.json") + val authorizationModel = fgaClient + .writeAuthorizationModel(mapper.readValue(authModelJson, WriteAuthorizationModelRequest::class.java)) + .get() + println("Authorization Model ID " + authorizationModel.authorizationModelId) + + // ReadAuthorizationModels - after Write + println("Reading Authorization Models") + models = fgaClient.readAuthorizationModels().get() + println("Models Count: " + models.authorizationModels.size) + + // ReadLatestAuthorizationModel - after Write + val latestAuthorizationModel = fgaClient.readLatestAuthorizationModel().get() + println( + "Latest Authorization Model ID " + + latestAuthorizationModel.authorizationModel!!.id + ) + + // Set the model ID + fgaClient.setAuthorizationModelId( + latestAuthorizationModel.authorizationModel!!.id + ) + + // Write + println("Writing Tuples") + fgaClient + .write( + ClientWriteRequest() + .writes( + listOf( + ClientTupleKey() + .user("user:anne") + .relation("writer") + ._object("document:roadmap") + ) + ), + ClientWriteOptions() + .disableTransactions(true) + .authorizationModelId(authorizationModel.authorizationModelId) + ) + .get() + println("Done Writing Tuples") + + // Read + println("Reading Tuples") + val readTuples = fgaClient.read(ClientReadRequest()).get() + println("Read Tuples" + mapper.writeValueAsString(readTuples)) + + // ReadChanges + println("Reading Tuple Changess") + val readChangesTuples = fgaClient.readChanges(ClientReadChangesRequest()).get() + println("Read Changes Tuples" + mapper.writeValueAsString(readChangesTuples)) + + // Check + println("Checking for access") + try { + val failingCheckResponse = fgaClient + .check( + ClientCheckRequest() + .user("user:anne") + .relation("reader") + ._object("document:roadmap") + ) + .get() + println("Allowed: " + failingCheckResponse.allowed) + } catch (e: Exception) { + println("Failed due to: " + e.message) + } + + // Checking for access with context + // TODO: Add ClientCheckRequest.context + // System.out.println("Checking for access with context"); + // var checkResponse = fgaClient + // .check(new ClientCheckRequest() + // .user("user:anne") + // .relation("reader") + // ._object("document:roadmap") + // .context(Map.of("ViewCount", 100))) + // .get(); + // System.out.println("Allowed: " + checkResponse.getAllowed()); + + // WriteAssertions + fgaClient + .writeAssertions( + listOf( + ClientAssertion() + .user("user:carl") + .relation("writer") + ._object("document:budget") + .expectation(true), + ClientAssertion() + .user("user:anne") + .relation("reader") + ._object("document:roadmap") + .expectation(false) + ) + ) + .get() + println("Assertions updated") + + // ReadAssertions + println("Reading Assertions") + val assertions = fgaClient.readAssertions().get() + println("Assertions " + mapper.writeValueAsString(assertions)) + + // DeleteStore + println("Deleting Current Store") + fgaClient.deleteStore().get() + println("Deleted Store: " + currentStore.name) + } + + // Small helper function to load resource files relative to this class. + private fun loadResource(filename: String): String { + return javaClass.module.classLoader.getResource(filename)?.readText()!! + } + + companion object { + @JvmStatic + fun main(args: Array) { + println("=== Example 1 (Kotlin) ===") + try { + KotlinExample1().run() + } catch (e: Exception) { + println("ERROR: $e") + } + } + } +} diff --git a/example/example1/src/main/resources/example1-auth-model.json b/example/example1/src/main/resources/example1-auth-model.json new file mode 100644 index 00000000..e1361656 --- /dev/null +++ b/example/example1/src/main/resources/example1-auth-model.json @@ -0,0 +1,73 @@ +{ + "schema_version": "1.1", + "type_definitions": [ + { + "type": "user" + }, + { + "type": "document", + "relations": { + "reader": { + "this": {} + }, + "writer": { + "this": {} + }, + "owner": { + "this": {} + } + }, + "metadata": { + "relations": { + "reader": { + "directly_related_user_types": [ + { + "type": "user" + } + ] + }, + "writer": { + "directly_related_user_types": [ + { + "type": "user" + } + ] + }, + "owner": { + "directly_related_user_types": [ + { + "type": "user" + } + ] + }, + "conditional_reader": { + "directly_related_user_types": [ + { + "condition": "name_starts_with_a", + "type": "user" + } + ] + } + } + } + } + ], + "conditions": { + "ViewCountLessThan200": { + "name": "ViewCountLessThan200", + "expression": "ViewCount < 200", + "parameters": { + "ViewCount": { + "type_name": "TYPE_NAME_INT" + }, + "Type": { + "type_name": "TYPE_NAME_STRING" + }, + "Name": { + "type_name": "TYPE_NAME_STRING" + } + } + } + } + } + \ No newline at end of file diff --git a/src/main/java/dev/openfga/sdk/api/model/GetStoreResponse.java b/src/main/java/dev/openfga/sdk/api/model/GetStoreResponse.java index af309ada..fb6874c1 100644 --- a/src/main/java/dev/openfga/sdk/api/model/GetStoreResponse.java +++ b/src/main/java/dev/openfga/sdk/api/model/GetStoreResponse.java @@ -28,7 +28,8 @@ GetStoreResponse.JSON_PROPERTY_ID, GetStoreResponse.JSON_PROPERTY_NAME, GetStoreResponse.JSON_PROPERTY_CREATED_AT, - GetStoreResponse.JSON_PROPERTY_UPDATED_AT + GetStoreResponse.JSON_PROPERTY_UPDATED_AT, + GetStoreResponse.JSON_PROPERTY_DELETED_AT }) public class GetStoreResponse { public static final String JSON_PROPERTY_ID = "id"; @@ -43,6 +44,9 @@ public class GetStoreResponse { public static final String JSON_PROPERTY_UPDATED_AT = "updated_at"; private OffsetDateTime updatedAt; + public static final String JSON_PROPERTY_DELETED_AT = "deleted_at"; + private OffsetDateTime deletedAt; + public GetStoreResponse() {} public GetStoreResponse id(String id) { @@ -133,6 +137,28 @@ public void setUpdatedAt(OffsetDateTime updatedAt) { this.updatedAt = updatedAt; } + public GetStoreResponse deletedAt(OffsetDateTime deletedAt) { + this.deletedAt = deletedAt; + return this; + } + + /** + * Get deletedAt + * @return deletedAt + **/ + @javax.annotation.Nullable + @JsonProperty(JSON_PROPERTY_DELETED_AT) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OffsetDateTime getDeletedAt() { + return deletedAt; + } + + @JsonProperty(JSON_PROPERTY_DELETED_AT) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public void setDeletedAt(OffsetDateTime deletedAt) { + this.deletedAt = deletedAt; + } + /** * Return true if this GetStoreResponse object is equal to o. */ @@ -148,12 +174,13 @@ public boolean equals(Object o) { return Objects.equals(this.id, getStoreResponse.id) && Objects.equals(this.name, getStoreResponse.name) && Objects.equals(this.createdAt, getStoreResponse.createdAt) - && Objects.equals(this.updatedAt, getStoreResponse.updatedAt); + && Objects.equals(this.updatedAt, getStoreResponse.updatedAt) + && Objects.equals(this.deletedAt, getStoreResponse.deletedAt); } @Override public int hashCode() { - return Objects.hash(id, name, createdAt, updatedAt); + return Objects.hash(id, name, createdAt, updatedAt, deletedAt); } @Override @@ -164,6 +191,7 @@ public String toString() { sb.append(" name: ").append(toIndentedString(name)).append("\n"); sb.append(" createdAt: ").append(toIndentedString(createdAt)).append("\n"); sb.append(" updatedAt: ").append(toIndentedString(updatedAt)).append("\n"); + sb.append(" deletedAt: ").append(toIndentedString(deletedAt)).append("\n"); sb.append("}"); return sb.toString(); } @@ -251,6 +279,16 @@ public String toUrlQueryString(String prefix) { .replaceAll("\\+", "%20"))); } + // add `deleted_at` to the URL query string + if (getDeletedAt() != null) { + joiner.add(String.format( + "%sdeleted_at%s=%s", + prefix, + suffix, + URLEncoder.encode(String.valueOf(getDeletedAt()), StandardCharsets.UTF_8) + .replaceAll("\\+", "%20"))); + } + return joiner.toString(); } } diff --git a/src/main/java/dev/openfga/sdk/api/model/NullValue.java b/src/main/java/dev/openfga/sdk/api/model/NullValue.java index d72381e6..d2718bf1 100644 --- a/src/main/java/dev/openfga/sdk/api/model/NullValue.java +++ b/src/main/java/dev/openfga/sdk/api/model/NullValue.java @@ -16,7 +16,7 @@ import com.fasterxml.jackson.annotation.JsonValue; /** - * `NullValue` is a singleton enumeration to represent the null value for the `Value` type union. The JSON representation for `NullValue` is JSON `null`. - NULL_VALUE: Null value. + * `NullValue` is a singleton enumeration to represent the null value for the `Value` type union. The JSON representation for `NullValue` is JSON `null`. - NULL_VALUE: Null value. */ public enum NullValue { NULL_VALUE("NULL_VALUE"), diff --git a/src/main/java/dev/openfga/sdk/api/model/Store.java b/src/main/java/dev/openfga/sdk/api/model/Store.java index 380284e4..8a99a7df 100644 --- a/src/main/java/dev/openfga/sdk/api/model/Store.java +++ b/src/main/java/dev/openfga/sdk/api/model/Store.java @@ -146,15 +146,15 @@ public Store deletedAt(OffsetDateTime deletedAt) { * Get deletedAt * @return deletedAt **/ - @javax.annotation.Nonnull + @javax.annotation.Nullable @JsonProperty(JSON_PROPERTY_DELETED_AT) - @JsonInclude(value = JsonInclude.Include.ALWAYS) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) public OffsetDateTime getDeletedAt() { return deletedAt; } @JsonProperty(JSON_PROPERTY_DELETED_AT) - @JsonInclude(value = JsonInclude.Include.ALWAYS) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) public void setDeletedAt(OffsetDateTime deletedAt) { this.deletedAt = deletedAt; } diff --git a/src/test-integration/java/dev/openfga/sdk/example/Example1.java b/src/test-integration/java/dev/openfga/sdk/example/Example1.java new file mode 100644 index 00000000..4a5b1ee3 --- /dev/null +++ b/src/test-integration/java/dev/openfga/sdk/example/Example1.java @@ -0,0 +1,199 @@ +package dev.openfga.sdk.example; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.openfga.sdk.api.client.ClientAssertion; +import dev.openfga.sdk.api.client.OpenFgaClient; +import dev.openfga.sdk.api.client.model.*; +import dev.openfga.sdk.api.configuration.*; +import dev.openfga.sdk.api.model.*; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; + +class Example1 { + public void run() throws Exception { + var credentials = new Credentials(); + if (System.getenv("FGA_CLIENT_ID") != null) { + credentials = new Credentials(new ClientCredentials() + .apiAudience(System.getenv("FGA_API_AUDIENCE")) + .apiTokenIssuer(System.getenv("FGA_TOKEN_ISSUER")) + .clientId("FGA_CLIENT_ID") + .clientSecret("FGA_CLIENT_SECRET")); + } else { + System.out.println("Proceeding with no credentials (expecting localhost)"); + } + + var configuration = new ClientConfiguration() + .apiUrl(System.getenv("FGA_API_URL")) // required, e.g. https://api.fga.example + .storeId(System.getenv("FGA_STORE_ID")) // not needed when calling `CreateStore` or `ListStores` + .authorizationModelId( + System.getenv("FGA_AUTHORIZATION_MODEL_ID")) // Optional, can be overridden per request + .credentials(credentials); + var fgaClient = new OpenFgaClient(configuration); + + // ListStores + System.out.println("Listing Stores"); + var stores1 = fgaClient.listStores().get(); + System.out.println("Stores Count: " + stores1.getStores().size()); + + // CreateStore + System.out.println("Creating Test Store"); + var store = fgaClient + .createStore(new CreateStoreRequest().name("Test Store")) + .get(); + System.out.println("Test Store ID: " + store.getId()); + + // Set the store id + fgaClient.setStoreId(store.getId()); + + // ListStores after Create + System.out.println("Listing Stores"); + var stores = fgaClient.listStores().get(); + System.out.println("Stores Count: " + stores.getStores().size()); + + // GetStore + System.out.println("Getting Current Store"); + var currentStore = fgaClient.getStore().get(); + System.out.println("Current Store Name: " + currentStore.getName()); + + // ReadAuthorizationModels + System.out.println("Reading Authorization Models"); + var models = fgaClient.readAuthorizationModels().get(); + System.out.println("Models Count: " + models.getAuthorizationModels().size()); + + // ReadLatestAuthorizationModel + try { + var latestAuthorizationModel = fgaClient + .readLatestAuthorizationModel() + .get(); // TODO: Should this really return null? Optional<...Response>? + System.out.println("Latest Authorization Model ID " + + latestAuthorizationModel.getAuthorizationModel().getId()); + } catch (Exception e) { + System.out.println("Latest Authorization Model not found"); + } + + var mapper = new ObjectMapper().findAndRegisterModules(); + + // WriteAuthorizationModel + var authModelJson = loadResource("example1-auth-model.json"); + var authorizationModel = fgaClient + .writeAuthorizationModel(mapper.readValue(authModelJson, WriteAuthorizationModelRequest.class)) + .get(); + System.out.println("Authorization Model ID " + authorizationModel.getAuthorizationModelId()); + + // ReadAuthorizationModels - after Write + System.out.println("Reading Authorization Models"); + models = fgaClient.readAuthorizationModels().get(); + System.out.println("Models Count: " + models.getAuthorizationModels().size()); + + // ReadLatestAuthorizationModel - after Write + var latestAuthorizationModel = fgaClient.readLatestAuthorizationModel().get(); + System.out.println("Latest Authorization Model ID " + + latestAuthorizationModel.getAuthorizationModel().getId()); + + // Set the model ID + fgaClient.setAuthorizationModelId( + latestAuthorizationModel.getAuthorizationModel().getId()); + + // Write + System.out.println("Writing Tuples"); + fgaClient + .write( + new ClientWriteRequest() + .writes(List.of(new ClientTupleKey() + .user("user:anne") + .relation("writer") + ._object("document:roadmap"))), + new ClientWriteOptions() + .disableTransactions(true) + .authorizationModelId(authorizationModel.getAuthorizationModelId())) + .get(); + System.out.println("Done Writing Tuples"); + + // Read + System.out.println("Reading Tuples"); + var readTuples = fgaClient.read(new ClientReadRequest()).get(); + System.out.println("Read Tuples" + mapper.writeValueAsString(readTuples)); + + // ReadChanges + System.out.println("Reading Tuple Changess"); + var readChangesTuples = + fgaClient.readChanges(new ClientReadChangesRequest()).get(); + System.out.println("Read Changes Tuples" + mapper.writeValueAsString(readChangesTuples)); + + // Check + System.out.println("Checking for access"); + try { + var failingCheckResponse = fgaClient + .check(new ClientCheckRequest() + .user("user:anne") + .relation("reader") + ._object("document:roadmap")) + .get(); + System.out.println("Allowed: " + failingCheckResponse.getAllowed()); + } catch (Exception e) { + System.out.println("Failed due to: " + e.getMessage()); + } + + // Checking for access with context + // TODO: Add ClientCheckRequest.context + // System.out.println("Checking for access with context"); + // var checkResponse = fgaClient + // .check(new ClientCheckRequest() + // .user("user:anne") + // .relation("reader") + // ._object("document:roadmap") + // .context(Map.of("ViewCount", 100))) + // .get(); + // System.out.println("Allowed: " + checkResponse.getAllowed()); + + // WriteAssertions + fgaClient + .writeAssertions(List.of( + new ClientAssertion() + .user("user:carl") + .relation("writer") + ._object("document:budget") + .expectation(true), + new ClientAssertion() + .user("user:anne") + .relation("reader") + ._object("document:roadmap") + .expectation(false))) + .get(); + System.out.println("Assertions updated"); + + // ReadAssertions + System.out.println("Reading Assertions"); + var assertions = fgaClient.readAssertions().get(); + System.out.println("Assertions " + mapper.writeValueAsString(assertions)); + + // DeleteStore + System.out.println("Deleting Current Store"); + fgaClient.deleteStore().get(); + System.out.println("Deleted Store: " + currentStore.getName()); + } + + public static void main(String[] args) { + System.out.println("=== Example 1 (Java) ==="); + try { + new Example1().run(); + } catch (Exception e) { + System.err.printf("ERROR: %s%n", e); + } + } + + String module = "main"; // Used only for integration testing + + // Small helper function to load resource files relative to this class. + private String loadResource(String filename) { + try { + var filepath = Paths.get("src", module, "resources", filename); + return Files.readString(filepath, StandardCharsets.UTF_8); + } catch (IOException cause) { + throw new RuntimeException("Unable to load resource: " + filename, cause); + } + } +} diff --git a/src/test-integration/java/dev/openfga/sdk/example/ExampleTest.java b/src/test-integration/java/dev/openfga/sdk/example/ExampleTest.java new file mode 100644 index 00000000..50fcfaf0 --- /dev/null +++ b/src/test-integration/java/dev/openfga/sdk/example/ExampleTest.java @@ -0,0 +1,13 @@ +package dev.openfga.sdk.example; + +import org.junit.jupiter.api.Test; + +public class ExampleTest { + private final Example1 example1 = new Example1(); + + @Test + public void example1() throws Exception { + example1.module = "test-integration"; + example1.run(); + } +} diff --git a/src/test-integration/resources/example1-auth-model.json b/src/test-integration/resources/example1-auth-model.json new file mode 100644 index 00000000..e1361656 --- /dev/null +++ b/src/test-integration/resources/example1-auth-model.json @@ -0,0 +1,73 @@ +{ + "schema_version": "1.1", + "type_definitions": [ + { + "type": "user" + }, + { + "type": "document", + "relations": { + "reader": { + "this": {} + }, + "writer": { + "this": {} + }, + "owner": { + "this": {} + } + }, + "metadata": { + "relations": { + "reader": { + "directly_related_user_types": [ + { + "type": "user" + } + ] + }, + "writer": { + "directly_related_user_types": [ + { + "type": "user" + } + ] + }, + "owner": { + "directly_related_user_types": [ + { + "type": "user" + } + ] + }, + "conditional_reader": { + "directly_related_user_types": [ + { + "condition": "name_starts_with_a", + "type": "user" + } + ] + } + } + } + } + ], + "conditions": { + "ViewCountLessThan200": { + "name": "ViewCountLessThan200", + "expression": "ViewCount < 200", + "parameters": { + "ViewCount": { + "type_name": "TYPE_NAME_INT" + }, + "Type": { + "type_name": "TYPE_NAME_STRING" + }, + "Name": { + "type_name": "TYPE_NAME_STRING" + } + } + } + } + } + \ No newline at end of file