diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..4b11d34 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "maven" + directory: "/" + schedule: + interval: "daily" + ignore: + - dependency-name: "org.apache.maven.plugins:maven-compiler-plugin" diff --git a/.github/project.yml b/.github/project.yml new file mode 100644 index 0000000..494c229 --- /dev/null +++ b/.github/project.yml @@ -0,0 +1,4 @@ +release: + current-version: "0" + next-version: "999-SNAPSHOT" + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..f1c04e0 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,48 @@ +name: Build + +on: + push: + branches: + - "main" + paths-ignore: + - '.gitignore' + - 'CODEOWNERS' + - 'LICENSE' + - '*.md' + - '*.adoc' + - '*.txt' + - '.all-contributorsrc' + pull_request: + paths-ignore: + - '.gitignore' + - 'CODEOWNERS' + - 'LICENSE' + - '*.md' + - '*.adoc' + - '*.txt' + - '.all-contributorsrc' + +jobs: + build: + name: Build on ${{ matrix.os }} + strategy: + fail-fast: false + matrix: +# os: [windows-latest, macos-latest, ubuntu-latest] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Prepare git + run: git config --global core.autocrlf false + if: startsWith(matrix.os, 'windows') + + - uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + distribution: temurin + java-version: 11 + cache: 'maven' + + - name: Build with Maven + run: mvn -B formatter:validate verify --file pom.xml diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml new file mode 100644 index 0000000..63d9d0f --- /dev/null +++ b/.github/workflows/pre-release.yml @@ -0,0 +1,25 @@ +name: Quarkiverse Pre Release + +on: + pull_request: + paths: + - '.github/project.yml' + +jobs: + release: + runs-on: ubuntu-latest + name: pre release + + steps: + - uses: radcortez/project-metadata-action@master + name: retrieve project metadata + id: metadata + with: + github-token: ${{secrets.GITHUB_TOKEN}} + metadata-file-path: '.github/project.yml' + + - name: Validate version + if: contains(steps.metadata.outputs.current-version, 'SNAPSHOT') + run: | + echo '::error::Cannot release a SNAPSHOT version.' + exit 1 \ No newline at end of file diff --git a/.github/workflows/quarkus-snapshot.yaml b/.github/workflows/quarkus-snapshot.yaml new file mode 100644 index 0000000..7200fd5 --- /dev/null +++ b/.github/workflows/quarkus-snapshot.yaml @@ -0,0 +1,52 @@ +name: "Quarkus ecosystem CI" +on: + workflow_dispatch: + watch: + types: [started] + + # For this CI to work, ECOSYSTEM_CI_TOKEN needs to contain a GitHub with rights to close the Quarkus issue that the user/bot has opened, + # while 'ECOSYSTEM_CI_REPO_PATH' needs to be set to the corresponding path in the 'quarkusio/quarkus-ecosystem-ci' repository + +env: + ECOSYSTEM_CI_REPO: quarkusio/quarkus-ecosystem-ci + ECOSYSTEM_CI_REPO_FILE: context.yaml + JAVA_VERSION: 11 + + ######################### + # Repo specific setting # + ######################### + + ECOSYSTEM_CI_REPO_PATH: quarkiverse-openfga + +jobs: + build: + name: "Build against latest Quarkus snapshot" + runs-on: ubuntu-latest + # Allow to manually launch the ecosystem CI in addition to the bots + if: github.actor == 'quarkusbot' || github.actor == 'quarkiversebot' || github.actor == '' + + steps: + - name: Install yq + run: sudo add-apt-repository ppa:rmescandon/yq && sudo apt update && sudo apt install yq -y + + - name: Set up Java + uses: actions/setup-java@v2 + with: + distribution: temurin + java-version: ${{ env.JAVA_VERSION }} + + - name: Checkout repo + uses: actions/checkout@v2 + with: + path: current-repo + + - name: Checkout Ecosystem + uses: actions/checkout@v2 + with: + repository: ${{ env.ECOSYSTEM_CI_REPO }} + path: ecosystem-ci + + - name: Setup and Run Tests + run: ./ecosystem-ci/setup-and-test + env: + ECOSYSTEM_CI_TOKEN: ${{ secrets.ECOSYSTEM_CI_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..2fd402b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,86 @@ +name: Quarkiverse Release + +on: + pull_request: + types: [closed] + paths: + - '.github/project.yml' + +jobs: + release: + runs-on: ubuntu-latest + name: release + if: ${{github.event.pull_request.merged == true}} + + steps: + - uses: radcortez/project-metadata-action@main + name: Retrieve project metadata + id: metadata + with: + github-token: ${{secrets.GITHUB_TOKEN}} + metadata-file-path: '.github/project.yml' + + - uses: actions/checkout@v3 + + - name: Import GPG key + id: import_gpg + uses: crazy-max/ghaction-import-gpg@v3 + with: + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 11 + cache: 'maven' + server-id: ossrh + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + + - name: Configure Git author + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + - name: Maven release ${{steps.metadata.outputs.current-version}} + run: | + git checkout -b release + mvn -B release:prepare -Prelease -DreleaseVersion=${{steps.metadata.outputs.current-version}} -DdevelopmentVersion=${{steps.metadata.outputs.next-version}} + if ! git diff --quiet docs/modules/ROOT/pages/includes/attributes.adoc; then + git add docs/modules/ROOT/pages/includes/attributes.adoc + git commit -m "Update stable version for documentation" + fi + git checkout ${{github.base_ref}} + git rebase release + mvn -B release:perform -Darguments=-DperformRelease -DperformRelease -Prelease + env: + MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} + + - name: Adjust tag for documentation changes + run: | + git checkout ${{steps.metadata.outputs.current-version}} + mvn -B clean install -DskipTests -DskipITs + if ! git diff --quiet docs/modules/ROOT/pages/includes/attributes.adoc; then + git add docs/modules/ROOT/pages/includes/attributes.adoc + git commit -m "Update stable version for documentation" + # Move the tag after inclusion of documentation adjustments + git tag -f ${{steps.metadata.outputs.current-version}} + fi + # Go back to main + git checkout main + + - name: Push changes to ${{github.base_ref}} + uses: ad-m/github-push-action@v0.6.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + branch: ${{github.base_ref}} + + - name: Push tags + uses: ad-m/github-push-action@v0.6.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + tags: true + branch: ${{github.base_ref}} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a19c3c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,64 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see https://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# Eclipse +.project +.classpath +.settings/ +bin/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Gradle +.gradle/ +build/ + +# Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +release.properties diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + http://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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..082ef76 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# Quarkus Openfga + +[![Version](https://img.shields.io/maven-central/v/io.quarkiverse.openfga/quarkus-openfga?logo=apache-maven&style=flat-square)](https://search.maven.org/artifact/io.quarkiverse.openfga/quarkus-openfga) + +## Welcome to Quarkiverse! + +Congratulations and thank you for creating a new Quarkus extension project in Quarkiverse! + +Feel free to replace this content with the proper description of your new project and necessary instructions how to use and contribute to it. + +You can find the basic info, Quarkiverse policies and conventions in [the Quarkiverse wiki](https://github.com/quarkiverse/quarkiverse/wiki). + +In case you are creating a Quarkus extension project for the first time, please follow [Building My First Extension](https://quarkus.io/guides/building-my-first-extension) guide. + +Other useful articles related to Quarkus extension development can be found under the [Writing Extensions](https://quarkus.io/guides/#writing-extensions) guide category on the [Quarkus.io](https://quarkus.io) website. + +Thanks again, good luck and have fun! + +## Documentation + +The documentation for this extension should be maintained as part of this repository and it is stored in the `docs/` directory. + +The layout should follow the [Antora's Standard File and Directory Set](https://docs.antora.org/antora/2.3/standard-directories/). + +Once the docs are ready to be published, please open a PR including this repository in the [Quarkiverse Docs Antora playbook](https://github.com/quarkiverse/quarkiverse-docs/blob/main/antora-playbook.yml#L7). See an example [here](https://github.com/quarkiverse/quarkiverse-docs/pull/1). diff --git a/deployment/pom.xml b/deployment/pom.xml new file mode 100644 index 0000000..4cf8afd --- /dev/null +++ b/deployment/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + + io.quarkiverse.openfga + quarkus-openfga-client-parent + 999-SNAPSHOT + + quarkus-openfga-client-deployment + Quarkus OpenFGA Client - Deployment + + + io.quarkiverse.openfga + quarkus-openfga-client + ${project.version} + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus + quarkus-jackson-deployment + + + io.quarkus + quarkus-mutiny-deployment + + + io.quarkus + quarkus-vertx-deployment + + + org.testcontainers + vault + + + org.hamcrest + hamcrest-core + + + junit + junit + + + + + io.quarkus + quarkus-devservices-common + + + io.quarkus + quarkus-smallrye-health-spi + + + io.quarkus + quarkus-junit5-internal + test + + + org.exparity + hamcrest-date + ${hamcrest.date.version} + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + + diff --git a/deployment/src/main/java/io/quarkiverse/openfga/deployment/DevServicesOpenFGAConfig.java b/deployment/src/main/java/io/quarkiverse/openfga/deployment/DevServicesOpenFGAConfig.java new file mode 100644 index 0000000..ca1e3aa --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/openfga/deployment/DevServicesOpenFGAConfig.java @@ -0,0 +1,85 @@ +package io.quarkiverse.openfga.deployment; + +import java.util.Optional; +import java.util.OptionalInt; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class DevServicesOpenFGAConfig { + + /** + * If DevServices has been explicitly enabled or disabled. DevServices is generally enabled + * by default, unless there is an existing configuration present. + *

+ * When DevServices is enabled Quarkus will attempt to automatically configure and start + * a database when running in Dev or Test mode. + */ + @ConfigItem + public Optional enabled; + + /** + * The container image name to use, for container based DevServices providers. + */ + @ConfigItem + public Optional imageName; + + /** + * Indicates if the OpenFGA instance managed by Quarkus Dev Services is shared. + * When shared, Quarkus looks for running containers using label-based service discovery. + * If a matching container is found, it is used, and so a second one is not started. + * Otherwise, Dev Services for OpenFGA starts a new container. + *

+ * The discovery uses the {@code quarkus-dev-service-openfga} label. + * The value is configured using the {@code service-name} property. + *

+ * Container sharing is only used in dev mode. + */ + @ConfigItem(defaultValue = "true") + public boolean shared; + + /** + * The value of the {@code quarkus-dev-service-openfga} label attached to the started container. + * This property is used when {@code shared} is set to {@code true}. + * In this case, before starting a container, Dev Services for OpenFGA looks for a container with the + * {@code quarkus-dev-service-openfga} label + * set to the configured value. If found, it will use this container instead of starting a new one. Otherwise it + * starts a new container with the {@code quarkus-dev-service-openfga} label set to the specified value. + *

+ * This property is used when you need multiple shared OpenFGA instances. + */ + @ConfigItem(defaultValue = "openfga") + public String serviceName; + + /** + * Optional fixed port the dev service will be bound to. + *

+ * If not defined, the port will be chosen randomly. + */ + @ConfigItem + public OptionalInt port; + + /** + * Name of authorization store to create for devservices. + *

+ * Defaults to "dev". + */ + @ConfigItem(defaultValue = "dev") + public String storeName; + + /** + * JSON formatted authorization model to upload during devservices initialization. + */ + @ConfigItem + public Optional authorizationModel; + + /** + * Location of JSON formatted authorization model file to upload during devservices initialization. + *

+ * The location can be prefixed with {@code classpath:} or {@code filesystem:} to specify where the file + * will be read from; if not prefixed, it will be read from the classpath. + */ + @ConfigItem + public Optional authorizationModelLocation; +} diff --git a/deployment/src/main/java/io/quarkiverse/openfga/deployment/DevServicesOpenFGAProcessor.java b/deployment/src/main/java/io/quarkiverse/openfga/deployment/DevServicesOpenFGAProcessor.java new file mode 100644 index 0000000..ca26189 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/openfga/deployment/DevServicesOpenFGAProcessor.java @@ -0,0 +1,288 @@ +package io.quarkiverse.openfga.deployment; + +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import org.jboss.logging.Logger; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +import io.quarkus.deployment.IsNormal; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.*; +import io.quarkus.deployment.builditem.DevServicesResultBuildItem.RunningDevService; +import io.quarkus.deployment.console.ConsoleInstalledBuildItem; +import io.quarkus.deployment.console.StartupLogCompressor; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; +import io.quarkus.deployment.logging.LoggingSetupBuildItem; +import io.quarkus.devservices.common.ContainerLocator; +import io.quarkus.runtime.configuration.ConfigUtils; +import io.quarkus.runtime.util.ClassPathUtils; + +public class DevServicesOpenFGAProcessor { + + private static final Logger log = Logger.getLogger(DevServicesOpenFGAProcessor.class); + static final String OPEN_FGA_VERSION = "v0.2.0"; + static final String OPEN_FGA_IMAGE = "openfga/openfga:" + OPEN_FGA_VERSION; + static final int OPEN_FGA_EXPOSED_PORT = 8080; + static final String DEV_SERVICE_LABEL = "quarkus-dev-service-openfga"; + static final String CONFIG_PREFIX = "quarkus.openfga."; + static final String URL_CONFIG_KEY = CONFIG_PREFIX + "url"; + static final String STORE_ID_CONFIG_KEY = CONFIG_PREFIX + "store-id"; + static final String AUTHORIZATION_MODEL_ID_CONFIG_KEY = CONFIG_PREFIX + "authorization-model-id"; + static final ContainerLocator openFGAContainerLocator = new ContainerLocator(DEV_SERVICE_LABEL, + OPEN_FGA_EXPOSED_PORT); + private static volatile RunningDevService devService; + private static volatile DevServicesOpenFGAConfig capturedDevServicesConfiguration; + private static volatile boolean first = true; + + @BuildStep(onlyIfNot = IsNormal.class, onlyIf = GlobalDevServicesConfig.Enabled.class) + public DevServicesResultBuildItem startContainers(OpenFGABuildTimeConfig config, + Optional consoleInstalledBuildItem, + LaunchModeBuildItem launchMode, + DockerStatusBuildItem dockerStatusBuildItem, + CuratedApplicationShutdownBuildItem closeBuildItem, + LoggingSetupBuildItem loggingSetupBuildItem, + GlobalDevServicesConfig devServicesConfig, + BuildProducer devServicesResults) { + + DevServicesOpenFGAConfig currentDevServicesConfiguration = config.devservices; + + // figure out if we need to shut down and restart any existing OpenFGA container + // if not and the OpenFGA container have already started we just return + if (devService != null) { + boolean restartRequired = !currentDevServicesConfiguration.equals(capturedDevServicesConfiguration); + if (!restartRequired) { + return devService.toBuildItem(); + } + try { + devService.close(); + } catch (Throwable e) { + log.error("Failed to stop OpenFGA container", e); + } + devService = null; + capturedDevServicesConfiguration = null; + } + + capturedDevServicesConfiguration = currentDevServicesConfiguration; + + StartupLogCompressor compressor = new StartupLogCompressor( + (launchMode.isTest() ? "(test) " : "") + "OpenFGA Dev Services Starting:", consoleInstalledBuildItem, + loggingSetupBuildItem); + try { + devService = startContainer(dockerStatusBuildItem, currentDevServicesConfiguration, launchMode, + devServicesConfig.timeout); + if (devService != null) { + + if (devService.isOwner()) { + log.info("Dev Services for OpenFGA started."); + log.infof("Other Quarkus applications in dev mode will find the " + + "instance automatically. For Quarkus applications in production mode, you can connect to" + + " this by starting your application with -D%s=%s", + URL_CONFIG_KEY, devService.getConfig().get(URL_CONFIG_KEY)); + } + } else { + return null; + } + } catch (Throwable t) { + compressor.closeAndDumpCaptured(); + throw new RuntimeException(t); + } finally { + compressor.close(); + } + + if (first) { + first = false; + Runnable closeTask = () -> { + if (devService != null) { + try { + devService.close(); + } catch (Throwable t) { + log.error("Failed to stop OpenFGA container", t); + } + devService = null; + log.info("Dev Services for OpenFGA shut down."); + } + first = true; + capturedDevServicesConfiguration = null; + }; + closeBuildItem.addCloseTask(closeTask, true); + } + + return devService.toBuildItem(); + } + + private RunningDevService startContainer(DockerStatusBuildItem dockerStatusBuildItem, + DevServicesOpenFGAConfig devServicesConfig, + LaunchModeBuildItem launchMode, Optional timeout) { + if (!devServicesConfig.enabled.orElse(true)) { + // explicitly disabled + log.debug("Not starting devservices for OpenFGA as it has been disabled in the config"); + return null; + } + + boolean needToStart = !ConfigUtils.isPropertyPresent(URL_CONFIG_KEY); + if (!needToStart) { + log.debug("Not starting devservices for default OpenFGA client as url has been provided"); + return null; + } + + if (!dockerStatusBuildItem.isDockerAvailable()) { + log.warn("Please configure " + URL_CONFIG_KEY + " or get a working docker instance"); + return null; + } + + DockerImageName dockerImageName = DockerImageName.parse(devServicesConfig.imageName.orElse(OPEN_FGA_IMAGE)) + .asCompatibleSubstituteFor(OPEN_FGA_IMAGE); + + final Supplier defaultOpenFGAInstanceSupplier = () -> { + + QuarkusOpenFGAContainer container = new QuarkusOpenFGAContainer(dockerImageName, devServicesConfig.port, + devServicesConfig.serviceName) + .withNetwork(Network.SHARED) + .waitingFor(Wait.forHttp("/stores")); + + timeout.ifPresent(container::withStartupTimeout); + + log.info("Starting OpenFGA..."); + + container.start(); + + var instanceURL = format("http://%s:%d", container.getHost(), container.getPort()); + + var devServicesConfigProperties = new HashMap(); + devServicesConfigProperties.put(URL_CONFIG_KEY, instanceURL); + + var storeInitializer = new DevServicesStoreInitializer(instanceURL); + + String storeId; + try { + log.info("Initializing authorization store..."); + + storeId = storeInitializer.createStore(devServicesConfig.storeName); + + devServicesConfigProperties.put(STORE_ID_CONFIG_KEY, storeId); + + } catch (Exception e) { + throw new RuntimeException("Store initialization failed", e); + } + + devServicesConfig.authorizationModel + .ifPresentOrElse(authModel -> { + try { + log.info("Initializing authorization model..."); + + var authorizationModelId = storeInitializer.createAuthorizationModel(storeId, authModel); + + devServicesConfigProperties.put(AUTHORIZATION_MODEL_ID_CONFIG_KEY, authorizationModelId); + + } catch (Exception e) { + throw new RuntimeException("Model initialization failed", e); + } + }, () -> devServicesConfig.authorizationModelLocation + .ifPresentOrElse(location -> { + try { + log.infof("Initializing authorization model from %s...", location); + + var modelPath = resolveModelPath(location); + + try (var modelStream = new FileInputStream(modelPath.toFile())) { + + var authModel = new String(modelStream.readAllBytes(), UTF_8); + + var authorizationModelId = storeInitializer.createAuthorizationModel(storeId, + authModel); + + devServicesConfigProperties.put(AUTHORIZATION_MODEL_ID_CONFIG_KEY, + authorizationModelId); + } + + } catch (Exception e) { + throw new RuntimeException("Model initialization failed", e); + } + }, () -> log.info( + "No authentication model provided, skipping authorization store & model initialization"))); + + return new RunningDevService(OpenFGAProcessor.FEATURE, container.getContainerId(), container::close, + devServicesConfigProperties); + }; + + return openFGAContainerLocator + .locateContainer(devServicesConfig.serviceName, devServicesConfig.shared, launchMode.getLaunchMode()) + .map(containerAddress -> { + var instanceURL = format("http://%s:%d", containerAddress.getHost(), containerAddress.getPort()); + return new RunningDevService(OpenFGAProcessor.FEATURE, containerAddress.getId(), + null, URL_CONFIG_KEY, instanceURL); + }) + .orElseGet(defaultOpenFGAInstanceSupplier); + } + + private static class QuarkusOpenFGAContainer extends GenericContainer { + OptionalInt fixedExposedPort; + + public QuarkusOpenFGAContainer(DockerImageName dockerImageName, OptionalInt fixedExposedPort, String serviceName) { + super(dockerImageName); + this.fixedExposedPort = fixedExposedPort; + withCommand("run"); + withNetwork(Network.SHARED); + if (serviceName != null) { // Only adds the label in dev mode. + withLabel(DEV_SERVICE_LABEL, serviceName); + } + } + + @Override + protected void configure() { + super.configure(); + if (fixedExposedPort.isPresent()) { + addFixedExposedPort(fixedExposedPort.getAsInt(), OPEN_FGA_EXPOSED_PORT); + } else { + addExposedPort(OPEN_FGA_EXPOSED_PORT); + } + } + + public int getPort() { + if (fixedExposedPort.isPresent()) { + return fixedExposedPort.getAsInt(); + } + return super.getMappedPort(OPEN_FGA_EXPOSED_PORT); + } + } + + private Path resolveModelPath(String location) throws IOException { + location = normalizeLocation(location); + if (location.startsWith("filesystem:")) { + return Path.of(location.substring("filesystem:".length())); + } + + var classpathPath = new AtomicReference(); + ClassPathUtils.consumeAsPaths(Thread.currentThread().getContextClassLoader(), location, classpathPath::set); + + return classpathPath.get(); + } + + private String normalizeLocation(String location) { + // Strip any 'classpath:' protocol prefixes because they are assumed + // but not recognized by ClassLoader.getResources() + if (location.startsWith("classpath:")) { + location = location.substring("classpath:".length()); + if (location.startsWith("/")) { + location = location.substring(1); + } + } + if (!location.endsWith("/")) { + location += "/"; + } + return location; + } +} diff --git a/deployment/src/main/java/io/quarkiverse/openfga/deployment/DevServicesStoreInitializer.java b/deployment/src/main/java/io/quarkiverse/openfga/deployment/DevServicesStoreInitializer.java new file mode 100644 index 0000000..e1da982 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/openfga/deployment/DevServicesStoreInitializer.java @@ -0,0 +1,69 @@ +package io.quarkiverse.openfga.deployment; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse.BodyHandlers; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.quarkiverse.openfga.client.model.dto.CreateStoreRequest; +import io.quarkiverse.openfga.client.model.dto.CreateStoreResponse; +import io.quarkiverse.openfga.client.model.dto.WriteAuthorizationModelResponse; + +public class DevServicesStoreInitializer { + + private static final ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules(); + + private final HttpClient httpClient = HttpClient.newHttpClient(); + private final URI instanceURL; + + public DevServicesStoreInitializer(String instanceURL) { + this.instanceURL = URI.create(instanceURL); + } + + public String createStore(String name) throws Exception { + + var requestBody = new CreateStoreRequest(name); + + var url = instanceURL.resolve("/stores"); + + var request = HttpRequest.newBuilder(url) + .POST(BodyPublishers.ofString(objectMapper.writeValueAsString(requestBody))) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .build(); + + var httpResponse = httpClient.send(request, BodyHandlers.ofString()); + if (httpResponse.statusCode() != 201) { + throw new IOException("Failed to create store for devservices"); + } + + var response = objectMapper.readValue(httpResponse.body(), CreateStoreResponse.class); + + return response.getId(); + } + + public String createAuthorizationModel(String storeId, String modelJSON) throws Exception { + + var url = instanceURL.resolve("/stores/" + storeId + "/authorization-models"); + + var request = HttpRequest.newBuilder(url) + .POST(BodyPublishers.ofString(modelJSON)) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .build(); + + var httpResponse = httpClient.send(request, BodyHandlers.ofString()); + if (httpResponse.statusCode() != 201) { + throw new IOException("Failed to create authorization model for devservices"); + } + + var response = objectMapper.readValue(httpResponse.body(), WriteAuthorizationModelResponse.class); + + return response.getAuthorizationModelId(); + } + +} diff --git a/deployment/src/main/java/io/quarkiverse/openfga/deployment/OpenFGABuildTimeConfig.java b/deployment/src/main/java/io/quarkiverse/openfga/deployment/OpenFGABuildTimeConfig.java new file mode 100644 index 0000000..47fc16d --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/openfga/deployment/OpenFGABuildTimeConfig.java @@ -0,0 +1,28 @@ +package io.quarkiverse.openfga.deployment; + +import io.quarkiverse.openfga.runtime.config.OpenFGAConfig; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(name = OpenFGAConfig.NAME, phase = ConfigPhase.BUILD_TIME) +public class OpenFGABuildTimeConfig { + + /** + * Whether a health check is published in case the smallrye-health extension is present. + */ + @ConfigItem(name = "health.enabled", defaultValue = "true") + public boolean healthEnabled; + + /** + * Whether tracing spans of client commands are reported. + */ + @ConfigItem(name = "tracing.enabled") + public boolean tracingEnabled; + + /** + * Dev services configuration. + */ + @ConfigItem + public DevServicesOpenFGAConfig devservices; +} diff --git a/deployment/src/main/java/io/quarkiverse/openfga/deployment/OpenFGAProcessor.java b/deployment/src/main/java/io/quarkiverse/openfga/deployment/OpenFGAProcessor.java new file mode 100644 index 0000000..31bdccf --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/openfga/deployment/OpenFGAProcessor.java @@ -0,0 +1,114 @@ +package io.quarkiverse.openfga.deployment; + +import static io.quarkus.deployment.Capability.SMALLRYE_HEALTH; +import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; + +import javax.enterprise.context.ApplicationScoped; + +import io.quarkiverse.openfga.client.AuthorizationModelClient; +import io.quarkiverse.openfga.client.StoreClient; +import io.quarkiverse.openfga.client.StoresClient; +import io.quarkiverse.openfga.client.api.API; +import io.quarkiverse.openfga.client.model.Store; +import io.quarkiverse.openfga.runtime.OpenFGARecorder; +import io.quarkiverse.openfga.runtime.config.OpenFGAConfig; +import io.quarkiverse.openfga.runtime.health.OpenFGAHealthCheck; +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.*; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.TlsConfig; +import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem; +import io.quarkus.vertx.deployment.VertxBuildItem; + +class OpenFGAProcessor { + + static final String FEATURE = "openfga-client"; + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } + + @BuildStep + void registerModelClasses(BuildProducer reflectiveClasses, + CombinedIndexBuildItem combinedIndexBuildItem) { + + final String[] modelClasses = combinedIndexBuildItem.getIndex() + .getKnownClasses().stream() + .filter(c -> c.name().packagePrefix().startsWith(Store.class.getPackageName())) + .map(c -> c.name().toString()) + .toArray(String[]::new); + reflectiveClasses.produce(ReflectiveClassBuildItem.weakClass(modelClasses)); + } + + @BuildStep + @Record(RUNTIME_INIT) + ServiceStartBuildItem registerSyntheticBeans( + OpenFGABuildTimeConfig buildTimeConfig, + OpenFGAConfig runtimeConfig, + TlsConfig tlsConfig, + SslNativeConfigBuildItem sslNativeConfig, + VertxBuildItem vertx, + ShutdownContextBuildItem shutdownContextBuildItem, + OpenFGARecorder recorder, + Capabilities capabilities, + BuildProducer syntheticBeans, + BuildProducer health, + BuildProducer sslNativeSupport) { + + RuntimeValue apiValue = recorder.createAPI( + runtimeConfig, tlsConfig, buildTimeConfig.tracingEnabled, + vertx.getVertx(), shutdownContextBuildItem); + + sslNativeSupport.produce(new ExtensionSslNativeSupportBuildItem(FEATURE)); + + syntheticBeans.produce( + SyntheticBeanBuildItem.configure(API.class) + .scope(ApplicationScoped.class) + .setRuntimeInit() + .runtimeValue(apiValue) + .done()); + + syntheticBeans.produce( + SyntheticBeanBuildItem.configure(StoresClient.class) + .scope(ApplicationScoped.class) + .setRuntimeInit() + .runtimeValue(recorder.createStoresClient(apiValue)) + .done()); + + syntheticBeans.produce( + SyntheticBeanBuildItem.configure(StoreClient.class) + .scope(ApplicationScoped.class) + .setRuntimeInit() + .runtimeValue(recorder.createStoreClient(apiValue, runtimeConfig)) + .done()); + + syntheticBeans.produce( + SyntheticBeanBuildItem.configure(AuthorizationModelClient.class) + .scope(ApplicationScoped.class) + .setRuntimeInit() + .runtimeValue(recorder.createAuthModelClient(apiValue, runtimeConfig)) + .done()); + + if (capabilities.isPresent(SMALLRYE_HEALTH) && buildTimeConfig.healthEnabled) { + + syntheticBeans.produce( + SyntheticBeanBuildItem.configure(OpenFGAHealthCheck.class) + .unremovable() + .scope(ApplicationScoped.class) + .setRuntimeInit() + .runtimeValue(recorder.createHealthCheck(apiValue, runtimeConfig)) + .done()); + + health.produce(new HealthBuildItem(OpenFGAHealthCheck.class.getName(), buildTimeConfig.healthEnabled)); + } + + return new ServiceStartBuildItem("openfga-client"); + } + +} diff --git a/deployment/src/test/java/io/quarkiverse/openfga/test/AuthorizationModelClientTest.java b/deployment/src/test/java/io/quarkiverse/openfga/test/AuthorizationModelClientTest.java new file mode 100644 index 0000000..acbcd32 --- /dev/null +++ b/deployment/src/test/java/io/quarkiverse/openfga/test/AuthorizationModelClientTest.java @@ -0,0 +1,243 @@ +package io.quarkiverse.openfga.test; + +import static java.time.Duration.ofSeconds; +import static java.time.OffsetDateTime.now; +import static java.time.temporal.ChronoUnit.SECONDS; +import static java.util.Collections.emptyList; +import static org.exparity.hamcrest.date.OffsetDateTimeMatchers.within; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkiverse.openfga.client.AuthorizationModelClient; +import io.quarkiverse.openfga.client.StoreClient; +import io.quarkiverse.openfga.client.StoresClient; +import io.quarkiverse.openfga.client.model.*; +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.mutiny.helpers.test.UniAssertSubscriber; + +public class AuthorizationModelClientTest { + + // Start unit test with extension loaded + @RegisterExtension + static final QuarkusUnitTest unitTest = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); + + @Inject + StoresClient storesClient; + + Store store; + StoreClient storeClient; + + AuthorizationModelClient authorizationModelClient; + + @BeforeEach + public void createTestStoreAndModel() { + store = storesClient.create("test").await().atMost(ofSeconds(10)); + storeClient = storesClient.store(store.getId()); + + // ensure it has an auth model + var documentTypeDef = new TypeDefinition("document", Map.of( + "reader", Userset.direct(), + "writer", Userset.direct())); + + var authModelId = storeClient.authorizationModels().create(List.of(documentTypeDef)) + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem() + .getItem(); + + authorizationModelClient = storeClient.authorizationModels().model(authModelId); + } + + @AfterEach + public void deleteTestStore() { + if (storeClient != null) { + storeClient.delete().await().atMost(ofSeconds(10)); + } + } + + @Test + @DisplayName("Can Read & Write Tuples") + public void canReadWriteTuples() { + + var tuples = List.of( + TupleKey.of("document:123", "reader", "me")); + var writes = authorizationModelClient.write(tuples, emptyList()) + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem() + .getItem(); + assertThat(writes.entrySet(), hasSize(0)); + + var foundTuples = storeClient.readAllTuples() + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem() + .getItem() + .stream().map(Tuple::getKey).collect(Collectors.toList()); + + assertThat(foundTuples, equalTo(tuples)); + } + + @Test + @DisplayName("Can Execute Checks") + public void canExecuteChecks() { + + var tuple = TupleKey.of("document:123", "reader", "me"); + + var writes = authorizationModelClient.write(List.of(tuple), emptyList()) + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem() + .getItem(); + assertThat(writes.entrySet(), hasSize(0)); + + var allowed = authorizationModelClient.check(tuple, null) + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem() + .getItem(); + assertThat(allowed, equalTo(true)); + } + + @Test + @DisplayName("Can Read All Relationships for an Object") + public void canReadAllRelationshipsForObject() { + + var readerTuple = TupleKey.of("document:123", "reader", "me"); + var writerTuple = TupleKey.of("document:123", "writer", "me"); + + authorizationModelClient.write(List.of(readerTuple), emptyList()) + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem(); + authorizationModelClient.write(List.of(writerTuple), emptyList()) + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem(); + + var objects = authorizationModelClient.queryAllTuples(PartialTupleKey.of("document:123", null, null)) + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem() + .getItem(); + assertThat(objects, containsInAnyOrder( + allOf( + hasProperty("key", equalTo(readerTuple)), + hasProperty("timestamp", within(5, SECONDS, now()))), + allOf( + hasProperty("key", equalTo(writerTuple)), + hasProperty("timestamp", within(5, SECONDS, now()))))); + } + + @Test + @DisplayName("Can Read All Relationships for an Object, and Relation") + public void canReadAllRelationshipsForObjectAndRelation() { + + var meTuple = TupleKey.of("document:123", "reader", "me"); + var youTuple = TupleKey.of("document:123", "reader", "you"); + + authorizationModelClient.write(List.of(meTuple), emptyList()) + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem(); + authorizationModelClient.write(List.of(youTuple), emptyList()) + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem(); + + var objects = authorizationModelClient.queryAllTuples(PartialTupleKey.of("document:123", "reader", null)) + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem() + .getItem(); + assertThat(objects, containsInAnyOrder( + allOf( + hasProperty("key", equalTo(meTuple)), + hasProperty("timestamp", within(5, SECONDS, now()))), + allOf( + hasProperty("key", equalTo(youTuple)), + hasProperty("timestamp", within(5, SECONDS, now()))))); + } + + @Test + @DisplayName("Can Read All Relationships for an Object, and User") + public void canReadAllRelationshipsForObjectAndUser() { + + var readerTuple = TupleKey.of("document:123", "reader", "me"); + var writerTuple = TupleKey.of("document:123", "writer", "me"); + + authorizationModelClient.write(List.of(readerTuple), emptyList()) + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem(); + authorizationModelClient.write(List.of(writerTuple), emptyList()) + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem(); + + var objects = authorizationModelClient.queryAllTuples(PartialTupleKey.of("document:123", null, "me")) + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem() + .getItem(); + assertThat(objects, containsInAnyOrder( + allOf( + hasProperty("key", equalTo(readerTuple)), + hasProperty("timestamp", within(5, SECONDS, now()))), + allOf( + hasProperty("key", equalTo(writerTuple)), + hasProperty("timestamp", within(5, SECONDS, now()))))); + } + + @Test + @DisplayName("Can Read All Relationships for an Object Type, and User") + public void canReadAllRelationshipsForObjectTypeAndUser() { + + var readerTuple = TupleKey.of("document:123", "reader", "me"); + var writerTuple = TupleKey.of("document:123", "writer", "me"); + + authorizationModelClient.write(List.of(readerTuple), emptyList()) + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem(); + authorizationModelClient.write(List.of(writerTuple), emptyList()) + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem(); + authorizationModelClient.write(List.of(TupleKey.of("document:123", "writer", "you")), emptyList()) + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem(); + + var objects = authorizationModelClient.queryAllTuples(PartialTupleKey.of("document:", null, "me")) + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem() + .getItem(); + assertThat(objects, containsInAnyOrder( + allOf( + hasProperty("key", equalTo(readerTuple)), + hasProperty("timestamp", within(5, SECONDS, now()))), + allOf( + hasProperty("key", equalTo(writerTuple)), + hasProperty("timestamp", within(5, SECONDS, now()))))); + } + + @Test + @DisplayName("Can List All Objects for an Object Type, Relation, and User") + public void canListAllRelationshipsForObjectTypeRelationAndUser() { + + var aTuple = TupleKey.of("document:123", "writer", "me"); + var bTuple = TupleKey.of("document:456", "writer", "me"); + + authorizationModelClient.write(List.of(aTuple), emptyList()) + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem(); + authorizationModelClient.write(List.of(bTuple), emptyList()) + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem(); + + var objects = authorizationModelClient.listObjects("document", "writer", "me", null) + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem() + .getItem(); + assertThat(objects, containsInAnyOrder("123", "456")); + } +} diff --git a/deployment/src/test/java/io/quarkiverse/openfga/test/AuthorizationModelsClientTest.java b/deployment/src/test/java/io/quarkiverse/openfga/test/AuthorizationModelsClientTest.java new file mode 100644 index 0000000..0ee8398 --- /dev/null +++ b/deployment/src/test/java/io/quarkiverse/openfga/test/AuthorizationModelsClientTest.java @@ -0,0 +1,74 @@ +package io.quarkiverse.openfga.test; + +import static java.time.Duration.ofSeconds; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkiverse.openfga.client.AuthorizationModelsClient; +import io.quarkiverse.openfga.client.StoreClient; +import io.quarkiverse.openfga.client.StoresClient; +import io.quarkiverse.openfga.client.model.Store; +import io.quarkiverse.openfga.client.model.TypeDefinition; +import io.quarkiverse.openfga.client.model.Userset; +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.mutiny.helpers.test.UniAssertSubscriber; + +public class AuthorizationModelsClientTest { + + // Start unit test with extension loaded + @RegisterExtension + static final QuarkusUnitTest unitTest = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); + + @Inject + StoresClient storesClient; + + Store store; + StoreClient storeClient; + AuthorizationModelsClient authorizationModelsClient; + + @BeforeEach + public void createTestStore() { + store = storesClient.create("test").await().atMost(ofSeconds(10)); + storeClient = storesClient.store(store.getId()); + authorizationModelsClient = storeClient.authorizationModels(); + } + + @AfterEach + public void deleteTestStore() { + if (storeClient != null) { + storeClient.delete().await().atMost(ofSeconds(10)); + } + } + + @Test + @DisplayName("Can List Models") + public void canList() { + + var typeDefinition = new TypeDefinition("document", Map.of("reader", Userset.direct())); + + authorizationModelsClient.create(List.of(typeDefinition)) + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem(); + + var models = authorizationModelsClient.listAll() + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem() + .getItem(); + + assertThat(models, hasSize(1)); + } +} diff --git a/deployment/src/test/java/io/quarkiverse/openfga/test/StoreClientTest.java b/deployment/src/test/java/io/quarkiverse/openfga/test/StoreClientTest.java new file mode 100644 index 0000000..c556929 --- /dev/null +++ b/deployment/src/test/java/io/quarkiverse/openfga/test/StoreClientTest.java @@ -0,0 +1,83 @@ +package io.quarkiverse.openfga.test; + +import static java.time.Duration.ofSeconds; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkiverse.openfga.client.StoreClient; +import io.quarkiverse.openfga.client.StoresClient; +import io.quarkiverse.openfga.client.model.*; +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.mutiny.helpers.test.UniAssertSubscriber; + +public class StoreClientTest { + + // Start unit test with extension loaded + @RegisterExtension + static final QuarkusUnitTest unitTest = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); + + @Inject + StoresClient storesClient; + + Store store; + StoreClient storeClient; + + @BeforeEach + public void createTestStore() { + store = storesClient.create("test").await().atMost(ofSeconds(10)); + storeClient = storesClient.store(store.getId()); + } + + @AfterEach + public void deleteTestStore() { + if (storeClient != null) { + storeClient.delete().await().atMost(ofSeconds(10)); + } + } + + @Test + @DisplayName("Can Get Store") + public void canGetStore() { + + var foundStore = storeClient.get() + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem() + .getItem(); + + assertThat(foundStore, equalTo(store)); + } + + @Test + @DisplayName("Can Delete Store") + public void canDeleteStores() { + + var preList = storesClient.listAll() + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem() + .getItem(); + assertThat(preList, hasItem(store)); + + storeClient.delete() + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem() + .assertCompleted(); + + var postList = storesClient.listAll() + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem() + .getItem(); + assertThat(postList, not(hasItem(store))); + } + +} diff --git a/deployment/src/test/java/io/quarkiverse/openfga/test/StoresClientTest.java b/deployment/src/test/java/io/quarkiverse/openfga/test/StoresClientTest.java new file mode 100644 index 0000000..a23e1e2 --- /dev/null +++ b/deployment/src/test/java/io/quarkiverse/openfga/test/StoresClientTest.java @@ -0,0 +1,130 @@ +package io.quarkiverse.openfga.test; + +import static java.time.OffsetDateTime.now; +import static java.time.temporal.ChronoUnit.SECONDS; +import static org.exparity.hamcrest.date.OffsetDateTimeMatchers.within; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkiverse.openfga.client.StoresClient; +import io.quarkiverse.openfga.client.model.Store; +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.helpers.test.UniAssertSubscriber; + +public class StoresClientTest { + + // Start unit test with extension loaded + @RegisterExtension + static final QuarkusUnitTest unitTest = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); + + @Inject + StoresClient storesClient; + + @BeforeEach + public void deleteAllStores() { + for (var store : storesClient.listAll().await().indefinitely()) { + storesClient.store(store.getId()).delete().await().indefinitely(); + } + } + + @Test + @DisplayName("Can Create Stores") + public void canCreateStores() { + + var store = storesClient.create("testing") + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem() + .getItem(); + + assertThat(store.getId(), not(emptyOrNullString())); + assertThat(store.getName(), equalTo("testing")); + assertThat(store.getCreatedAt(), within(5, SECONDS, now())); + assertThat(store.getUpdatedAt(), within(5, SECONDS, now())); + assertThat(store.getDeletedAt(), is(nullValue())); + } + + @Test + @DisplayName("Can List Stores Without Pagination") + public void canListStoresWithoutPagination() { + + var createdStores = Multi.createFrom().items("testing1", "testing2") + .onItem().transformToUniAndConcatenate(name -> storesClient.create(name)) + .collect().asList() + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem() + .getItem(); + + assertThat(createdStores.stream().map(Store::getName).collect(Collectors.toList()), + containsInAnyOrder("testing1", "testing2")); + + var list = storesClient.list(2, null) + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem() + .getItem(); + + assertThat(list.getItems(), containsInAnyOrder(createdStores.toArray())); + } + + @Test + @DisplayName("Can List Stores With Pagination") + public void canListStoresWithPagination() { + + var createdStores = Multi.createFrom().items("testing1", "testing2", "testing3") + .onItem().transformToUniAndConcatenate(name -> storesClient.create(name)) + .collect().asList() + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem() + .getItem(); + + assertThat(createdStores.stream().map(Store::getName).collect(Collectors.toList()), + containsInAnyOrder("testing1", "testing2", "testing3")); + + var list = storesClient.listAll(1) + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem() + .getItem(); + + assertThat(list, containsInAnyOrder(createdStores.toArray())); + } + + @Test + @DisplayName("Can Delete Stores") + public void canDeleteStores() { + + var store = storesClient.create("testing") + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem() + .getItem(); + + var preList = storesClient.listAll(1) + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem() + .getItem(); + assertThat(preList, hasSize(1)); + + storesClient.store(store.getId()).delete() + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem() + .assertCompleted(); + + var postList = storesClient.listAll(1) + .subscribe().withSubscriber(UniAssertSubscriber.create()) + .awaitItem() + .getItem(); + assertThat(postList, hasSize(0)); + } + +} diff --git a/docs/antora.yml b/docs/antora.yml new file mode 100644 index 0000000..e1807fb --- /dev/null +++ b/docs/antora.yml @@ -0,0 +1,5 @@ +name: quarkus-openfga-client +title: Quarkus OpenFGA Client +version: dev +nav: + - modules/ROOT/nav.adoc diff --git a/docs/modules/ROOT/assets/images/.keepme b/docs/modules/ROOT/assets/images/.keepme new file mode 100644 index 0000000..e69de29 diff --git a/docs/modules/ROOT/examples/.keepme b/docs/modules/ROOT/examples/.keepme new file mode 100644 index 0000000..e69de29 diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc new file mode 100644 index 0000000..b820469 --- /dev/null +++ b/docs/modules/ROOT/nav.adoc @@ -0,0 +1 @@ +* xref:index.adoc[Quarkus OpenFGA Client] diff --git a/docs/modules/ROOT/pages/includes/attributes.adoc b/docs/modules/ROOT/pages/includes/attributes.adoc new file mode 100644 index 0000000..19a687e --- /dev/null +++ b/docs/modules/ROOT/pages/includes/attributes.adoc @@ -0,0 +1,3 @@ +:project-version: 0 + +:examples-dir: ./../examples/ \ No newline at end of file diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc new file mode 100644 index 0000000..9890e35 --- /dev/null +++ b/docs/modules/ROOT/pages/index.adoc @@ -0,0 +1,27 @@ += Quarkus OpenFGA Client + +include::./includes/attributes.adoc[] + +TIP: Describe what the extension does here. + +== Installation + +If you want to use this extension, you need to add the `io.quarkiverse.openfga:quarkus-openfga-client` extension first to your build file. + +For instance, with Maven, add the following dependency to your POM file: + +[source,xml,subs=attributes+] +---- + + io.quarkiverse.openfga + quarkus-openfga-client + {project-version} + +---- + +[[extension-configuration-reference]] +== Extension Configuration Reference + +TIP: Remove this section if you don't have Quarkus configuration properties in your extension. + +include::includes/quarkus-openfga-client.adoc[leveloffset=+1, opts=optional] diff --git a/docs/pom.xml b/docs/pom.xml new file mode 100644 index 0000000..bf94a18 --- /dev/null +++ b/docs/pom.xml @@ -0,0 +1,106 @@ + + + 4.0.0 + + io.quarkiverse.openfga + quarkus-openfga-client-parent + 999-SNAPSHOT + ../pom.xml + + + quarkus-openfga-client-docs + Quarkus OpenFGA Client - Documentation + + + + + io.quarkiverse.openfga + quarkus-openfga-client-deployment + ${project.version} + + + + + modules/ROOT/examples + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + it.ozimov + yaml-properties-maven-plugin + + + initialize + + read-project-properties + + + + ${project.basedir}/../.github/project.yml + + + + + + + maven-resources-plugin + + + copy-resources + prepare-package + + copy-resources + + + ${project.basedir}/modules/ROOT/pages/includes/ + + + ${project.basedir}/../target/asciidoc/generated/config/ + quarkus-openfga-client.adoc + false + + + ${project.basedir}/templates/includes + attributes.adoc + true + + + + + + copy-images + prepare-package + + copy-resources + + + ${project.build.directory}/generated-docs/_images/ + + + ${project.basedir}/modules/ROOT/assets/images/ + false + + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + + + + + diff --git a/docs/templates/includes/attributes.adoc b/docs/templates/includes/attributes.adoc new file mode 100644 index 0000000..e1a2881 --- /dev/null +++ b/docs/templates/includes/attributes.adoc @@ -0,0 +1,3 @@ +:project-version: ${release.current-version} + +:examples-dir: ./../examples/ \ No newline at end of file diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml new file mode 100644 index 0000000..26ae3fd --- /dev/null +++ b/integration-tests/pom.xml @@ -0,0 +1,97 @@ + + + 4.0.0 + + io.quarkiverse.openfga + quarkus-openfga-client-parent + 999-SNAPSHOT + + quarkus-openfga-client-integration-tests + Quarkus OpenFGA Client - Integration Tests + + true + + + + io.quarkus + quarkus-resteasy-reactive + + + io.quarkus + quarkus-resteasy-reactive-jackson + + + io.quarkiverse.openfga + quarkus-openfga-client + ${project.version} + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + maven-failsafe-plugin + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + + native-image + + + native + + + + + + maven-surefire-plugin + + ${native.surefire.skip} + + + + + + false + native + + + + diff --git a/integration-tests/src/main/java/io/quarkiverse/openfga/it/OpenFGAResource.java b/integration-tests/src/main/java/io/quarkiverse/openfga/it/OpenFGAResource.java new file mode 100644 index 0000000..78cf565 --- /dev/null +++ b/integration-tests/src/main/java/io/quarkiverse/openfga/it/OpenFGAResource.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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. + */ +package io.quarkiverse.openfga.it; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; + +import java.util.List; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; + +import io.quarkiverse.openfga.client.StoreClient; +import io.quarkiverse.openfga.client.model.AuthorizationModel; +import io.smallrye.mutiny.Uni; + +@Path("/openfga") +@ApplicationScoped +public class OpenFGAResource { + + @Inject + StoreClient storeClient; + + @GET + @Path("authorization-models") + @Produces(APPLICATION_JSON) + public Uni> listModels() { + return storeClient.authorizationModels().listAll(); + } +} diff --git a/integration-tests/src/test/java/io/quarkiverse/openfga/it/OpenFGAResourceIT.java b/integration-tests/src/test/java/io/quarkiverse/openfga/it/OpenFGAResourceIT.java new file mode 100644 index 0000000..511a7f3 --- /dev/null +++ b/integration-tests/src/test/java/io/quarkiverse/openfga/it/OpenFGAResourceIT.java @@ -0,0 +1,7 @@ +package io.quarkiverse.openfga.it; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class OpenFGAResourceIT extends OpenFGAResourceTest { +} diff --git a/integration-tests/src/test/java/io/quarkiverse/openfga/it/OpenFGAResourceTest.java b/integration-tests/src/test/java/io/quarkiverse/openfga/it/OpenFGAResourceTest.java new file mode 100644 index 0000000..202fe2d --- /dev/null +++ b/integration-tests/src/test/java/io/quarkiverse/openfga/it/OpenFGAResourceTest.java @@ -0,0 +1,24 @@ +package io.quarkiverse.openfga.it; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.hasSize; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class OpenFGAResourceTest { + + @Test + public void testHelloEndpoint() { + + given() + .when().get("/openfga/authorization-models") + .then() + .log().ifValidationFails() + .statusCode(200) + .contentType("application/json") + .body("$", hasSize(1)); + } +} diff --git a/integration-tests/src/test/resources/application.properties b/integration-tests/src/test/resources/application.properties new file mode 100644 index 0000000..8a3b205 --- /dev/null +++ b/integration-tests/src/test/resources/application.properties @@ -0,0 +1 @@ +quarkus.openfga.devservices.authorization-model-location=classpath:auth-model.json diff --git a/integration-tests/src/test/resources/auth-model.json b/integration-tests/src/test/resources/auth-model.json new file mode 100644 index 0000000..6451890 --- /dev/null +++ b/integration-tests/src/test/resources/auth-model.json @@ -0,0 +1,18 @@ +{ + "type_definitions": [ + { + "type": "thing", + "relations": { + "reader": { + "this": {} + }, + "writer": { + "this": {} + }, + "owner": { + "this": {} + } + } + } + ] +} diff --git a/model/pom.xml b/model/pom.xml new file mode 100644 index 0000000..5ec7b6a --- /dev/null +++ b/model/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + io.quarkiverse.openfga + quarkus-openfga-client-parent + 999-SNAPSHOT + + quarkus-openfga-client-model + Quarkus OpenFGA Client - Model + OpenFGA Client API model classes + + + + io.quarkus + quarkus-jackson + + + + com.google.code.findbugs + jsr305 + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + + + + org.jboss.jandex + jandex-maven-plugin + ${jandex-maven-plugin.version} + + + make-index + + jandex + + + + + + + diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/Assertion.java b/model/src/main/java/io/quarkiverse/openfga/client/model/Assertion.java new file mode 100644 index 0000000..ca32cb3 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/Assertion.java @@ -0,0 +1,50 @@ +package io.quarkiverse.openfga.client.model; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class Assertion { + private final TupleKey tupleKey; + private final boolean expectation; + + public Assertion(@JsonProperty("tuple_key") TupleKey tupleKey, boolean expectation) { + this.tupleKey = Preconditions.parameterNonNull(tupleKey, "tupleKey"); + this.expectation = expectation; + } + + @JsonProperty("tuple_key") + public TupleKey getTupleKey() { + return tupleKey; + } + + public boolean getExpectation() { + return expectation; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (Assertion) obj; + return Objects.equals(this.tupleKey, that.tupleKey) && + this.expectation == that.expectation; + } + + @Override + public int hashCode() { + return Objects.hash(tupleKey, expectation); + } + + @Override + public String toString() { + return "Assertion[" + + "tupleKey=" + tupleKey + ", " + + "expectation=" + expectation + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/AuthorizationModel.java b/model/src/main/java/io/quarkiverse/openfga/client/model/AuthorizationModel.java new file mode 100644 index 0000000..c33f6f4 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/AuthorizationModel.java @@ -0,0 +1,51 @@ +package io.quarkiverse.openfga.client.model; + +import java.util.List; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class AuthorizationModel { + private final String id; + private final List typeDefinitions; + + public AuthorizationModel(String id, @JsonProperty("type_definitions") List typeDefinitions) { + this.id = Preconditions.parameterNonNull(id, "id"); + this.typeDefinitions = Preconditions.parameterNonNull(typeDefinitions, "typeDefinitions"); + } + + public String getId() { + return id; + } + + @JsonProperty("type_definitions") + public List getTypeDefinitions() { + return typeDefinitions; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (AuthorizationModel) obj; + return Objects.equals(this.id, that.id) && + Objects.equals(this.typeDefinitions, that.typeDefinitions); + } + + @Override + public int hashCode() { + return Objects.hash(id, typeDefinitions); + } + + @Override + public String toString() { + return "AuthorizationModel[" + + "id=" + id + ", " + + "typeDefinitions=" + typeDefinitions + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/ContextualTupleKeys.java b/model/src/main/java/io/quarkiverse/openfga/client/model/ContextualTupleKeys.java new file mode 100644 index 0000000..00d2c50 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/ContextualTupleKeys.java @@ -0,0 +1,44 @@ +package io.quarkiverse.openfga.client.model; + +import java.util.List; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class ContextualTupleKeys { + @JsonProperty("tuple_keys") + private final List tupleKeys; + + public ContextualTupleKeys(@JsonProperty("tuple_keys") List tupleKeys) { + this.tupleKeys = Preconditions.parameterNonNull(tupleKeys, "tupleKeys"); + } + + @JsonProperty("tuple_keys") + public List getTupleKeys() { + return tupleKeys; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (ContextualTupleKeys) obj; + return Objects.equals(this.tupleKeys, that.tupleKeys); + } + + @Override + public int hashCode() { + return Objects.hash(tupleKeys); + } + + @Override + public String toString() { + return "ContextualTupleKeys[" + + "tupleKeys=" + tupleKeys + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/DirectUserset.java b/model/src/main/java/io/quarkiverse/openfga/client/model/DirectUserset.java new file mode 100644 index 0000000..4ceb801 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/DirectUserset.java @@ -0,0 +1,6 @@ +package io.quarkiverse.openfga.client.model; + +import java.util.HashMap; + +public class DirectUserset extends HashMap { +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/FGAException.java b/model/src/main/java/io/quarkiverse/openfga/client/model/FGAException.java new file mode 100644 index 0000000..bc2950d --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/FGAException.java @@ -0,0 +1,166 @@ +package io.quarkiverse.openfga.client.model; + +import javax.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class FGAException extends Exception { + + public enum Code { + UNKNOWN_ERROR, + + @JsonProperty("no_error") + NO_ERROR, + + @JsonProperty("validation_error") + VALIDATION_ERROR, + + @JsonProperty("authorization_model_not_found") + AUTHORIZATION_MODEL_NOT_FOUND, + + @JsonProperty("authorization_model_resolution_too_complex") + AUTHORIZATION_MODEL_RESOLUTION_TOO_COMPLEX, + + @JsonProperty("invalid_write_input") + INVALID_WRITE_INPUT, + + @JsonProperty("cannot_allow_duplicate_tuples_in_one_request") + CANNOT_ALLOW_DUPLICATE_TUPLES_IN_ONE_REQUEST, + + @JsonProperty("cannot_allow_duplicate_types_in_one_request") + CANNOT_ALLOW_DUPLICATE_TYPES_IN_ONE_REQUEST, + + @JsonProperty("cannot_allow_multiple_references_to_one_relation") + CANNOT_ALLOW_MULTIPLE_REFERENCES_TO_ONE_RELATION, + + @JsonProperty("invalid_continuation_token") + INVALID_CONTINUATION_TOKEN, + + @JsonProperty("invalid_tuple_set") + INVALID_TUPLE_SET, + + @JsonProperty("invalid_check_input") + INVALID_CHECK_INPUT, + + @JsonProperty("invalid_expand_input") + INVALID_EXPAND_INPUT, + + @JsonProperty("unsupported_user_set") + UNSUPPORTED_USER_SET, + + @JsonProperty("invalid_object_format") + INVALID_OBJECT_FORMAT, + + @JsonProperty("write_failed_due_to_invalid_input") + WRITE_FAILED_DUE_TO_INVALID_INPUT, + + @JsonProperty("authorization_model_assertions_not_found") + AUTHORIZATION_MODEL_ASSERTIONS_NOT_FOUND, + + @JsonProperty("latest_authorization_model_not_found") + LATEST_AUTHORIZATION_MODEL_NOT_FOUND, + + @JsonProperty("type_not_found") + TYPE_NOT_FOUND, + + @JsonProperty("relation_not_found") + RELATION_NOT_FOUND, + + @JsonProperty("empty_relation_definition") + EMPTY_RELATION_DEFINITION, + + @JsonProperty("invalid_user") + INVALID_USER, + + @JsonProperty("invalid_tuple") + INVALID_TUPLE, + + @JsonProperty("unknown_relation") + UNKNOWN_RELATION, + + @JsonProperty("store_id_invalid_length") + STORE_ID_INVALID_LENGTH, + + @JsonProperty("assertions_too_many_items") + ASSERTIONS_TOO_MANY_ITEMS, + + @JsonProperty("id_too_long") + ID_TOO_LONG, + + @JsonProperty("authorization_model_id_too_long") + AUTHORIZATION_MODEL_ID_TOO_LONG, + + @JsonProperty("tuple_key_value_not_specified") + TUPLE_KEY_VALUE_NOT_SPECIFIED, + + @JsonProperty("tuple_keys_too_many_or_too_few_items") + TUPLE_KEYS_TOO_MANY_OR_TOO_FEW_ITEMS, + + @JsonProperty("page_size_invalid") + PAGE_SIZE_INVALID, + + @JsonProperty("param_missing_value") + PARAM_MISSING_VALUE, + + @JsonProperty("difference_base_missing_value") + DIFFERENCE_BASE_MISSING_VALUE, + + @JsonProperty("subtract_base_missing_value") + SUBTRACT_BASE_MISSING_VALUE, + + @JsonProperty("object_too_long") + OBJECT_TOO_LONG, + + @JsonProperty("relation_too_long") + RELATION_TOO_LONG, + + @JsonProperty("type_definitions_too_few_items") + TYPE_DEFINITIONS_TOO_FEW_ITEMS, + + @JsonProperty("type_invalid_length") + TYPE_INVALID_LENGTH, + + @JsonProperty("type_invalid_pattern") + TYPE_INVALID_PATTERN, + + @JsonProperty("relations_too_few_items") + RELATIONS_TOO_FEW_ITEMS, + + @JsonProperty("relations_too_long") + RELATIONS_TOO_LONG, + + @JsonProperty("relations_invalid_pattern") + RELATIONS_INVALID_PATTERN, + + @JsonProperty("object_invalid_pattern") + OBJECT_INVALID_PATTERN, + + @JsonProperty("query_string_type_continuation_token_mismatch") + QUERY_STRING_TYPE_CONTINUATION_TOKEN_MISMATCH, + + @JsonProperty("exceeded_entity_limit") + EXCEEDED_ENTITY_LIMIT, + + @JsonProperty("invalid_contextual_tuple") + INVALID_CONTEXTUAL_TUPLE, + + @JsonProperty("duplicate_contextual_tuple") + DUPLICATE_CONTEXTUAL_TUPLE + } + + private final Code code; + + public FGAException(Code code, @Nullable String message) { + super(message); + this.code = code; + } + + public Code getcode() { + return code; + } + + @Nullable + public String message() { + return getMessage(); + } +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/ObjectRelation.java b/model/src/main/java/io/quarkiverse/openfga/client/model/ObjectRelation.java new file mode 100644 index 0000000..43bc2e6 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/ObjectRelation.java @@ -0,0 +1,47 @@ +package io.quarkiverse.openfga.client.model; + +import java.util.Objects; + +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class ObjectRelation { + private final String object; + private final String relation; + + public ObjectRelation(String object, String relation) { + this.object = Preconditions.parameterNonNull(object, "object"); + this.relation = Preconditions.parameterNonNull(relation, "relation"); + } + + public String getObject() { + return object; + } + + public String getRelation() { + return relation; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (ObjectRelation) obj; + return Objects.equals(this.object, that.object) && + Objects.equals(this.relation, that.relation); + } + + @Override + public int hashCode() { + return Objects.hash(object, relation); + } + + @Override + public String toString() { + return "ObjectRelation[" + + "object=" + object + ", " + + "relation=" + relation + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/PartialTupleKey.java b/model/src/main/java/io/quarkiverse/openfga/client/model/PartialTupleKey.java new file mode 100644 index 0000000..3517ab1 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/PartialTupleKey.java @@ -0,0 +1,68 @@ +package io.quarkiverse.openfga.client.model; + +import java.util.Objects; + +import javax.annotation.Nullable; + +public final class PartialTupleKey { + @Nullable + private final String object; + @Nullable + private final String relation; + @Nullable + private final String user; + + public PartialTupleKey( + @Nullable String object, + @Nullable String relation, + @Nullable String user) { + this.object = object; + this.relation = relation; + this.user = user; + } + + public static PartialTupleKey of(@Nullable String object, @Nullable String relation, @Nullable String user) { + return new PartialTupleKey(object, relation, user); + } + + @Nullable + public String getObject() { + return object; + } + + @Nullable + public String getRelation() { + return relation; + } + + @Nullable + public String getUser() { + return user; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (PartialTupleKey) obj; + return Objects.equals(this.object, that.object) && + Objects.equals(this.relation, that.relation) && + Objects.equals(this.user, that.user); + } + + @Override + public int hashCode() { + return Objects.hash(object, relation, user); + } + + @Override + public String toString() { + return "PartialTupleKey[" + + "object=" + object + ", " + + "relation=" + relation + ", " + + "user=" + user + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/Store.java b/model/src/main/java/io/quarkiverse/openfga/client/model/Store.java new file mode 100644 index 0000000..57c963d --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/Store.java @@ -0,0 +1,86 @@ +package io.quarkiverse.openfga.client.model; + +import java.time.OffsetDateTime; +import java.util.Objects; + +import javax.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class Store { + private final String id; + private final String name; + @JsonProperty("created_at") + private final OffsetDateTime createdAt; + @JsonProperty("updated_at") + private final OffsetDateTime updatedAt; + @JsonProperty("deleted_at") + @Nullable + private final OffsetDateTime deletedAt; + + public Store(String id, String name, @JsonProperty("created_at") OffsetDateTime createdAt, + @JsonProperty("updated_at") OffsetDateTime updatedAt, + @JsonProperty("deleted_at") @Nullable OffsetDateTime deletedAt) { + this.id = Preconditions.parameterNonNull(id, "id"); + this.name = Preconditions.parameterNonNull(name, "name"); + this.createdAt = Preconditions.parameterNonNull(createdAt, "createdAt"); + this.updatedAt = Preconditions.parameterNonNull(updatedAt, "updatedAt"); + this.deletedAt = deletedAt; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + @JsonProperty("created_at") + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + @JsonProperty("updated_at") + public OffsetDateTime getUpdatedAt() { + return updatedAt; + } + + @JsonProperty("deleted_at") + @Nullable + public OffsetDateTime getDeletedAt() { + return deletedAt; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (Store) obj; + return Objects.equals(this.id, that.id) && + Objects.equals(this.name, that.name) && + Objects.equals(this.createdAt, that.createdAt) && + Objects.equals(this.updatedAt, that.updatedAt) && + Objects.equals(this.deletedAt, that.deletedAt); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, createdAt, updatedAt, deletedAt); + } + + @Override + public String toString() { + return "Store[" + + "id=" + id + ", " + + "name=" + name + ", " + + "createdAt=" + createdAt + ", " + + "updatedAt=" + updatedAt + ", " + + "deletedAt=" + deletedAt + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/Tuple.java b/model/src/main/java/io/quarkiverse/openfga/client/model/Tuple.java new file mode 100644 index 0000000..e31381e --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/Tuple.java @@ -0,0 +1,48 @@ +package io.quarkiverse.openfga.client.model; + +import java.time.OffsetDateTime; +import java.util.Objects; + +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class Tuple { + private final TupleKey key; + private final OffsetDateTime timestamp; + + public Tuple(TupleKey key, OffsetDateTime timestamp) { + this.key = Preconditions.parameterNonNull(key, "key"); + this.timestamp = Preconditions.parameterNonNull(timestamp, "timestamp"); + } + + public TupleKey getKey() { + return key; + } + + public OffsetDateTime getTimestamp() { + return timestamp; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (Tuple) obj; + return Objects.equals(this.key, that.key) && + Objects.equals(this.timestamp, that.timestamp); + } + + @Override + public int hashCode() { + return Objects.hash(key, timestamp); + } + + @Override + public String toString() { + return "Tuple[" + + "key=" + key + ", " + + "timestamp=" + timestamp + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/TupleChange.java b/model/src/main/java/io/quarkiverse/openfga/client/model/TupleChange.java new file mode 100644 index 0000000..ef50d6e --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/TupleChange.java @@ -0,0 +1,59 @@ +package io.quarkiverse.openfga.client.model; + +import java.time.OffsetDateTime; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class TupleChange { + private final TupleKey tupleKey; + private final TupleOperation operation; + private final OffsetDateTime timestamp; + + public TupleChange(@JsonProperty("tuple_key") TupleKey tupleKey, TupleOperation operation, OffsetDateTime timestamp) { + this.tupleKey = Preconditions.parameterNonNull(tupleKey, "tupleKey"); + this.operation = Preconditions.parameterNonNull(operation, "operation"); + this.timestamp = Preconditions.parameterNonNull(timestamp, "timestamp"); + } + + @JsonProperty("tuple_key") + public TupleKey getTupleKey() { + return tupleKey; + } + + public TupleOperation getOperation() { + return operation; + } + + public OffsetDateTime getTimestamp() { + return timestamp; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (TupleChange) obj; + return Objects.equals(this.tupleKey, that.tupleKey) && + Objects.equals(this.operation, that.operation) && + Objects.equals(this.timestamp, that.timestamp); + } + + @Override + public int hashCode() { + return Objects.hash(tupleKey, operation, timestamp); + } + + @Override + public String toString() { + return "TupleChange[" + + "tupleKey=" + tupleKey + ", " + + "operation=" + operation + ", " + + "timestamp=" + timestamp + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/TupleKey.java b/model/src/main/java/io/quarkiverse/openfga/client/model/TupleKey.java new file mode 100644 index 0000000..882e51c --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/TupleKey.java @@ -0,0 +1,59 @@ +package io.quarkiverse.openfga.client.model; + +import java.util.Objects; + +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class TupleKey { + private final String object; + private final String relation; + private final String user; + + public TupleKey(String object, String relation, String user) { + this.object = Preconditions.parameterNonNull(object, "object"); + this.relation = Preconditions.parameterNonNull(relation, "relation"); + this.user = Preconditions.parameterNonNull(user, "user"); + } + + public static TupleKey of(String object, String relation, String user) { + return new TupleKey(object, relation, user); + } + + public String getObject() { + return object; + } + + public String getRelation() { + return relation; + } + + public String getUser() { + return user; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (TupleKey) obj; + return Objects.equals(this.object, that.object) && + Objects.equals(this.relation, that.relation) && + Objects.equals(this.user, that.user); + } + + @Override + public int hashCode() { + return Objects.hash(object, relation, user); + } + + @Override + public String toString() { + return "TupleKey[" + + "object=" + object + ", " + + "relation=" + relation + ", " + + "user=" + user + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/TupleKeys.java b/model/src/main/java/io/quarkiverse/openfga/client/model/TupleKeys.java new file mode 100644 index 0000000..f7ae690 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/TupleKeys.java @@ -0,0 +1,56 @@ +package io.quarkiverse.openfga.client.model; + +import java.util.List; +import java.util.Objects; + +import javax.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class TupleKeys { + private final List tupleKeys; + + public TupleKeys(@JsonProperty("tuple_keys") List tupleKeys) { + if (tupleKeys.isEmpty()) { + throw new IllegalStateException("tupleKeys requires a minimum of 1 item"); + } + this.tupleKeys = Preconditions.parameterNonNull(tupleKeys, "tupleKeys"); + ; + } + + public static TupleKeys of(@Nullable List tupleKeys) { + if (tupleKeys == null || tupleKeys.isEmpty()) { + return null; + } + return new TupleKeys(tupleKeys); + } + + @JsonProperty("tuple_keys") + public List getTupleKeys() { + return tupleKeys; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (TupleKeys) obj; + return Objects.equals(this.tupleKeys, that.tupleKeys); + } + + @Override + public int hashCode() { + return Objects.hash(tupleKeys); + } + + @Override + public String toString() { + return "TupleKeys[" + + "tupleKeys=" + tupleKeys + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/TupleOperation.java b/model/src/main/java/io/quarkiverse/openfga/client/model/TupleOperation.java new file mode 100644 index 0000000..9419aa8 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/TupleOperation.java @@ -0,0 +1,10 @@ +package io.quarkiverse.openfga.client.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public enum TupleOperation { + @JsonProperty("TUPLE_OPERATION_WRITE") + WRITE, + @JsonProperty("TUPLE_OPERATION_DELETE") + DELETE, +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/TypeDefinition.java b/model/src/main/java/io/quarkiverse/openfga/client/model/TypeDefinition.java new file mode 100644 index 0000000..87b9277 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/TypeDefinition.java @@ -0,0 +1,48 @@ +package io.quarkiverse.openfga.client.model; + +import java.util.Map; +import java.util.Objects; + +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class TypeDefinition { + private final String type; + private final Map relations; + + public TypeDefinition(String type, Map relations) { + this.type = Preconditions.parameterNonNull(type, "type"); + this.relations = Preconditions.parameterNonNull(relations, "relations"); + } + + public String getType() { + return type; + } + + public Map getRelations() { + return relations; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (TypeDefinition) obj; + return Objects.equals(this.type, that.type) && + Objects.equals(this.relations, that.relations); + } + + @Override + public int hashCode() { + return Objects.hash(type, relations); + } + + @Override + public String toString() { + return "TypeDefinition[" + + "type=" + type + ", " + + "relations=" + relations + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/TypeDefinitions.java b/model/src/main/java/io/quarkiverse/openfga/client/model/TypeDefinitions.java new file mode 100644 index 0000000..b12d4f8 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/TypeDefinitions.java @@ -0,0 +1,44 @@ +package io.quarkiverse.openfga.client.model; + +import java.util.List; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class TypeDefinitions { + @JsonProperty("type_definitions") + private final List typeDefinitions; + + public TypeDefinitions(@JsonProperty("type_definitions") List typeDefinitions) { + this.typeDefinitions = Preconditions.parameterNonNull(typeDefinitions, "typeDefinitions"); + } + + @JsonProperty("type_definitions") + public List getTypeDefinitions() { + return typeDefinitions; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (TypeDefinitions) obj; + return Objects.equals(this.typeDefinitions, that.typeDefinitions); + } + + @Override + public int hashCode() { + return Objects.hash(typeDefinitions); + } + + @Override + public String toString() { + return "TypeDefinitions[" + + "typeDefinitions=" + typeDefinitions + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/Userset.java b/model/src/main/java/io/quarkiverse/openfga/client/model/Userset.java new file mode 100644 index 0000000..b05118a --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/Userset.java @@ -0,0 +1,127 @@ +package io.quarkiverse.openfga.client.model; + +import java.util.Objects; + +import javax.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkiverse.openfga.client.model.nodes.V1; + +public final class Userset { + @Nullable + @JsonProperty("this") + private final DirectUserset directUserset; + @Nullable + private final ObjectRelation computedUserset; + @Nullable + private final V1.TupleToUserset tupleToUserset; + @Nullable + private final Usersets union; + @Nullable + private final Usersets intersection; + @Nullable + private final V1.Difference difference; + + public Userset( + @Nullable @JsonProperty("this") DirectUserset directUserset, + @Nullable ObjectRelation computedUserset, + @Nullable V1.TupleToUserset tupleToUserset, + @Nullable Usersets union, + @Nullable Usersets intersection, + @Nullable V1.Difference difference) { + this.directUserset = directUserset; + this.computedUserset = computedUserset; + this.tupleToUserset = tupleToUserset; + this.union = union; + this.intersection = intersection; + this.difference = difference; + } + + public static Userset direct() { + return new Userset(new DirectUserset(), null, null, null, null, null); + } + + public static Userset computed(ObjectRelation computedUserset) { + return new Userset(null, computedUserset, null, null, null, null); + } + + public static Userset tupleTo(V1.TupleToUserset tupleToUserset) { + return new Userset(null, null, tupleToUserset, null, null, null); + } + + public static Userset union(Usersets union) { + return new Userset(null, null, null, union, null, null); + } + + public static Userset intersection(Usersets intersection) { + return new Userset(null, null, null, null, intersection, null); + } + + public static Userset difference(V1.Difference difference) { + return new Userset(null, null, null, null, null, difference); + } + + @Nullable + @JsonProperty("this") + public DirectUserset getDirectUserset() { + return directUserset; + } + + @Nullable + public ObjectRelation getComputedUserset() { + return computedUserset; + } + + @Nullable + public V1.TupleToUserset getTupleToUserset() { + return tupleToUserset; + } + + @Nullable + public Usersets getUnion() { + return union; + } + + @Nullable + public Usersets getIntersection() { + return intersection; + } + + @Nullable + public V1.Difference getDifference() { + return difference; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (Userset) obj; + return Objects.equals(this.directUserset, that.directUserset) && + Objects.equals(this.computedUserset, that.computedUserset) && + Objects.equals(this.tupleToUserset, that.tupleToUserset) && + Objects.equals(this.union, that.union) && + Objects.equals(this.intersection, that.intersection) && + Objects.equals(this.difference, that.difference); + } + + @Override + public int hashCode() { + return Objects.hash(directUserset, computedUserset, tupleToUserset, union, intersection, difference); + } + + @Override + public String toString() { + return "Userset[" + + "directUserset=" + directUserset + ", " + + "computedUserset=" + computedUserset + ", " + + "tupleToUserset=" + tupleToUserset + ", " + + "union=" + union + ", " + + "intersection=" + intersection + ", " + + "difference=" + difference + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/UsersetTree.java b/model/src/main/java/io/quarkiverse/openfga/client/model/UsersetTree.java new file mode 100644 index 0000000..69db8f7 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/UsersetTree.java @@ -0,0 +1,40 @@ +package io.quarkiverse.openfga.client.model; + +import java.util.Objects; + +import io.quarkiverse.openfga.client.model.nodes.Node; +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class UsersetTree { + private final Node root; + + public UsersetTree(Node root) { + this.root = Preconditions.parameterNonNull(root, "root"); + } + + public Node getRoot() { + return root; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (UsersetTree) obj; + return Objects.equals(this.root, that.root); + } + + @Override + public int hashCode() { + return Objects.hash(root); + } + + @Override + public String toString() { + return "UsersetTree[" + + "root=" + root + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/Usersets.java b/model/src/main/java/io/quarkiverse/openfga/client/model/Usersets.java new file mode 100644 index 0000000..c192e7b --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/Usersets.java @@ -0,0 +1,40 @@ +package io.quarkiverse.openfga.client.model; + +import java.util.List; +import java.util.Objects; + +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class Usersets { + private final List child; + + public Usersets(List child) { + this.child = Preconditions.parameterNonNull(child, "child"); + } + + public List getChild() { + return child; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (Usersets) obj; + return Objects.equals(this.child, that.child); + } + + @Override + public int hashCode() { + return Objects.hash(child); + } + + @Override + public String toString() { + return "Usersets[" + + "child=" + child + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/dto/CheckBody.java b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/CheckBody.java new file mode 100644 index 0000000..4050699 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/CheckBody.java @@ -0,0 +1,83 @@ +package io.quarkiverse.openfga.client.model.dto; + +import java.util.Objects; + +import javax.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkiverse.openfga.client.model.ContextualTupleKeys; +import io.quarkiverse.openfga.client.model.TupleKey; +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class CheckBody { + @JsonProperty("tuple_key") + private final TupleKey tupleKey; + @JsonProperty("contextual_tuples") + @Nullable + private final ContextualTupleKeys contextualTupleKeys; + @JsonProperty("authorization_model_id") + @Nullable + private final String authorizationModelId; + @Nullable + private final Boolean trace; + + public CheckBody(@JsonProperty("tuple_key") TupleKey tupleKey, + @JsonProperty("contextual_tuples") @Nullable ContextualTupleKeys contextualTupleKeys, + @JsonProperty("authorization_model_id") @Nullable String authorizationModelId, @Nullable Boolean trace) { + this.tupleKey = Preconditions.parameterNonNull(tupleKey, "tupleKey"); + this.contextualTupleKeys = contextualTupleKeys; + this.authorizationModelId = authorizationModelId; + this.trace = trace; + } + + @JsonProperty("tuple_key") + public TupleKey getTupleKey() { + return tupleKey; + } + + @JsonProperty("contextual_tuples") + @Nullable + public ContextualTupleKeys getContextualTupleKeys() { + return contextualTupleKeys; + } + + @JsonProperty("authorization_model_id") + @Nullable + public String getAuthorizationModelId() { + return authorizationModelId; + } + + @Nullable + public Boolean getTrace() { + return trace; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (CheckBody) obj; + return Objects.equals(this.tupleKey, that.tupleKey) && + Objects.equals(this.contextualTupleKeys, that.contextualTupleKeys) && + Objects.equals(this.authorizationModelId, that.authorizationModelId) && + Objects.equals(this.trace, that.trace); + } + + @Override + public int hashCode() { + return Objects.hash(tupleKey, contextualTupleKeys, authorizationModelId, trace); + } + + @Override + public String toString() { + return "CheckBody[" + + "tupleKey=" + tupleKey + ", " + + "contextualTupleKeys=" + contextualTupleKeys + ", " + + "authorizationModelId=" + authorizationModelId + ", " + + "trace=" + trace + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/dto/CheckResponse.java b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/CheckResponse.java new file mode 100644 index 0000000..12c1de8 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/CheckResponse.java @@ -0,0 +1,49 @@ +package io.quarkiverse.openfga.client.model.dto; + +import java.util.Objects; + +import javax.annotation.Nullable; + +public final class CheckResponse { + private final boolean allowed; + @Nullable + private final String resolution; + + public CheckResponse(boolean allowed, @Nullable String resolution) { + this.allowed = allowed; + this.resolution = resolution; + } + + public boolean getAllowed() { + return allowed; + } + + @Nullable + public String getResolution() { + return resolution; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (CheckResponse) obj; + return this.allowed == that.allowed && + Objects.equals(this.resolution, that.resolution); + } + + @Override + public int hashCode() { + return Objects.hash(allowed, resolution); + } + + @Override + public String toString() { + return "CheckResponse[" + + "allowed=" + allowed + ", " + + "resolution=" + resolution + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/dto/CreateStoreRequest.java b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/CreateStoreRequest.java new file mode 100644 index 0000000..45403e2 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/CreateStoreRequest.java @@ -0,0 +1,39 @@ +package io.quarkiverse.openfga.client.model.dto; + +import java.util.Objects; + +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class CreateStoreRequest { + private final String name; + + public CreateStoreRequest(String name) { + this.name = Preconditions.parameterNonNull(name, "name"); + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (CreateStoreRequest) obj; + return Objects.equals(this.name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + @Override + public String toString() { + return "CreateStoreRequest[" + + "name=" + name + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/dto/CreateStoreResponse.java b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/CreateStoreResponse.java new file mode 100644 index 0000000..413a1b5 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/CreateStoreResponse.java @@ -0,0 +1,76 @@ +package io.quarkiverse.openfga.client.model.dto; + +import java.time.OffsetDateTime; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkiverse.openfga.client.model.Store; +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class CreateStoreResponse { + private final String id; + private final String name; + @JsonProperty("created_at") + private final OffsetDateTime createdAt; + @JsonProperty("updated_at") + private final OffsetDateTime updatedAt; + + public CreateStoreResponse(String id, String name, @JsonProperty("created_at") OffsetDateTime createdAt, + @JsonProperty("updated_at") OffsetDateTime updatedAt) { + this.id = Preconditions.parameterNonNull(id, "id"); + this.name = Preconditions.parameterNonNull(name, "name"); + this.createdAt = Preconditions.parameterNonNull(createdAt, "createdAt"); + this.updatedAt = Preconditions.parameterNonNull(updatedAt, "updatedAt"); + } + + public Store asStore() { + return new Store(id, name, createdAt, updatedAt, null); + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + @JsonProperty("created_at") + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + @JsonProperty("updated_at") + public OffsetDateTime getUpdatedAt() { + return updatedAt; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (CreateStoreResponse) obj; + return Objects.equals(this.id, that.id) && + Objects.equals(this.name, that.name) && + Objects.equals(this.createdAt, that.createdAt) && + Objects.equals(this.updatedAt, that.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, createdAt, updatedAt); + } + + @Override + public String toString() { + return "CreateStoreResponse[" + + "id=" + id + ", " + + "name=" + name + ", " + + "createdAt=" + createdAt + ", " + + "updatedAt=" + updatedAt + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ExpandBody.java b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ExpandBody.java new file mode 100644 index 0000000..2219a34 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ExpandBody.java @@ -0,0 +1,59 @@ +package io.quarkiverse.openfga.client.model.dto; + +import java.util.Objects; + +import javax.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkiverse.openfga.client.model.TupleKey; +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class ExpandBody { + @JsonProperty("tuple_key") + private final TupleKey tupleKey; + @JsonProperty("authorization_model_id") + @Nullable + private final String authorizationModelId; + + public ExpandBody(@JsonProperty("tuple_key") TupleKey tupleKey, + @JsonProperty("authorization_model_id") @Nullable String authorizationModelId) { + this.tupleKey = Preconditions.parameterNonNull(tupleKey, "tupleKey"); + this.authorizationModelId = authorizationModelId; + } + + @JsonProperty("tuple_key") + public TupleKey getTupleKey() { + return tupleKey; + } + + @JsonProperty("authorization_model_id") + @Nullable + public String getAuthorizationModelId() { + return authorizationModelId; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (ExpandBody) obj; + return Objects.equals(this.tupleKey, that.tupleKey) && + Objects.equals(this.authorizationModelId, that.authorizationModelId); + } + + @Override + public int hashCode() { + return Objects.hash(tupleKey, authorizationModelId); + } + + @Override + public String toString() { + return "ExpandBody[" + + "tupleKey=" + tupleKey + ", " + + "authorizationModelId=" + authorizationModelId + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ExpandResponse.java b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ExpandResponse.java new file mode 100644 index 0000000..26ba37b --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ExpandResponse.java @@ -0,0 +1,40 @@ +package io.quarkiverse.openfga.client.model.dto; + +import java.util.Objects; + +import io.quarkiverse.openfga.client.model.UsersetTree; +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class ExpandResponse { + private final UsersetTree tree; + + public ExpandResponse(UsersetTree tree) { + this.tree = Preconditions.parameterNonNull(tree, "tree"); + } + + public UsersetTree getTree() { + return tree; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (ExpandResponse) obj; + return Objects.equals(this.tree, that.tree); + } + + @Override + public int hashCode() { + return Objects.hash(tree); + } + + @Override + public String toString() { + return "ExpandResponse[" + + "tree=" + tree + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/dto/GetStoreResponse.java b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/GetStoreResponse.java new file mode 100644 index 0000000..b59a7d2 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/GetStoreResponse.java @@ -0,0 +1,76 @@ +package io.quarkiverse.openfga.client.model.dto; + +import java.time.OffsetDateTime; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkiverse.openfga.client.model.Store; +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class GetStoreResponse { + private final String id; + private final String name; + @JsonProperty("created_at") + private final OffsetDateTime createdAt; + @JsonProperty("updated_at") + private final OffsetDateTime updatedAt; + + public GetStoreResponse(String id, String name, @JsonProperty("created_at") OffsetDateTime createdAt, + @JsonProperty("updated_at") OffsetDateTime updatedAt) { + this.id = Preconditions.parameterNonNull(id, "id"); + this.name = Preconditions.parameterNonNull(name, "name"); + this.createdAt = Preconditions.parameterNonNull(createdAt, "createdAt"); + this.updatedAt = Preconditions.parameterNonNull(updatedAt, "updatedAt"); + } + + public Store asStore() { + return new Store(id, name, createdAt, updatedAt, null); + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + @JsonProperty("created_at") + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + @JsonProperty("updated_at") + public OffsetDateTime getUpdatedAt() { + return updatedAt; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (GetStoreResponse) obj; + return Objects.equals(this.id, that.id) && + Objects.equals(this.name, that.name) && + Objects.equals(this.createdAt, that.createdAt) && + Objects.equals(this.updatedAt, that.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, createdAt, updatedAt); + } + + @Override + public String toString() { + return "GetStoreResponse[" + + "id=" + id + ", " + + "name=" + name + ", " + + "createdAt=" + createdAt + ", " + + "updatedAt=" + updatedAt + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ListObjectsBody.java b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ListObjectsBody.java new file mode 100644 index 0000000..a01eb6a --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ListObjectsBody.java @@ -0,0 +1,91 @@ +package io.quarkiverse.openfga.client.model.dto; + +import java.util.Objects; + +import javax.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkiverse.openfga.client.model.ContextualTupleKeys; + +public final class ListObjectsBody { + @JsonProperty("authorization_model_id") + @Nullable + private final String authorizationModelId; + @Nullable + private final String type; + @Nullable + private final String relation; + @Nullable + private final String user; + @JsonProperty("contextual_tuples") + @Nullable + private final ContextualTupleKeys contextualTupleKeys; + + public ListObjectsBody(@JsonProperty("authorization_model_id") @Nullable String authorizationModelId, + @Nullable String type, @Nullable String relation, @Nullable String user, + @Nullable @JsonProperty("contextual_tuples") ContextualTupleKeys contextualTupleKeys) { + this.authorizationModelId = authorizationModelId; + this.type = type; + this.relation = relation; + this.user = user; + this.contextualTupleKeys = contextualTupleKeys; + } + + @JsonProperty("authorization_model_id") + @Nullable + public String getAuthorizationModelId() { + return authorizationModelId; + } + + @Nullable + public String getType() { + return type; + } + + @Nullable + public String getRelation() { + return relation; + } + + @Nullable + public String getUser() { + return user; + } + + @JsonProperty("contextual_tuples") + @Nullable + public ContextualTupleKeys getContextualTupleKeys() { + return contextualTupleKeys; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (ListObjectsBody) obj; + return Objects.equals(this.authorizationModelId, that.authorizationModelId) && + Objects.equals(this.type, that.type) && + Objects.equals(this.relation, that.relation) && + Objects.equals(this.user, that.user) && + Objects.equals(this.contextualTupleKeys, that.contextualTupleKeys); + } + + @Override + public int hashCode() { + return Objects.hash(authorizationModelId, type, relation, user, contextualTupleKeys); + } + + @Override + public String toString() { + return "ListObjectsBody[" + + "authorizationModelId=" + authorizationModelId + ", " + + "type=" + type + ", " + + "relation=" + relation + ", " + + "user=" + user + ", " + + "contextualTupleKeys=" + contextualTupleKeys + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ListObjectsResponse.java b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ListObjectsResponse.java new file mode 100644 index 0000000..c5546c7 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ListObjectsResponse.java @@ -0,0 +1,44 @@ +package io.quarkiverse.openfga.client.model.dto; + +import java.util.List; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class ListObjectsResponse { + @JsonProperty("object_ids") + private final List objectIds; + + public ListObjectsResponse(@JsonProperty("object_ids") List objectIds) { + this.objectIds = Preconditions.parameterNonNull(objectIds, "objectIds"); + } + + @JsonProperty("object_ids") + public List getObjectIds() { + return objectIds; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (ListObjectsResponse) obj; + return Objects.equals(this.objectIds, that.objectIds); + } + + @Override + public int hashCode() { + return Objects.hash(objectIds); + } + + @Override + public String toString() { + return "ListObjectsResponse[" + + "objectIds=" + objectIds + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ListStoresResponse.java b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ListStoresResponse.java new file mode 100644 index 0000000..08800c9 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ListStoresResponse.java @@ -0,0 +1,57 @@ +package io.quarkiverse.openfga.client.model.dto; + +import java.util.List; +import java.util.Objects; + +import javax.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkiverse.openfga.client.model.Store; +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class ListStoresResponse { + private final List stores; + @JsonProperty("continuation_token") + @Nullable + private final String continuationToken; + + public ListStoresResponse(List stores, @JsonProperty("continuation_token") @Nullable String continuationToken) { + this.stores = Preconditions.parameterNonNull(stores, "stores"); + this.continuationToken = continuationToken; + } + + public List getStores() { + return stores; + } + + @JsonProperty("continuation_token") + @Nullable + public String getContinuationToken() { + return continuationToken; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (ListStoresResponse) obj; + return Objects.equals(this.stores, that.stores) && + Objects.equals(this.continuationToken, that.continuationToken); + } + + @Override + public int hashCode() { + return Objects.hash(stores, continuationToken); + } + + @Override + public String toString() { + return "ListStoresResponse[" + + "stores=" + stores + ", " + + "continuationToken=" + continuationToken + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ReadAssertionsResponse.java b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ReadAssertionsResponse.java new file mode 100644 index 0000000..877a880 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ReadAssertionsResponse.java @@ -0,0 +1,54 @@ +package io.quarkiverse.openfga.client.model.dto; + +import java.util.List; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkiverse.openfga.client.model.Assertion; +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class ReadAssertionsResponse { + @JsonProperty("authorization_model_id") + private final String authorizationModelId; + private final List assertions; + + public ReadAssertionsResponse(@JsonProperty("authorization_model_id") String authorizationModelId, + List assertions) { + this.authorizationModelId = Preconditions.parameterNonNull(authorizationModelId, "authorizationModelId"); + this.assertions = Preconditions.parameterNonNull(assertions, "assertions"); + } + + @JsonProperty("authorization_model_id") + public String getAuthorizationModelId() { + return authorizationModelId; + } + + public List getAssertions() { + return assertions; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (ReadAssertionsResponse) obj; + return Objects.equals(this.authorizationModelId, that.authorizationModelId) && + Objects.equals(this.assertions, that.assertions); + } + + @Override + public int hashCode() { + return Objects.hash(authorizationModelId, assertions); + } + + @Override + public String toString() { + return "ReadAssertionsResponse[" + + "authorizationModelId=" + authorizationModelId + ", " + + "assertions=" + assertions + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ReadAuthorizationModelResponse.java b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ReadAuthorizationModelResponse.java new file mode 100644 index 0000000..3e9d41b --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ReadAuthorizationModelResponse.java @@ -0,0 +1,44 @@ +package io.quarkiverse.openfga.client.model.dto; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkiverse.openfga.client.model.AuthorizationModel; +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class ReadAuthorizationModelResponse { + @JsonProperty("authorization_model") + private final AuthorizationModel authorizationModel; + + public ReadAuthorizationModelResponse(@JsonProperty("authorization_model") AuthorizationModel authorizationModel) { + this.authorizationModel = Preconditions.parameterNonNull(authorizationModel, "authorizationModel"); + } + + @JsonProperty("authorization_model") + public AuthorizationModel getAuthorizationModel() { + return authorizationModel; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (ReadAuthorizationModelResponse) obj; + return Objects.equals(this.authorizationModel, that.authorizationModel); + } + + @Override + public int hashCode() { + return Objects.hash(authorizationModel); + } + + @Override + public String toString() { + return "ReadAuthorizationModelResponse[" + + "authorizationModel=" + authorizationModel + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ReadAuthorizationModelsResponse.java b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ReadAuthorizationModelsResponse.java new file mode 100644 index 0000000..b9f3c41 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ReadAuthorizationModelsResponse.java @@ -0,0 +1,60 @@ +package io.quarkiverse.openfga.client.model.dto; + +import java.util.List; +import java.util.Objects; + +import javax.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkiverse.openfga.client.model.AuthorizationModel; +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class ReadAuthorizationModelsResponse { + @JsonProperty("authorization_models") + private final List authorizationModels; + @JsonProperty("continuation_token") + @Nullable + private final String continuationToken; + + public ReadAuthorizationModelsResponse(@JsonProperty("authorization_models") List authorizationModels, + @JsonProperty("continuation_token") @Nullable String continuationToken) { + this.authorizationModels = Preconditions.parameterNonNull(authorizationModels, "authorizationModels"); + this.continuationToken = continuationToken; + } + + @JsonProperty("authorization_models") + public List getAuthorizationModels() { + return authorizationModels; + } + + @JsonProperty("continuation_token") + @Nullable + public String getContinuationToken() { + return continuationToken; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (ReadAuthorizationModelsResponse) obj; + return Objects.equals(this.authorizationModels, that.authorizationModels) && + Objects.equals(this.continuationToken, that.continuationToken); + } + + @Override + public int hashCode() { + return Objects.hash(authorizationModels, continuationToken); + } + + @Override + public String toString() { + return "ReadAuthorizationModelsResponse[" + + "authorizationModels=" + authorizationModels + ", " + + "continuationToken=" + continuationToken + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ReadBody.java b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ReadBody.java new file mode 100644 index 0000000..ea80d60 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ReadBody.java @@ -0,0 +1,85 @@ +package io.quarkiverse.openfga.client.model.dto; + +import java.util.Objects; + +import javax.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkiverse.openfga.client.model.PartialTupleKey; +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class ReadBody { + @JsonProperty("tuple_key") + private final PartialTupleKey tupleKey; + @JsonProperty("authorization_model_id") + @Nullable + private final String authorizationModelId; + @JsonProperty("page_size") + @Nullable + private final Integer pageSize; + @JsonProperty("continuation_token") + @Nullable + private final String continuationToken; + + public ReadBody(@JsonProperty("tuple_key") PartialTupleKey tupleKey, + @JsonProperty("authorization_model_id") @Nullable String authorizationModelId, + @JsonProperty("page_size") @Nullable Integer pageSize, + @JsonProperty("continuation_token") @Nullable String continuationToken) { + this.tupleKey = Preconditions.parameterNonNull(tupleKey, "tupleKey"); + this.authorizationModelId = authorizationModelId; + this.pageSize = pageSize; + this.continuationToken = continuationToken; + } + + @JsonProperty("tuple_key") + public PartialTupleKey getTupleKey() { + return tupleKey; + } + + @JsonProperty("authorization_model_id") + @Nullable + public String getAuthorizationModelId() { + return authorizationModelId; + } + + @JsonProperty("page_size") + @Nullable + public Integer getPageSize() { + return pageSize; + } + + @JsonProperty("continuation_token") + @Nullable + public String getContinuationToken() { + return continuationToken; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (ReadBody) obj; + return Objects.equals(this.tupleKey, that.tupleKey) && + Objects.equals(this.authorizationModelId, that.authorizationModelId) && + Objects.equals(this.pageSize, that.pageSize) && + Objects.equals(this.continuationToken, that.continuationToken); + } + + @Override + public int hashCode() { + return Objects.hash(tupleKey, authorizationModelId, pageSize, continuationToken); + } + + @Override + public String toString() { + return "ReadBody[" + + "tupleKey=" + tupleKey + ", " + + "authorizationModelId=" + authorizationModelId + ", " + + "pageSize=" + pageSize + ", " + + "continuationToken=" + continuationToken + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ReadChangesResponse.java b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ReadChangesResponse.java new file mode 100644 index 0000000..0c397e1 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ReadChangesResponse.java @@ -0,0 +1,41 @@ +package io.quarkiverse.openfga.client.model.dto; + +import java.util.List; +import java.util.Objects; + +import io.quarkiverse.openfga.client.model.TupleChange; +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class ReadChangesResponse { + private final List changes; + + public ReadChangesResponse(List changes) { + this.changes = Preconditions.parameterNonNull(changes, "changes"); + } + + public List getChanges() { + return changes; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (ReadChangesResponse) obj; + return Objects.equals(this.changes, that.changes); + } + + @Override + public int hashCode() { + return Objects.hash(changes); + } + + @Override + public String toString() { + return "ReadChangesResponse[" + + "changes=" + changes + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ReadResponse.java b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ReadResponse.java new file mode 100644 index 0000000..6ea1ce9 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ReadResponse.java @@ -0,0 +1,57 @@ +package io.quarkiverse.openfga.client.model.dto; + +import java.util.List; +import java.util.Objects; + +import javax.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkiverse.openfga.client.model.Tuple; +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class ReadResponse { + private final List tuples; + @JsonProperty("continuation_token") + @Nullable + private final String continuationToken; + + public ReadResponse(List tuples, @JsonProperty("continuation_token") @Nullable String continuationToken) { + this.tuples = Preconditions.parameterNonNull(tuples, "tuples"); + this.continuationToken = continuationToken; + } + + public List getTuples() { + return tuples; + } + + @JsonProperty("continuation_token") + @Nullable + public String getContinuationToken() { + return continuationToken; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (ReadResponse) obj; + return Objects.equals(this.tuples, that.tuples) && + Objects.equals(this.continuationToken, that.continuationToken); + } + + @Override + public int hashCode() { + return Objects.hash(tuples, continuationToken); + } + + @Override + public String toString() { + return "ReadResponse[" + + "tuples=" + tuples + ", " + + "continuationToken=" + continuationToken + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ReadTuplesBody.java b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ReadTuplesBody.java new file mode 100644 index 0000000..998a6b8 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ReadTuplesBody.java @@ -0,0 +1,59 @@ +package io.quarkiverse.openfga.client.model.dto; + +import java.util.Objects; + +import javax.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public final class ReadTuplesBody { + @JsonProperty("page_size") + @Nullable + private final Integer pageSize; + @JsonProperty("continuation_token") + @Nullable + private final String continuationToken; + + public ReadTuplesBody( + @JsonProperty("page_size") @Nullable Integer pageSize, + @JsonProperty("continuation_token") @Nullable String continuationToken) { + this.pageSize = pageSize; + this.continuationToken = continuationToken; + } + + @JsonProperty("page_size") + @Nullable + public Integer getPageSize() { + return pageSize; + } + + @JsonProperty("continuation_token") + @Nullable + public String getContinuationToken() { + return continuationToken; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (ReadTuplesBody) obj; + return Objects.equals(this.pageSize, that.pageSize) && + Objects.equals(this.continuationToken, that.continuationToken); + } + + @Override + public int hashCode() { + return Objects.hash(pageSize, continuationToken); + } + + @Override + public String toString() { + return "ReadTuplesBody[" + + "pageSize=" + pageSize + ", " + + "continuationToken=" + continuationToken + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ReadTuplesResponse.java b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ReadTuplesResponse.java new file mode 100644 index 0000000..ea13a82 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/ReadTuplesResponse.java @@ -0,0 +1,57 @@ +package io.quarkiverse.openfga.client.model.dto; + +import java.util.List; +import java.util.Objects; + +import javax.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkiverse.openfga.client.model.Tuple; +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class ReadTuplesResponse { + private final List tuples; + @JsonProperty("continuation_token") + @Nullable + private final String continuationToken; + + public ReadTuplesResponse(List tuples, @JsonProperty("continuation_token") @Nullable String continuationToken) { + this.tuples = Preconditions.parameterNonNull(tuples, "tuples"); + this.continuationToken = continuationToken; + } + + public List getTuples() { + return tuples; + } + + @JsonProperty("continuation_token") + @Nullable + public String getContinuationToken() { + return continuationToken; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (ReadTuplesResponse) obj; + return Objects.equals(this.tuples, that.tuples) && + Objects.equals(this.continuationToken, that.continuationToken); + } + + @Override + public int hashCode() { + return Objects.hash(tuples, continuationToken); + } + + @Override + public String toString() { + return "ReadTuplesResponse[" + + "tuples=" + tuples + ", " + + "continuationToken=" + continuationToken + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/dto/WriteAssertionsRequest.java b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/WriteAssertionsRequest.java new file mode 100644 index 0000000..78d30dd --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/WriteAssertionsRequest.java @@ -0,0 +1,41 @@ +package io.quarkiverse.openfga.client.model.dto; + +import java.util.List; +import java.util.Objects; + +import io.quarkiverse.openfga.client.model.Assertion; +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class WriteAssertionsRequest { + private final List assertions; + + public WriteAssertionsRequest(List assertions) { + this.assertions = Preconditions.parameterNonNull(assertions, "assertions"); + } + + public List getAssertions() { + return assertions; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (WriteAssertionsRequest) obj; + return Objects.equals(this.assertions, that.assertions); + } + + @Override + public int hashCode() { + return Objects.hash(assertions); + } + + @Override + public String toString() { + return "WriteAssertionsRequest[" + + "assertions=" + assertions + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/dto/WriteAuthorizationModelResponse.java b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/WriteAuthorizationModelResponse.java new file mode 100644 index 0000000..d57c3a3 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/WriteAuthorizationModelResponse.java @@ -0,0 +1,43 @@ +package io.quarkiverse.openfga.client.model.dto; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class WriteAuthorizationModelResponse { + @JsonProperty("authorization_model_id") + private final String authorizationModelId; + + public WriteAuthorizationModelResponse(@JsonProperty("authorization_model_id") String authorizationModelId) { + this.authorizationModelId = Preconditions.parameterNonNull(authorizationModelId, "authorizationModelId"); + } + + @JsonProperty("authorization_model_id") + public String getAuthorizationModelId() { + return authorizationModelId; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (WriteAuthorizationModelResponse) obj; + return Objects.equals(this.authorizationModelId, that.authorizationModelId); + } + + @Override + public int hashCode() { + return Objects.hash(authorizationModelId); + } + + @Override + public String toString() { + return "WriteAuthorizationModelResponse[" + + "authorizationModelId=" + authorizationModelId + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/dto/WriteBody.java b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/WriteBody.java new file mode 100644 index 0000000..1f27c62 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/WriteBody.java @@ -0,0 +1,68 @@ +package io.quarkiverse.openfga.client.model.dto; + +import java.util.Objects; + +import javax.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkiverse.openfga.client.model.TupleKeys; + +public final class WriteBody { + @Nullable + private final TupleKeys writes; + @Nullable + private final TupleKeys deletes; + @JsonProperty("authorization_model_id") + @Nullable + private final String authorizationModelId; + + public WriteBody(@Nullable TupleKeys writes, @Nullable TupleKeys deletes, + @JsonProperty("authorization_model_id") @Nullable String authorizationModelId) { + this.writes = writes; + this.deletes = deletes; + this.authorizationModelId = authorizationModelId; + } + + @Nullable + public TupleKeys getWrites() { + return writes; + } + + @Nullable + public TupleKeys getDeletes() { + return deletes; + } + + @JsonProperty("authorization_model_id") + @Nullable + public String getAuthorizationModelId() { + return authorizationModelId; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (WriteBody) obj; + return Objects.equals(this.writes, that.writes) && + Objects.equals(this.deletes, that.deletes) && + Objects.equals(this.authorizationModelId, that.authorizationModelId); + } + + @Override + public int hashCode() { + return Objects.hash(writes, deletes, authorizationModelId); + } + + @Override + public String toString() { + return "WriteBody[" + + "writes=" + writes + ", " + + "deletes=" + deletes + ", " + + "authorizationModelId=" + authorizationModelId + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/dto/WriteResponse.java b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/WriteResponse.java new file mode 100644 index 0000000..a8cdb85 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/WriteResponse.java @@ -0,0 +1,6 @@ +package io.quarkiverse.openfga.client.model.dto; + +import java.util.HashMap; + +public class WriteResponse extends HashMap { +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/dto/package-info.java b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/package-info.java new file mode 100644 index 0000000..12f5924 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/dto/package-info.java @@ -0,0 +1,2 @@ +@javax.annotation.ParametersAreNonnullByDefault +package io.quarkiverse.openfga.client.model.dto; diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/nodes/Computed.java b/model/src/main/java/io/quarkiverse/openfga/client/model/nodes/Computed.java new file mode 100644 index 0000000..04e0a78 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/nodes/Computed.java @@ -0,0 +1,39 @@ +package io.quarkiverse.openfga.client.model.nodes; + +import java.util.Objects; + +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class Computed { + private final String userset; + + public Computed(String userset) { + this.userset = Preconditions.parameterNonNull(userset, "userset"); + } + + public String getUserset() { + return userset; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (Computed) obj; + return Objects.equals(this.userset, that.userset); + } + + @Override + public int hashCode() { + return Objects.hash(userset); + } + + @Override + public String toString() { + return "Computed[" + + "userset=" + userset + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/nodes/Difference.java b/model/src/main/java/io/quarkiverse/openfga/client/model/nodes/Difference.java new file mode 100644 index 0000000..7a54f16 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/nodes/Difference.java @@ -0,0 +1,47 @@ +package io.quarkiverse.openfga.client.model.nodes; + +import java.util.Objects; + +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class Difference { + private final Node base; + private final Node subtract; + + public Difference(Node base, Node subtract) { + this.base = Preconditions.parameterNonNull(base, "base"); + this.subtract = Preconditions.parameterNonNull(subtract, "subtract"); + } + + public Node getBase() { + return base; + } + + public Node getSubtract() { + return subtract; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (Difference) obj; + return Objects.equals(this.base, that.base) && + Objects.equals(this.subtract, that.subtract); + } + + @Override + public int hashCode() { + return Objects.hash(base, subtract); + } + + @Override + public String toString() { + return "Difference[" + + "base=" + base + ", " + + "subtract=" + subtract + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/nodes/Leaf.java b/model/src/main/java/io/quarkiverse/openfga/client/model/nodes/Leaf.java new file mode 100644 index 0000000..10d5e9f --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/nodes/Leaf.java @@ -0,0 +1,64 @@ +package io.quarkiverse.openfga.client.model.nodes; + +import java.util.Objects; + +import javax.annotation.Nullable; + +public final class Leaf { + @Nullable + private final Users users; + @Nullable + private final Computed computed; + @Nullable + private final TupleToUserset tupleToUserset; + + public Leaf( + @Nullable Users users, + @Nullable Computed computed, + @Nullable TupleToUserset tupleToUserset) { + this.users = users; + this.computed = computed; + this.tupleToUserset = tupleToUserset; + } + + @Nullable + public Users getUsers() { + return users; + } + + @Nullable + public Computed getComputed() { + return computed; + } + + @Nullable + public TupleToUserset getTupleToUserset() { + return tupleToUserset; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (Leaf) obj; + return Objects.equals(this.users, that.users) && + Objects.equals(this.computed, that.computed) && + Objects.equals(this.tupleToUserset, that.tupleToUserset); + } + + @Override + public int hashCode() { + return Objects.hash(users, computed, tupleToUserset); + } + + @Override + public String toString() { + return "Leaf[" + + "users=" + users + ", " + + "computed=" + computed + ", " + + "tupleToUserset=" + tupleToUserset + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/nodes/Node.java b/model/src/main/java/io/quarkiverse/openfga/client/model/nodes/Node.java new file mode 100644 index 0000000..b4d9236 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/nodes/Node.java @@ -0,0 +1,82 @@ +package io.quarkiverse.openfga.client.model.nodes; + +import java.util.Objects; + +import javax.annotation.Nullable; + +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class Node { + private final String name; + @Nullable + private final Leaf leaf; + @Nullable + private final Difference difference; + @Nullable + private final Nodes union; + @Nullable + private final Nodes intersection; + + public Node(String name, @Nullable Leaf leaf, @Nullable Difference difference, @Nullable Nodes union, + @Nullable Nodes intersection) { + this.name = Preconditions.parameterNonNull(name, "name"); + this.leaf = leaf; + this.difference = difference; + this.union = union; + this.intersection = intersection; + } + + public String getName() { + return name; + } + + @Nullable + public Leaf getLeaf() { + return leaf; + } + + @Nullable + public Difference getDifference() { + return difference; + } + + @Nullable + public Nodes getUnion() { + return union; + } + + @Nullable + public Nodes getIntersection() { + return intersection; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (Node) obj; + return Objects.equals(this.name, that.name) && + Objects.equals(this.leaf, that.leaf) && + Objects.equals(this.difference, that.difference) && + Objects.equals(this.union, that.union) && + Objects.equals(this.intersection, that.intersection); + } + + @Override + public int hashCode() { + return Objects.hash(name, leaf, difference, union, intersection); + } + + @Override + public String toString() { + return "Node[" + + "name=" + name + ", " + + "leaf=" + leaf + ", " + + "difference=" + difference + ", " + + "union=" + union + ", " + + "intersection=" + intersection + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/nodes/Nodes.java b/model/src/main/java/io/quarkiverse/openfga/client/model/nodes/Nodes.java new file mode 100644 index 0000000..7b6b4df --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/nodes/Nodes.java @@ -0,0 +1,40 @@ +package io.quarkiverse.openfga.client.model.nodes; + +import java.util.List; +import java.util.Objects; + +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class Nodes { + private final List nodes; + + public Nodes(List nodes) { + this.nodes = Preconditions.parameterNonNull(nodes, "nodes"); + } + + public List getNodes() { + return nodes; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (Nodes) obj; + return Objects.equals(this.nodes, that.nodes); + } + + @Override + public int hashCode() { + return Objects.hash(nodes); + } + + @Override + public String toString() { + return "Nodes[" + + "nodes=" + nodes + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/nodes/TupleToUserset.java b/model/src/main/java/io/quarkiverse/openfga/client/model/nodes/TupleToUserset.java new file mode 100644 index 0000000..ee00758 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/nodes/TupleToUserset.java @@ -0,0 +1,48 @@ +package io.quarkiverse.openfga.client.model.nodes; + +import java.util.List; +import java.util.Objects; + +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class TupleToUserset { + private final String tupleset; + private final List computed; + + public TupleToUserset(String tupleset, List computed) { + this.tupleset = Preconditions.parameterNonNull(tupleset, "tupleset"); + this.computed = Preconditions.parameterNonNull(computed, "computed"); + } + + public String getTupleset() { + return tupleset; + } + + public List getcomputed() { + return computed; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (TupleToUserset) obj; + return Objects.equals(this.tupleset, that.tupleset) && + Objects.equals(this.computed, that.computed); + } + + @Override + public int hashCode() { + return Objects.hash(tupleset, computed); + } + + @Override + public String toString() { + return "TupleToUserset[" + + "tupleset=" + tupleset + ", " + + "computed=" + computed + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/nodes/Users.java b/model/src/main/java/io/quarkiverse/openfga/client/model/nodes/Users.java new file mode 100644 index 0000000..9bc815b --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/nodes/Users.java @@ -0,0 +1,40 @@ +package io.quarkiverse.openfga.client.model.nodes; + +import java.util.List; +import java.util.Objects; + +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public final class Users { + private final List users; + + public Users(List users) { + this.users = Preconditions.parameterNonNull(users, "users"); + } + + public List getUsers() { + return users; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (Users) obj; + return Objects.equals(this.users, that.users); + } + + @Override + public int hashCode() { + return Objects.hash(users); + } + + @Override + public String toString() { + return "Users[" + + "users=" + users + ']'; + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/nodes/V1.java b/model/src/main/java/io/quarkiverse/openfga/client/model/nodes/V1.java new file mode 100644 index 0000000..15de306 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/nodes/V1.java @@ -0,0 +1,95 @@ +package io.quarkiverse.openfga.client.model.nodes; + +import java.util.Objects; + +import io.quarkiverse.openfga.client.model.ObjectRelation; +import io.quarkiverse.openfga.client.model.Userset; +import io.quarkiverse.openfga.client.model.utils.Preconditions; + +public class V1 { + + public static final class TupleToUserset { + private final ObjectRelation tupleset; + private final ObjectRelation computedUserset; + + public TupleToUserset(ObjectRelation tupleset, ObjectRelation computedUserset) { + this.tupleset = Preconditions.parameterNonNull(tupleset, "tupleset"); + this.computedUserset = Preconditions.parameterNonNull(computedUserset, "computedUserset"); + } + + public ObjectRelation getTupleset() { + return tupleset; + } + + public ObjectRelation getComputedUserset() { + return computedUserset; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (TupleToUserset) obj; + return Objects.equals(this.tupleset, that.tupleset) && + Objects.equals(this.computedUserset, that.computedUserset); + } + + @Override + public int hashCode() { + return Objects.hash(tupleset, computedUserset); + } + + @Override + public String toString() { + return "TupleToUserset[" + + "tupleset=" + tupleset + ", " + + "computedUserset=" + computedUserset + ']'; + } + + } + + public static final class Difference { + private final Userset base; + private final Userset subtract; + + public Difference(Userset base, Userset subtract) { + this.base = Preconditions.parameterNonNull(base, "base"); + this.subtract = Preconditions.parameterNonNull(subtract, "subtract"); + } + + public Userset getBase() { + return base; + } + + public Userset getSubtract() { + return subtract; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (Difference) obj; + return Objects.equals(this.base, that.base) && + Objects.equals(this.subtract, that.subtract); + } + + @Override + public int hashCode() { + return Objects.hash(base, subtract); + } + + @Override + public String toString() { + return "Difference[" + + "base=" + base + ", " + + "subtract=" + subtract + ']'; + } + + } + +} diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/package-info.java b/model/src/main/java/io/quarkiverse/openfga/client/model/package-info.java new file mode 100644 index 0000000..fc8eaad --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/package-info.java @@ -0,0 +1,4 @@ +@ParametersAreNonnullByDefault +package io.quarkiverse.openfga.client.model; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/model/src/main/java/io/quarkiverse/openfga/client/model/utils/Preconditions.java b/model/src/main/java/io/quarkiverse/openfga/client/model/utils/Preconditions.java new file mode 100644 index 0000000..f65b9d1 --- /dev/null +++ b/model/src/main/java/io/quarkiverse/openfga/client/model/utils/Preconditions.java @@ -0,0 +1,13 @@ +package io.quarkiverse.openfga.client.model.utils; + +import java.util.Objects; + +import javax.annotation.Nonnull; + +public class Preconditions { + + public static @Nonnull T parameterNonNull(@Nonnull T value, @Nonnull String name) { + return Objects.requireNonNull(value, name + " parameter must not be null"); + } + +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..a3e6030 --- /dev/null +++ b/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + io.quarkiverse + quarkiverse-parent + 10 + + io.quarkiverse.openfga + quarkus-openfga-client-parent + 999-SNAPSHOT + pom + Quarkus OpenFGA Client - Parent + + deployment + model + runtime + docs + + + 3.8.1 + UTF-8 + UTF-8 + 2.11.2.Final + 1.2.3 + 2.0.8 + + + + + io.quarkus + quarkus-bom + ${quarkus.version} + pom + import + + + + + + it + + + performRelease + !true + + + + integration-tests + + + + diff --git a/runtime/pom.xml b/runtime/pom.xml new file mode 100644 index 0000000..b4b6d80 --- /dev/null +++ b/runtime/pom.xml @@ -0,0 +1,72 @@ + + + 4.0.0 + + io.quarkiverse.openfga + quarkus-openfga-client-parent + 999-SNAPSHOT + + quarkus-openfga-client + Quarkus OpenFGA Client - Runtime + + + io.quarkiverse.openfga + quarkus-openfga-client-model + ${project.version} + + + io.smallrye.reactive + smallrye-mutiny-vertx-web-client + + + io.quarkus + quarkus-mutiny + + + io.quarkus + quarkus-vertx + + + io.quarkus.arc + arc + + + io.quarkus + quarkus-smallrye-health + true + + + + + + io.quarkus + quarkus-extension-maven-plugin + ${quarkus.version} + + + compile + + extension-descriptor + + + ${project.groupId}:${project.artifactId}-deployment:${project.version} + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + + diff --git a/runtime/src/main/java/io/quarkiverse/openfga/client/AssertionsClient.java b/runtime/src/main/java/io/quarkiverse/openfga/client/AssertionsClient.java new file mode 100644 index 0000000..ec11262 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/openfga/client/AssertionsClient.java @@ -0,0 +1,32 @@ +package io.quarkiverse.openfga.client; + +import java.util.List; + +import io.quarkiverse.openfga.client.api.API; +import io.quarkiverse.openfga.client.model.Assertion; +import io.quarkiverse.openfga.client.model.dto.ReadAssertionsResponse; +import io.quarkiverse.openfga.client.model.dto.WriteAssertionsRequest; +import io.smallrye.mutiny.Uni; + +public class AssertionsClient { + + private final API api; + private final String storeId; + private final String authorizationModelId; + + public AssertionsClient(API api, String storeId, String authorizationModelId) { + this.api = api; + this.storeId = storeId; + this.authorizationModelId = authorizationModelId; + } + + public Uni> list() { + return api.readAssertions(storeId, authorizationModelId) + .map(ReadAssertionsResponse::getAssertions); + } + + public Uni update(List assertions) { + return api.writeAssertions(storeId, authorizationModelId, new WriteAssertionsRequest(assertions)); + } + +} diff --git a/runtime/src/main/java/io/quarkiverse/openfga/client/AuthorizationModelClient.java b/runtime/src/main/java/io/quarkiverse/openfga/client/AuthorizationModelClient.java new file mode 100644 index 0000000..5282568 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/openfga/client/AuthorizationModelClient.java @@ -0,0 +1,92 @@ +package io.quarkiverse.openfga.client; + +import static io.quarkiverse.openfga.client.utils.PaginatedList.collectAllPages; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import javax.annotation.Nullable; + +import io.quarkiverse.openfga.client.api.API; +import io.quarkiverse.openfga.client.model.*; +import io.quarkiverse.openfga.client.model.dto.*; +import io.quarkiverse.openfga.client.utils.PaginatedList; +import io.smallrye.mutiny.Uni; + +public class AuthorizationModelClient { + + private final API api; + private final String storeId; + private final String authorizationModelId; + + public AuthorizationModelClient(API api, String storeId, @Nullable String authorizationModelId) { + this.api = api; + this.storeId = storeId; + this.authorizationModelId = authorizationModelId; + } + + public Uni get() { + return api.readAuthorizationModel(storeId, authorizationModelId) + .map(ReadAuthorizationModelResponse::getAuthorizationModel); + } + + public Uni check(TupleKey tupleKey, @Nullable ContextualTupleKeys contextualTupleKeys) { + return api.check(storeId, new CheckBody(tupleKey, contextualTupleKeys, authorizationModelId, null)) + .map(CheckResponse::getAllowed); + } + + public Uni expand(TupleKey tupleKey) { + return api.expand(storeId, new ExpandBody(tupleKey, authorizationModelId)) + .map(ExpandResponse::getTree); + } + + public Uni> listObjects(String type, @Nullable String relation, String user, + @Nullable List contextualTupleKeys) { + return api + .listObjects(storeId, + new ListObjectsBody(authorizationModelId, type, relation, user, + contextualTupleKeys != null ? new ContextualTupleKeys(contextualTupleKeys) : null)) + .map(ListObjectsResponse::getObjectIds); + } + + public Uni> queryTuples(PartialTupleKey tupleKey, @Nullable Integer pageSize, + @Nullable String pagingToken) { + return api.read(storeId, new ReadBody(tupleKey, authorizationModelId, pageSize, pagingToken)) + .map(res -> new PaginatedList<>(res.getTuples(), res.getContinuationToken())); + } + + public Uni> queryAllTuples(PartialTupleKey tupleKey) { + return queryAllTuples(tupleKey, null); + } + + public Uni> queryAllTuples(PartialTupleKey tupleKey, @Nullable Integer pageSize) { + return collectAllPages(pageSize, (currentPageSize, currentToken) -> { + return queryTuples(tupleKey, currentPageSize, currentToken); + }); + } + + public Uni> readTuples(@Nullable Integer pageSize, @Nullable String pagingToken) { + return api.readTuples(storeId, new ReadTuplesBody(pageSize, pagingToken)) + .map(res -> new PaginatedList<>(res.getTuples(), res.getContinuationToken())); + } + + public Uni> readAllTuples() { + return readAllTuples(null); + } + + public Uni> readAllTuples(@Nullable Integer pageSize) { + return collectAllPages(pageSize, this::readTuples); + } + + public Uni write(TupleKey tupleKey) { + return write(List.of(tupleKey), null) + .replaceWithVoid(); + } + + public Uni> write(@Nullable List writes, @Nullable List deletes) { + return api.write(storeId, new WriteBody(TupleKeys.of(writes), TupleKeys.of(deletes), authorizationModelId)) + .map(Function.identity()); + } + +} diff --git a/runtime/src/main/java/io/quarkiverse/openfga/client/AuthorizationModelsClient.java b/runtime/src/main/java/io/quarkiverse/openfga/client/AuthorizationModelsClient.java new file mode 100644 index 0000000..b803b63 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/openfga/client/AuthorizationModelsClient.java @@ -0,0 +1,49 @@ +package io.quarkiverse.openfga.client; + +import static io.quarkiverse.openfga.client.utils.PaginatedList.collectAllPages; + +import java.util.List; + +import javax.annotation.Nullable; + +import io.quarkiverse.openfga.client.api.API; +import io.quarkiverse.openfga.client.model.AuthorizationModel; +import io.quarkiverse.openfga.client.model.TypeDefinition; +import io.quarkiverse.openfga.client.model.TypeDefinitions; +import io.quarkiverse.openfga.client.model.dto.WriteAuthorizationModelResponse; +import io.quarkiverse.openfga.client.utils.PaginatedList; +import io.smallrye.mutiny.Uni; + +public class AuthorizationModelsClient { + + private final API api; + private final String storeId; + + public AuthorizationModelsClient(API api, String storeId) { + this.api = api; + this.storeId = storeId; + } + + public Uni> list(@Nullable Integer pageSize, @Nullable String pagingToken) { + return api.readAuthorizationModels(storeId, pageSize, pagingToken) + .map(res -> new PaginatedList<>(res.getAuthorizationModels(), res.getContinuationToken())); + } + + public Uni> listAll() { + return listAll(null); + } + + public Uni> listAll(@Nullable Integer pageSize) { + return collectAllPages(pageSize, this::list); + } + + public Uni create(List typeDefinitions) { + return api.writeAuthorizationModel(storeId, new TypeDefinitions(typeDefinitions)) + .map(WriteAuthorizationModelResponse::getAuthorizationModelId); + } + + public AuthorizationModelClient model(String authorizationModelId) { + return new AuthorizationModelClient(api, storeId, authorizationModelId); + } + +} diff --git a/runtime/src/main/java/io/quarkiverse/openfga/client/StoreClient.java b/runtime/src/main/java/io/quarkiverse/openfga/client/StoreClient.java new file mode 100644 index 0000000..5a66b89 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/openfga/client/StoreClient.java @@ -0,0 +1,65 @@ +package io.quarkiverse.openfga.client; + +import static io.quarkiverse.openfga.client.utils.PaginatedList.collectAllPages; + +import java.util.List; + +import javax.annotation.Nullable; + +import io.quarkiverse.openfga.client.api.API; +import io.quarkiverse.openfga.client.model.Store; +import io.quarkiverse.openfga.client.model.Tuple; +import io.quarkiverse.openfga.client.model.TupleChange; +import io.quarkiverse.openfga.client.model.dto.GetStoreResponse; +import io.quarkiverse.openfga.client.model.dto.ReadChangesResponse; +import io.quarkiverse.openfga.client.model.dto.ReadTuplesBody; +import io.quarkiverse.openfga.client.utils.PaginatedList; +import io.smallrye.mutiny.Uni; + +public class StoreClient { + + private final API api; + private final String storeId; + + public StoreClient(API api, String storeId) { + this.api = api; + this.storeId = storeId; + + } + + public Uni get() { + return api.getStore(storeId).map(GetStoreResponse::asStore); + } + + public Uni delete() { + return api.deleteStore(storeId); + } + + public Uni> changes(@Nullable String type, @Nullable Integer pageSize, + @Nullable String continuationToken) { + return api.readChanges(storeId, type, pageSize, continuationToken) + .map(ReadChangesResponse::getChanges); + } + + public Uni> readTuples(@Nullable Integer pageSize, @Nullable String pagingToken) { + return api.readTuples(storeId, new ReadTuplesBody(pageSize, pagingToken)) + .map(res -> new PaginatedList<>(res.getTuples(), res.getContinuationToken())); + } + + public Uni> readAllTuples() { + return readAllTuples(null); + } + + public Uni> readAllTuples(@Nullable Integer pageSize) { + return collectAllPages(pageSize, this::readTuples); + } + + public AuthorizationModelsClient authorizationModels() { + return new AuthorizationModelsClient(api, storeId); + } + + public AssertionsClient assertions(String authorizationModelId) { + return new AssertionsClient(api, storeId, authorizationModelId); + } + +} diff --git a/runtime/src/main/java/io/quarkiverse/openfga/client/StoresClient.java b/runtime/src/main/java/io/quarkiverse/openfga/client/StoresClient.java new file mode 100644 index 0000000..00d2404 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/openfga/client/StoresClient.java @@ -0,0 +1,49 @@ +package io.quarkiverse.openfga.client; + +import static io.quarkiverse.openfga.client.utils.PaginatedList.collectAllPages; + +import java.util.List; + +import javax.annotation.Nullable; +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import io.quarkiverse.openfga.client.api.API; +import io.quarkiverse.openfga.client.model.Store; +import io.quarkiverse.openfga.client.model.dto.CreateStoreRequest; +import io.quarkiverse.openfga.client.model.dto.CreateStoreResponse; +import io.quarkiverse.openfga.client.utils.PaginatedList; +import io.smallrye.mutiny.Uni; + +@ApplicationScoped +public class StoresClient { + + private final API api; + + @Inject + public StoresClient(API api) { + this.api = api; + } + + public Uni> list(@Nullable Integer pageSize, @Nullable String continuationToken) { + return api.listStores(pageSize, continuationToken) + .map(res -> new PaginatedList<>(res.getStores(), res.getContinuationToken())); + } + + public Uni> listAll() { + return listAll(null); + } + + public Uni> listAll(@Nullable Integer pageSize) { + return collectAllPages(pageSize, this::list); + } + + public Uni create(String name) { + return api.createStore(new CreateStoreRequest(name)).map(CreateStoreResponse::asStore); + } + + public StoreClient store(String storeId) { + return new StoreClient(api, storeId); + } + +} diff --git a/runtime/src/main/java/io/quarkiverse/openfga/client/api/API.java b/runtime/src/main/java/io/quarkiverse/openfga/client/api/API.java new file mode 100644 index 0000000..5b718dc --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/openfga/client/api/API.java @@ -0,0 +1,318 @@ +package io.quarkiverse.openfga.client.api; + +import static io.quarkiverse.openfga.client.api.Queries.query; +import static io.quarkiverse.openfga.client.api.Vars.vars; +import static io.smallrye.mutiny.unchecked.Unchecked.function; +import static io.smallrye.mutiny.unchecked.Unchecked.supplier; +import static io.vertx.core.http.HttpMethod.*; +import static io.vertx.mutiny.core.http.HttpHeaders.ACCEPT; +import static io.vertx.mutiny.core.http.HttpHeaders.CONTENT_TYPE; +import static java.lang.String.format; + +import java.util.Map; +import java.util.Optional; + +import javax.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; + +import io.quarkiverse.openfga.client.model.TypeDefinitions; +import io.quarkiverse.openfga.client.model.dto.*; +import io.quarkiverse.openfga.runtime.config.OpenFGAConfig; +import io.quarkus.runtime.TlsConfig; +import io.smallrye.mutiny.Uni; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.RequestOptions; +import io.vertx.ext.auth.authentication.Credentials; +import io.vertx.ext.auth.authentication.TokenCredentials; +import io.vertx.mutiny.core.Vertx; +import io.vertx.mutiny.core.buffer.Buffer; +import io.vertx.mutiny.ext.web.client.HttpRequest; +import io.vertx.mutiny.ext.web.client.WebClient; +import io.vertx.mutiny.ext.web.client.predicate.ResponsePredicate; +import io.vertx.mutiny.ext.web.codec.BodyCodec; +import io.vertx.mutiny.uritemplate.UriTemplate; +import io.vertx.mutiny.uritemplate.Variables; + +public class API { + + private final WebClient webClient; + private final Optional credentials; + private final ObjectMapper objectMapper; + + public API(OpenFGAConfig config, TlsConfig tlsConfig, boolean tracingEnabled, Vertx vertx) { + this(VertxWebClientFactory.create(config, tlsConfig, tracingEnabled, vertx), + config.sharedKey.map(TokenCredentials::new)); + } + + public API(WebClient webClient, Optional credentials) { + this.webClient = webClient; + this.credentials = credentials; + this.objectMapper = new ObjectMapper() + .registerModule(new JavaTimeModule()) + .registerModule(new Jdk8Module()) + .registerModule(new ParameterNamesModule()) + .setSerializationInclusion(JsonInclude.Include.NON_NULL); + } + + public void close() { + webClient.close(); + } + + public Uni listStores(@Nullable Integer pageSize, @Nullable String continuationToken) { + return execute( + request("List Stores", + GET, + STORES_URI, + vars(), + query(PAGE_SIZE_PARAM, pageSize, CONT_TOKEN_PARAM, continuationToken)), + ExpectedStatus.OK, + ListStoresResponse.class); + } + + public Uni createStore(CreateStoreRequest body) { + return execute( + request("Create Store", + POST, + STORES_URI, + vars()), + body, + ExpectedStatus.CREATED, + CreateStoreResponse.class); + } + + public Uni getStore(String storeId) { + return execute( + request("Get Store", + GET, + STORE_URI, + vars(STORE_ID_PARAM, storeId)), + ExpectedStatus.OK, + GetStoreResponse.class); + } + + public Uni deleteStore(String storeId) { + return execute( + request("Delete Store", + DELETE, + STORE_URI, + vars(STORE_ID_PARAM, storeId)), + ExpectedStatus.NO_CONTENT); + } + + public Uni readAssertions(String storeId, @Nullable String authorizationModelId) { + return execute( + request("Read Assertions", + GET, + ASSERTIONS_URI, + vars(STORE_ID_PARAM, storeId, AUTH_MODEL_ID_PARAM, authorizationModelId)), + ExpectedStatus.OK, + ReadAssertionsResponse.class); + } + + public Uni writeAssertions(String storeId, String authorizationModelId, WriteAssertionsRequest body) { + return execute( + request("Write Assertions", + PUT, + ASSERTIONS_URI, + vars(STORE_ID_PARAM, storeId, AUTH_MODEL_ID_PARAM, authorizationModelId)), + body, + ExpectedStatus.NO_CONTENT); + } + + public Uni readAuthorizationModels(String storeId, @Nullable Integer pageSize, + @Nullable String continuationToken) { + return execute( + request("Read Auth Models", + GET, + AUTH_MODELS_URI, + vars(STORE_ID_PARAM, storeId), + query(PAGE_SIZE_PARAM, pageSize, CONT_TOKEN_PARAM, continuationToken)), + ExpectedStatus.OK, + ReadAuthorizationModelsResponse.class); + } + + public Uni writeAuthorizationModel(String storeId, TypeDefinitions typeDefinitions) { + return execute( + request("Write Auth Model", + POST, + AUTH_MODELS_URI, + vars(STORE_ID_PARAM, storeId)), + typeDefinitions, + ExpectedStatus.CREATED, + WriteAuthorizationModelResponse.class); + } + + public Uni readAuthorizationModel(String storeId, String id) { + return execute( + request("Read Auth Model", + GET, + AUTH_MODEL_URI, + vars(STORE_ID_PARAM, storeId, ID_PARAM, id)), + ExpectedStatus.OK, + ReadAuthorizationModelResponse.class); + } + + public Uni readChanges(String storeId, @Nullable String type, @Nullable Integer pageSize, + @Nullable String continuationToken) { + return execute( + request("Read Changes", + GET, + CHANGES_URI, + vars(STORE_ID_PARAM, storeId), + query(TYPE_PARAM, type, PAGE_SIZE_PARAM, pageSize, CONT_TOKEN_PARAM, continuationToken)), + ExpectedStatus.OK, + ReadChangesResponse.class); + } + + public Uni check(String storeId, CheckBody body) { + return execute( + request("Check", + POST, + CHECK_URI, + vars(STORE_ID_PARAM, storeId)), + body, + ExpectedStatus.OK, + CheckResponse.class); + } + + public Uni expand(String storeId, ExpandBody body) { + return execute( + request("Expand", + POST, + EXPAND_URI, + vars(STORE_ID_PARAM, storeId)), + body, + ExpectedStatus.OK, + ExpandResponse.class); + } + + public Uni listObjects(String storeId, ListObjectsBody body) { + return execute( + request("List Objects", + POST, + LIST_OBJECTS_URI, + vars(STORE_ID_PARAM, storeId)), + body, + ExpectedStatus.OK, + ListObjectsResponse.class); + } + + public Uni read(String storeId, ReadBody body) { + return execute( + request("Read", + POST, + READ_URI, + vars(STORE_ID_PARAM, storeId)), + body, + ExpectedStatus.OK, + ReadResponse.class); + } + + public Uni readTuples(String storeId, ReadTuplesBody body) { + return execute( + request("Read Tuples", + POST, + READ_TUPLES_URI, + vars(STORE_ID_PARAM, storeId)), + body, + ExpectedStatus.OK, + ReadTuplesResponse.class); + } + + public Uni write(String storeId, WriteBody body) { + return execute( + request("Write", + POST, + WRITE_URI, + vars(STORE_ID_PARAM, storeId)), + body, + ExpectedStatus.OK, + WriteResponse.class); + } + + private Uni execute(HttpRequest request, B body, ExpectedStatus expectedStatus, Class responseType) { + return Uni.createFrom() + .deferred(supplier(() -> { + return prepare(request, expectedStatus) + .putHeader(ACCEPT.toString(), APPLICATION_JSON) + .expect(ResponsePredicate.JSON) + .putHeader(CONTENT_TYPE.toString(), APPLICATION_JSON) + .sendBuffer(Buffer.buffer(objectMapper.writeValueAsString(body))); + })) + .onItem().transform(function(response -> objectMapper.readValue(response.bodyAsString(), responseType))); + } + + private Uni execute(HttpRequest request, B body, ExpectedStatus expectedStatus) { + return Uni.createFrom() + .deferred(supplier(() -> { + return prepare(request, expectedStatus) + .putHeader(CONTENT_TYPE.toString(), APPLICATION_JSON) + .sendBuffer(Buffer.buffer(objectMapper.writeValueAsString(body))) + .replaceWithVoid(); + })); + } + + private Uni execute(HttpRequest request, ExpectedStatus expectedStatus, Class responseType) { + return prepare(request, expectedStatus) + .putHeader(ACCEPT.toString(), APPLICATION_JSON) + .expect(ResponsePredicate.JSON) + .as(BodyCodec.buffer()) + .send() + .onItem().transform(function(response -> objectMapper.readValue(response.bodyAsString(), responseType))); + } + + private Uni execute(HttpRequest request, ExpectedStatus expectedStatus) { + return prepare(request, expectedStatus) + .send() + .replaceWithVoid(); + } + + private HttpRequest prepare(HttpRequest request, ExpectedStatus expectedStatus) { + + // Add creds + credentials.ifPresent(request::authentication); + + return request.expect(expectedStatus.responsePredicate); + } + + private HttpRequest request(String operationName, HttpMethod method, UriTemplate uriTemplate, Variables variables, + Map query) { + variables.set(FULL_QUERY_PARAM, query); + return request(operationName, method, uriTemplate, variables); + } + + private HttpRequest request(String operationName, HttpMethod method, UriTemplate uriTemplate, Variables variables) { + RequestOptions options = new RequestOptions() + .setURI(uriTemplate.expandToString(variables)) + .setTraceOperation(format("FGA| %s", operationName.toUpperCase())); + return webClient.request(method, options); + } + + private static final String APPLICATION_JSON = "application/json"; + private static final String PAGE_SIZE_PARAM = "page_size"; + private static final String CONT_TOKEN_PARAM = "continuation_token"; + private static final String STORE_ID_PARAM = "store_id"; + private static final String AUTH_MODEL_ID_PARAM = "authorization_model_id"; + private static final String TYPE_PARAM = "type"; + private static final String ID_PARAM = "id"; + private static final String FULL_QUERY_PARAM = "query"; + + private static final UriTemplate STORES_URI = UriTemplate.of("/stores{?query*}"); + private static final UriTemplate STORE_URI = UriTemplate.of("/stores/{store_id}"); + private static final UriTemplate ASSERTIONS_URI = UriTemplate.of("/stores/{store_id}/assertions/{authorization_model_id}"); + private static final UriTemplate AUTH_MODELS_URI = UriTemplate.of("/stores/{store_id}/authorization-models{?query*}"); + private static final UriTemplate AUTH_MODEL_URI = UriTemplate.of("/stores/{store_id}/authorization-models/{id}"); + private static final UriTemplate CHANGES_URI = UriTemplate.of("/stores/{store_id}/changes"); + private static final UriTemplate CHECK_URI = UriTemplate.of("/stores/{store_id}/check"); + private static final UriTemplate EXPAND_URI = UriTemplate.of("/stores/{store_id}/expand"); + private static final UriTemplate LIST_OBJECTS_URI = UriTemplate.of("/stores/{store_id}/list-objects"); + private static final UriTemplate READ_URI = UriTemplate.of("/stores/{store_id}/read"); + private static final UriTemplate WRITE_URI = UriTemplate.of("/stores/{store_id}/write"); + private static final UriTemplate READ_TUPLES_URI = UriTemplate.of("/stores/{store_id}/read-tuples"); + +} diff --git a/runtime/src/main/java/io/quarkiverse/openfga/client/api/Errors.java b/runtime/src/main/java/io/quarkiverse/openfga/client/api/Errors.java new file mode 100644 index 0000000..16b777a --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/openfga/client/api/Errors.java @@ -0,0 +1,25 @@ +package io.quarkiverse.openfga.client.api; + +import static io.vertx.mutiny.core.http.HttpHeaders.CONTENT_TYPE; + +import io.quarkiverse.openfga.client.model.FGAException; +import io.vertx.mutiny.ext.web.client.predicate.ErrorConverter; + +class Errors { + + static final ErrorConverter errorConverter = ErrorConverter.createFullBody(result -> { + + var response = result.response(); + + if (response.headers().contains(CONTENT_TYPE, "application/json", true)) { + try { + return response.bodyAsJson(FGAException.class); + } catch (Throwable t) { + // Ignore + } + } + + return new FGAException(FGAException.Code.UNKNOWN_ERROR, null); + }); + +} diff --git a/runtime/src/main/java/io/quarkiverse/openfga/client/api/ExpectedStatus.java b/runtime/src/main/java/io/quarkiverse/openfga/client/api/ExpectedStatus.java new file mode 100644 index 0000000..86cab07 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/openfga/client/api/ExpectedStatus.java @@ -0,0 +1,19 @@ +package io.quarkiverse.openfga.client.api; + +import static io.quarkiverse.openfga.client.api.Errors.errorConverter; + +import io.vertx.mutiny.ext.web.client.predicate.ResponsePredicate; + +public enum ExpectedStatus { + + OK(ResponsePredicate.create(ResponsePredicate.SC_OK, errorConverter)), + CREATED(ResponsePredicate.create(ResponsePredicate.SC_CREATED, errorConverter)), + NO_CONTENT(ResponsePredicate.create(ResponsePredicate.SC_NO_CONTENT, errorConverter)); + + public final ResponsePredicate responsePredicate; + + ExpectedStatus(ResponsePredicate responsePredicate) { + this.responsePredicate = responsePredicate; + } + +} diff --git a/runtime/src/main/java/io/quarkiverse/openfga/client/api/Queries.java b/runtime/src/main/java/io/quarkiverse/openfga/client/api/Queries.java new file mode 100644 index 0000000..bd3fc02 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/openfga/client/api/Queries.java @@ -0,0 +1,36 @@ +package io.quarkiverse.openfga.client.api; + +import java.util.HashMap; +import java.util.Map; + +import javax.annotation.Nullable; + +public class Queries { + + public static Map query(String key1, @Nullable V value1) { + var map = new HashMap(); + if (value1 != null) { + map.put(key1, value1.toString()); + } + return map; + } + + public static Map query(String key1, @Nullable V1 value1, String key2, @Nullable V2 value2) { + var map = query(key1, value1); + if (value2 != null) { + map.put(key2, value2.toString()); + } + return map; + } + + public static Map query(String key1, @Nullable V1 value1, String key2, @Nullable V2 value2, + String key3, + @Nullable V2 value3) { + var map = query(key1, value1, key2, value2); + if (value3 != null) { + map.put(key3, value3.toString()); + } + return map; + } + +} diff --git a/runtime/src/main/java/io/quarkiverse/openfga/client/api/Vars.java b/runtime/src/main/java/io/quarkiverse/openfga/client/api/Vars.java new file mode 100644 index 0000000..5bb30f7 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/openfga/client/api/Vars.java @@ -0,0 +1,60 @@ +package io.quarkiverse.openfga.client.api; + +import static io.vertx.mutiny.uritemplate.Variables.variables; + +import javax.annotation.Nullable; + +import io.vertx.mutiny.uritemplate.Variables; + +public class Vars { + + private static final Variables EMPTY_VARIABLES = variables(); + + public static Variables vars() { + return EMPTY_VARIABLES; + } + + public static Variables vars(String key1, @Nullable V value1) { + var vars = Variables.variables(); + if (value1 != null) { + vars.set(key1, value1.toString()); + } + return vars; + } + + public static Variables vars(String key1, @Nullable V1 value1, String key2, @Nullable V2 value2) { + var vars = vars(key1, value1); + if (value2 != null) { + vars.set(key2, value2.toString()); + } + return vars; + } + + public static Variables vars(String key1, @Nullable V1 value1, String key2, @Nullable V2 value2, + String key3, @Nullable V3 value3) { + var vars = vars(key1, value1, key2, value2); + if (value3 != null) { + vars.set(key3, value3.toString()); + } + return vars; + } + + public static Variables vars(String key1, @Nullable V1 value1, String key2, @Nullable V2 value2, + String key3, @Nullable V3 value3, String key4, @Nullable V4 value4) { + var vars = vars(key1, value1, key2, value2, key3, value3); + if (value4 != null) { + vars.set(key4, value4.toString()); + } + return vars; + } + + public static Variables vars(String key1, @Nullable V1 value1, String key2, @Nullable V2 value2, + String key3, @Nullable V3 value3, String key4, @Nullable V4 value4, String key5, @Nullable V4 value5) { + var vars = vars(key1, value1, key2, value2, key3, value3, key4, value4); + if (value5 != null) { + vars.set(key5, value5.toString()); + } + return vars; + } + +} diff --git a/runtime/src/main/java/io/quarkiverse/openfga/client/api/VertxWebClientFactory.java b/runtime/src/main/java/io/quarkiverse/openfga/client/api/VertxWebClientFactory.java new file mode 100644 index 0000000..af8edae --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/openfga/client/api/VertxWebClientFactory.java @@ -0,0 +1,51 @@ +package io.quarkiverse.openfga.client.api; + +import org.jboss.logging.Logger; + +import io.quarkiverse.openfga.runtime.config.OpenFGAConfig; +import io.quarkus.runtime.TlsConfig; +import io.vertx.core.net.PemTrustOptions; +import io.vertx.core.tracing.TracingPolicy; +import io.vertx.ext.web.client.WebClientOptions; +import io.vertx.mutiny.core.Vertx; +import io.vertx.mutiny.ext.web.client.WebClient; + +class VertxWebClientFactory { + + private static final Logger log = Logger.getLogger(VertxWebClientFactory.class.getName()); + + public static WebClient create(OpenFGAConfig config, TlsConfig tlsConfig, boolean tracingEnabled, Vertx vertx) { + + var url = config.url; + + WebClientOptions options = new WebClientOptions() + .setSsl("https".equals(url.getProtocol())) + .setDefaultHost(url.getHost()) + .setDefaultPort(url.getPort() != -1 ? url.getPort() : url.getDefaultPort()) + .setConnectTimeout((int) config.connectTimeout.toMillis()) + .setIdleTimeout((int) config.readTimeout.getSeconds() * 2) + .setTracingPolicy(tracingEnabled ? TracingPolicy.PROPAGATE : TracingPolicy.IGNORE); + + config.nonProxyHosts.ifPresent(options::setNonProxyHosts); + + boolean trustAll = config.tls.skipVerify.orElseGet(() -> tlsConfig.trustAll); + if (trustAll) { + skipVerify(options); + } else { + config.tls.caCert.ifPresent(caCert -> cacert(options, caCert)); + } + + return WebClient.create(vertx, options); + } + + private static void cacert(WebClientOptions options, String cacert) { + log.debug("configure tls with " + cacert); + options.setTrustOptions(new PemTrustOptions().addCertPath(cacert)); + } + + private static void skipVerify(WebClientOptions options) { + log.debug("configure tls with skip-verify"); + options.setTrustAll(true); + options.setVerifyHost(false); + } +} diff --git a/runtime/src/main/java/io/quarkiverse/openfga/client/api/package-info.java b/runtime/src/main/java/io/quarkiverse/openfga/client/api/package-info.java new file mode 100644 index 0000000..41225cc --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/openfga/client/api/package-info.java @@ -0,0 +1,4 @@ +@ParametersAreNonnullByDefault +package io.quarkiverse.openfga.client.api; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/runtime/src/main/java/io/quarkiverse/openfga/client/package-info.java b/runtime/src/main/java/io/quarkiverse/openfga/client/package-info.java new file mode 100644 index 0000000..8a0b354 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/openfga/client/package-info.java @@ -0,0 +1,4 @@ +@ParametersAreNonnullByDefault +package io.quarkiverse.openfga.client; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/runtime/src/main/java/io/quarkiverse/openfga/client/utils/PaginatedList.java b/runtime/src/main/java/io/quarkiverse/openfga/client/utils/PaginatedList.java new file mode 100644 index 0000000..c3f675d --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/openfga/client/utils/PaginatedList.java @@ -0,0 +1,72 @@ +package io.quarkiverse.openfga.client.utils; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiFunction; + +import javax.annotation.Nullable; + +import io.quarkiverse.openfga.client.model.utils.Preconditions; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; + +public final class PaginatedList { + private final List items; + @Nullable + private final String token; + + public PaginatedList(List items, @Nullable String token) { + this.items = Preconditions.parameterNonNull(items, "items"); + this.token = token; + } + + public Boolean isLastPage() { + return token != null && !token.isEmpty(); + } + + public static Uni> collectAllPages(@Nullable Integer pageSize, + BiFunction>> listGenerator) { + return Multi.createBy() + .repeating().uni(AtomicReference::new, lastToken -> { + return listGenerator.apply(pageSize, lastToken.get()) + .onItem().invoke(list -> lastToken.set(list.getToken())); + }) + .whilst(PaginatedList::isLastPage) + .onItem().transformToIterable(PaginatedList::getItems) + .collect().asList(); + } + + public List getItems() { + return items; + } + + @Nullable + public String getToken() { + return token; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (PaginatedList) obj; + return Objects.equals(this.items, that.items) && + Objects.equals(this.token, that.token); + } + + @Override + public int hashCode() { + return Objects.hash(items, token); + } + + @Override + public String toString() { + return "PaginatedList[" + + "items=" + items + ", " + + "token=" + token + ']'; + } + +} diff --git a/runtime/src/main/java/io/quarkiverse/openfga/runtime/OpenFGARecorder.java b/runtime/src/main/java/io/quarkiverse/openfga/runtime/OpenFGARecorder.java new file mode 100644 index 0000000..e2906ad --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/openfga/runtime/OpenFGARecorder.java @@ -0,0 +1,45 @@ +package io.quarkiverse.openfga.runtime; + +import io.quarkiverse.openfga.client.AuthorizationModelClient; +import io.quarkiverse.openfga.client.StoreClient; +import io.quarkiverse.openfga.client.StoresClient; +import io.quarkiverse.openfga.client.api.API; +import io.quarkiverse.openfga.runtime.config.OpenFGAConfig; +import io.quarkiverse.openfga.runtime.health.OpenFGAHealthCheck; +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.ShutdownContext; +import io.quarkus.runtime.TlsConfig; +import io.quarkus.runtime.annotations.Recorder; +import io.vertx.mutiny.core.Vertx; + +@Recorder +public class OpenFGARecorder { + + public RuntimeValue createAPI(OpenFGAConfig config, TlsConfig tlsConfig, boolean tracingEnabled, + RuntimeValue vertx, ShutdownContext shutdownContext) { + var api = new API(config, tlsConfig, tracingEnabled, Vertx.newInstance(vertx.getValue())); + shutdownContext.addShutdownTask(api::close); + return new RuntimeValue<>(api); + } + + public RuntimeValue createStoresClient(RuntimeValue api) { + StoresClient storesClient = new StoresClient(api.getValue()); + return new RuntimeValue<>(storesClient); + } + + public RuntimeValue createStoreClient(RuntimeValue api, OpenFGAConfig config) { + StoreClient storeClient = new StoreClient(api.getValue(), config.storeId); + return new RuntimeValue<>(storeClient); + } + + public RuntimeValue createAuthModelClient(RuntimeValue api, OpenFGAConfig config) { + AuthorizationModelClient authModelClient = new AuthorizationModelClient(api.getValue(), config.storeId, + config.authorizationModelId.orElse(null)); + return new RuntimeValue<>(authModelClient); + } + + public RuntimeValue createHealthCheck(RuntimeValue api, OpenFGAConfig config) { + OpenFGAHealthCheck healthCheck = new OpenFGAHealthCheck(api.getValue(), config.storeId); + return new RuntimeValue<>(healthCheck); + } +} diff --git a/runtime/src/main/java/io/quarkiverse/openfga/runtime/config/OpenFGAConfig.java b/runtime/src/main/java/io/quarkiverse/openfga/runtime/config/OpenFGAConfig.java new file mode 100644 index 0000000..ba4b444 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/openfga/runtime/config/OpenFGAConfig.java @@ -0,0 +1,79 @@ +package io.quarkiverse.openfga.runtime.config; + +import static io.quarkiverse.openfga.runtime.config.OpenFGAConfig.NAME; + +import java.net.URL; +import java.time.Duration; +import java.util.List; +import java.util.Optional; + +import io.quarkiverse.openfga.client.AuthorizationModelClient; +import io.quarkiverse.openfga.client.StoreClient; +import io.quarkus.runtime.annotations.ConfigDocSection; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(name = NAME, phase = ConfigPhase.RUN_TIME) +public class OpenFGAConfig { + + public static final String NAME = "openfga"; + public static final String DEFAULT_CONNECT_TIMEOUT = "5S"; + public static final String DEFAULT_READ_TIMEOUT = "5S"; + + /** + * OpenFGA server URL. + *

+ * Example: http://openfga:8080 + */ + @ConfigItem + public URL url; + + /** + * Shared authentication key. + */ + @ConfigItem + public Optional sharedKey; + + /** + * Store id for default {@link StoreClient} bean. + */ + @ConfigItem + public String storeId; + + /** + * Authorization model id for default {@link AuthorizationModelClient} bean. + */ + @ConfigItem + public Optional authorizationModelId; + + /** + * TLS configuration. + */ + @ConfigItem + @ConfigDocSection + public OpenFGATLSConfig tls; + + /** + * Timeout to establish a connection with Vault. + */ + @ConfigItem(defaultValue = DEFAULT_CONNECT_TIMEOUT) + public Duration connectTimeout; + + /** + * Request timeout on Vault. + */ + @ConfigItem(defaultValue = DEFAULT_READ_TIMEOUT) + public Duration readTimeout; + + /** + * List of remote hosts that are not proxied when the client is configured to use a proxy. This + * list serves the same purpose as the JVM {@code nonProxyHosts} configuration. + * + *

+ * Entries can use the * wildcard character for pattern matching, e.g *.example.com matches + * www.example.com. + */ + @ConfigItem + public Optional> nonProxyHosts; +} diff --git a/runtime/src/main/java/io/quarkiverse/openfga/runtime/config/OpenFGATLSConfig.java b/runtime/src/main/java/io/quarkiverse/openfga/runtime/config/OpenFGATLSConfig.java new file mode 100644 index 0000000..afed888 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/openfga/runtime/config/OpenFGATLSConfig.java @@ -0,0 +1,30 @@ +package io.quarkiverse.openfga.runtime.config; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class OpenFGATLSConfig { + + /** + * Allows to bypass certificate validation on TLS communications. + *

+ * If true this will allow TLS communications with OpenFGA, without checking the validity of the + * certificate presented by OpenFGA. This is discouraged in production because it allows man in the middle + * type of attacks. + */ + @ConfigItem + public Optional skipVerify; + + /** + * Certificate bundle used to validate TLS communications with OpenFGA. + *

+ * The path to a pem bundle file, if TLS is required, and trusted certificates are not set through + * javax.net.ssl.trustStore system property. + */ + @ConfigItem + public Optional caCert; + +} diff --git a/runtime/src/main/java/io/quarkiverse/openfga/runtime/health/OpenFGAHealthCheck.java b/runtime/src/main/java/io/quarkiverse/openfga/runtime/health/OpenFGAHealthCheck.java new file mode 100644 index 0000000..30ec807 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/openfga/runtime/health/OpenFGAHealthCheck.java @@ -0,0 +1,35 @@ +package io.quarkiverse.openfga.runtime.health; + +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.HealthCheckResponseBuilder; + +import io.quarkiverse.openfga.client.api.API; + +public class OpenFGAHealthCheck implements HealthCheck { + + private final API api; + private final String storeId; + + public OpenFGAHealthCheck(API api, String storeId) { + this.api = api; + this.storeId = storeId; + } + + @Override + public HealthCheckResponse call() { + + final HealthCheckResponseBuilder builder = HealthCheckResponse.named("OpenFGA client connection health check"); + + try { + api.getStore(storeId).await().indefinitely(); + + builder.up(); + + } catch (Exception e) { + builder.down().withData("reason", e.getMessage()).build(); + } + + return builder.build(); + } +} diff --git a/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000..c23af86 --- /dev/null +++ b/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,11 @@ +name: OpenFGA +description: OpenFGA client and JAX-RS fine grained authorization integration. +metadata: + keywords: + - openfga + - fga +# guide: https://quarkiverse.github.io/quarkiverse-docs/openfga/dev/ +categories: +- authorization +- security +status: preview