diff --git a/.gitignore b/.gitignore index 9b21127..e1db716 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,184 @@ -# Ignore Gradle project-specific cache directory +# Created by https://www.toptal.com/developers/gitignore/api/java,gradle,cmake,intellij+all,visualstudiocode +# Edit at https://www.toptal.com/developers/gitignore?templates=java,gradle,cmake,intellij+all,visualstudiocode + +### CMake ### +CMakeLists.txt.user +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +_deps + +### CMake Patch ### +CMakeUserPresets.json + +# External projects +*-prefix/ + +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Java ### +# 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 http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Gradle ### .gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache -# ignore code editor stuff -.idea -.vscode +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath -# Ignore Gradle build output directory -build +### Gradle Patch ### +# Java heap dump +*.hprof -# Ignore stg schemas -stg-schemas/ -bin +# End of https://www.toptal.com/developers/gitignore/api/java,gradle,cmake,intellij+all,visualstudiocode +/generated/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8b47be4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,249 @@ +# Contributing to Permit.io Java SDK + +Thank you for your interest in contributing to the Permit.io Java SDK! This document provides guidelines and +instructions for contributing to the project. + +## Table of Contents + +- [Getting Started](#getting-started) +- [Prerequisites](#prerequisites) +- [Development Workflow](#development-workflow) +- [Regenerating OpenAPI Models](#regenerating-openapi-models) +- [Project Structure](#project-structure) +- [Code Style](#code-style) +- [Submitting Changes](#submitting-changes) +- [Reporting Issues](#reporting-issues) +- [Community](#community) + +## Getting Started + +1. **Fork the repository** on GitHub +2. **Clone your fork** locally: + ```bash + git clone https://github.com/YOUR_USERNAME/permit-java.git + cd permit-java + ``` +3. **Add the upstream remote**: + ```bash + git remote add upstream https://github.com/permitio/permit-java.git + ``` + +## Prerequisites + +Before contributing, ensure you have the following installed: + +- **Java JDK 8+**: For building and running the SDK +- **Java JDK 11+**: Required for running the OpenAPI Generator (the SDK itself targets Java 8) +- **Gradle**: Version 7.0+ (or use the included Gradle wrapper `./gradlew`) +- **OpenAPI Generator**: Required for regenerating API models from the Permit.io OpenAPI specification + +### Installing OpenAPI Generator + +The OpenAPI Generator is used to generate Java model classes from the Permit.io API specification. Install it using one +of the following methods: + +**Using Homebrew (macOS):** + +```bash +brew install openapi-generator +``` + +**Using npm:** + +```bash +npm install @openapitools/openapi-generator-cli -g +``` + +**Using Docker:** + +```bash +docker pull openapitools/openapi-generator-cli +``` + +For more installation options, see +the [OpenAPI Generator Installation Guide](https://openapi-generator.tech/docs/installation). + +## Development Workflow + +1. **Create a feature branch** from `master`: + ```bash + git checkout -b feature/your-feature-name + ``` + +2. **Make your changes** and ensure all tests pass: + ```bash + ./gradlew build + ./gradlew test + ``` + +3. **Commit your changes** with clear, descriptive messages: + ```bash + git commit -m "Add feature: description of your changes" + ``` + +4. **Push to your fork**: + ```bash + git push origin feature/your-feature-name + ``` + +5. **Submit a pull request** with a clear description of your changes + +## Regenerating OpenAPI Models + +The SDK includes auto-generated model classes from the Permit.io OpenAPI specification. If you need to regenerate these +models (e.g., when the API is updated), use the provided Makefile targets: + +### Generate OpenAPI Models + +```bash +make generate-openapi +``` + +If your default Java is version 8, you need to use Java 11+ for the generator: + +```bash +# macOS - use Java 11+ for the generator +JAVA_HOME=$(/usr/libexec/java_home -v 17) make generate-openapi +``` + +This command: + +1. Fetches the latest OpenAPI specification from `https://api.permit.io/v2/openapi.json` +2. Generates Java model classes using the configuration in `openapi-config.json` +3. Outputs the generated code to the `generated/` directory + +Note: The `--skip-validate-spec` flag is used to bypass minor validation issues in the upstream API specification that +do not affect code generation. + +### Copying Generated Files to the Project + +After running `make generate-openapi`, you need to **manually copy** the generated model files to the SDK source +directory: + +```bash +# Copy generated models to the SDK +cp generated/src/main/java/io/permit/sdk/openapi/model/*.java \ + src/main/java/io/permit/sdk/openapi/models/ +``` + +**Important notes:** + +- The generator outputs to `generated/src/main/java/io/permit/sdk/openapi/model/` (singular "model") +- The SDK uses `src/main/java/io/permit/sdk/openapi/models/` (plural "models") +- Only copy the model files you need; not all generated files are used in the SDK +- Review the changes before committing to ensure compatibility + +### Clean Generated Files + +```bash +make clean-openapi +``` + +This removes the `generated/` directory and all auto-generated files. + +### Generate JSON Schema (Optional) + +```bash +make generate-jsonschema +``` + +This generates JSON schema files from the OpenAPI specification and outputs them to the `schemas/` directory. This +requires the `openapi2jsonschema` tool. + +## Project Structure + +The SDK source code is organized under `src/main/java/io/permit/sdk/`: + +``` +src/main/java/io/permit/sdk/ +|-- api/ # API client classes for Permit.io REST API +| |-- models/ # SDK-specific models for API operations +| | |-- CreateOrUpdateResult.java # Result wrapper for sync operations +| | |-- UserModel.java # User data model +| | |-- RoleModel.java # Role data model +| | +-- ... # Other API models +| |-- UsersApi.java # User management operations +| |-- RolesApi.java # Role management operations +| |-- TenantsApi.java # Tenant management operations +| |-- ResourcesApi.java # Resource management operations +| +-- ... # Other API clients +| +|-- enforcement/ # Enforcement models for permission checks +| |-- User.java # User model for check() calls +| |-- Resource.java # Resource model for check() calls +| |-- Enforcer.java # Core enforcement logic +| |-- CheckQuery.java # Permission check query builder +| +-- ... # Other enforcement models +| +|-- examples/ # Example code demonstrating SDK usage +| |-- BasicPermissionCheckExample.java +| |-- AdvancedPermissionCheckExample.java +| +-- UserSyncExample.java +| +|-- openapi/ # Auto-generated OpenAPI models +| +-- models/ # Generated model classes from Permit.io API spec +| # (copy generated files here from generated/src/main/java/...) +| +|-- util/ # Utility classes and helpers +| +|-- Permit.java # Main SDK entry point +|-- PermitConfig.java # SDK configuration builder +|-- PermitContext.java # Context management for API calls +|-- ApiKeyLevel.java # API key permission levels +|-- ApiContextLevel.java # API context level definitions ++-- FactsSyncTimeoutPolicy.java # Timeout policy for facts synchronization +``` + +## Code Style + +Please follow these guidelines when contributing: + +- Follow standard Java naming conventions +- Use meaningful variable and method names +- Include Javadoc comments for public APIs +- Write unit tests for new functionality +- Keep methods focused and concise +- Avoid introducing new dependencies unless necessary + +## Submitting Changes + +### Pull Request Guidelines + +- **One feature per PR**: Keep pull requests focused on a single feature or fix +- **Clear description**: Explain what your changes do and why +- **Tests**: Include tests for new functionality +- **Documentation**: Update documentation if needed +- **Clean history**: Rebase your branch on `master` before submitting + +### Pull Request Template + +When submitting a PR, please include: + +1. **Summary**: Brief description of the changes +2. **Motivation**: Why these changes are needed +3. **Testing**: How the changes were tested +4. **Breaking Changes**: Note any breaking changes (if applicable) + +## Reporting Issues + +When reporting issues, please include: + +- **Java version**: Output of `java -version` +- **SDK version**: The version of the Permit.io SDK you're using +- **Steps to reproduce**: Clear steps to reproduce the issue +- **Expected behavior**: What you expected to happen +- **Actual behavior**: What actually happened +- **Stack trace**: If applicable, include the full stack trace + +Use the [GitHub Issues](https://github.com/permitio/permit-java/issues) page to report bugs or request features. + +## Community + +- **Documentation**: [Permit.io Docs](https://docs.permit.io) +- **Slack**: [Permit.io Community](https://permit.io/slack) +- **GitHub Discussions**: For questions and discussions about the SDK + +## License + +By contributing to this project, you agree that your contributions will be licensed under +the [Apache License 2.0](LICENSE). diff --git a/Makefile b/Makefile index 144fa2c..05e8485 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ ## generate openapi models generate-openapi: - openapi-generator generate -i https://api.permit.io/v2/openapi.json -g java -o generated/ -c openapi-config.json + openapi-generator generate -i https://api.permit.io/v2/openapi.json -g java -o generated/ -c openapi-config.json --skip-validate-spec clean-openapi: rm -rf generated/ diff --git a/README.md b/README.md index 8547414..44aaf85 100644 --- a/README.md +++ b/README.md @@ -1,154 +1,156 @@ ![Java.png](imgs/Java.png) -# Java SDK for Permit.io -Java SDK for interacting with the Permit.io full-stack permissions platform. +# Permit.io Java SDK + +> **Important Notice**: This is the last version of the SDK that supports Java 8. Starting from the next major release, +> the SDK will require **Java 17** or higher, aligning with the most widely adopted LTS version in production +> environments. + +The official Java SDK for interacting with the Permit.io full-stack permissions platform. ## Overview -This guide will walk you through the steps of installing the Permit.io Java SDK and integrating it into your code. +This guide walks you through installing the Permit.io Java SDK and integrating it into your application. + +## Table of Contents + +- [Requirements](#requirements) +- [Installation](#installation) + - [Maven](#maven) + - [Gradle](#gradle) +- [Usage](#usage) +- [Examples](#examples) + - [Running Tests](#running-tests) +- [API Reference](#api-reference) +- [Contributing](#contributing) +- [License](#license) +- [Support](#support) + +## Requirements + +- **Java**: Version 8 or higher +- **Build Tool**: Maven 3.6+ or Gradle 7.0+ +- **Permit.io Account**: An API key from the [Permit.io Dashboard](https://app.permit.io) +- **PDP (Policy Decision Point)**: A running Permit.io PDP container for authorization checks ## Installation -For [Maven](https://maven.apache.org/) projects, use: +### Maven + +Add the following dependency to your `pom.xml` file: ```xml + - io.permit - permit-sdk-java - 2.0.0 + io.permit + permit-sdk-java + 2.0.0 ``` -For [Gradle](https://gradle.org/) projects, configure `permit-sdk-java` as a dependency in your `build.gradle` file: +### Gradle + +Add the following dependency to your `build.gradle` file: ```groovy dependencies { - // ... - implementation 'io.permit:permit-sdk-java:2.0.0' } ``` +For Kotlin DSL (`build.gradle.kts`): + +```kotlin +dependencies { + implementation("io.permit:permit-sdk-java:2.0.0") +} +``` + ## Usage +For complete, runnable examples, see the [Examples](#examples) section below. + ### Initializing the SDK -To init the SDK, you need to create a new Permit client with the API key you got from the Permit.io dashboard. +Create a `Permit` client with your API key from the [Permit.io Dashboard](https://app.permit.io): -First we will create a new `PermitConfig` object so we can pass it to the Permit client. +- See: [BasicPermissionCheckExample.java](src/main/java/io/permit/sdk/examples/BasicPermissionCheckExample.java) - Shows + SDK initialization with `PermitConfig.Builder` -Second, we will create a new `Permit` client with the `PermitConfig` object we created. +### Checking Permissions -```java -import io.permit.sdk.Permit; -import io.permit.sdk.PermitConfig; +Use `permit.check()` to verify if a user can perform an action on a resource: -// This line initializes the SDK and connects your Java app -// to the Permit.io PDP container you've set up in the previous step. -Permit permit = new Permit( - new PermitConfig.Builder("[YOUR_API_KEY]") - // in production, you might need to change this url to fit your deployment - .withPdpAddress("http://localhost:7766") - // optionally, if you wish to get more debug messages to your log, set this to true - .withDebugMode(false) - .build() - ); -``` +- **Basic checks**: + [BasicPermissionCheckExample.java](src/main/java/io/permit/sdk/examples/BasicPermissionCheckExample.java) - + Permission checks with default tenant +- **Advanced checks**: + [AdvancedPermissionCheckExample.java](src/main/java/io/permit/sdk/examples/AdvancedPermissionCheckExample.java) - + Checks with user attributes and explicit tenant. -### Checking permissions +### Syncing Users -To check permissions using our `permit.check()` method, you will have to create User and Resource models as input to the permission check. -The models are located in `` +Before running permission checks, sync users to Permit.io using `permit.api.users.sync()`: -Follow the example below: +- See: [UserSyncExample.java](src/main/java/io/permit/sdk/examples/UserSyncExample.java) - User synchronization with + full profile and minimal information. -```java -import io.permit.sdk.enforcement.Resource; -import io.permit.sdk.enforcement.User; -import io.permit.sdk.Permit; +## Examples -boolean permitted = permit.check( - // building the user object using User.fromString() - // the user key (this is the unique identifier of the user in the permission system). - User.fromString("[USER KEY]"), - // the action key (string) - "create", - // the resource object, can be initialized from string if the "default" tenant is used. - Resource.fromString("document") -); +Complete, runnable examples are available in the [ +`src/main/java/io/permit/sdk/examples`](src/main/java/io/permit/sdk/examples) directory: -if (permitted) { - System.out.println("User is PERMITTED to create a document in the 'default' tenant"); -} else { - System.out.println("User is NOT PERMITTED to create a document in the 'default' tenant"); -} -``` +| Example | Description | +|-----------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------| +| [BasicPermissionCheckExample.java](src/main/java/io/permit/sdk/examples/BasicPermissionCheckExample.java) | SDK initialization and basic permission checks | +| [AdvancedPermissionCheckExample.java](src/main/java/io/permit/sdk/examples/AdvancedPermissionCheckExample.java) | Permission checks with user attributes and tenant context | +| [UserSyncExample.java](src/main/java/io/permit/sdk/examples/UserSyncExample.java) | Synchronizing users with the Permit API | -A more complicated example (passing attributes on the user object, using an explicit tenant in the resource): +### Running Tests -```java -import io.permit.sdk.enforcement.Resource; -import io.permit.sdk.enforcement.User; -import java.util.HashMap; +Each example has a corresponding unit test that uses Mockito mocks (no PDP required): +| Example | Test | +|--------------------------------|-------------------------------------------------------------------------------------------------------------------------| +| BasicPermissionCheckExample | [BasicPermissionCheckExampleTest.java](src/test/java/io/permit/sdk/examples/BasicPermissionCheckExampleTest.java) | +| AdvancedPermissionCheckExample | [AdvancedPermissionCheckExampleTest.java](src/test/java/io/permit/sdk/examples/AdvancedPermissionCheckExampleTest.java) | +| UserSyncExample | [UserSyncExampleTest.java](src/test/java/io/permit/sdk/examples/UserSyncExampleTest.java) | -HashMap userAttributes = new HashMap<>(); -userAttributes.put("age", Integer.valueOf(20)); -userAttributes.put("favorite_color", "yellow"); +```bash +# Run all tests +./gradlew test -boolean permitted = permit.check( - // building the user object using the User.Builder class - new User.Builder("[USER KEY]").withAttributes(userAttributes).build(), - // the action key (string) - "create", - // building the resource object using the Resource.Builder in order to pass an explicit tenant key: "awesome-inc" - new Resource.Builder("document").withTenant("awesome-inc").build() -); +# Run only example tests +./gradlew test --tests "io.permit.sdk.examples.*" -if (permitted) { - System.out.println("User is PERMITTED to create a document in the 'awesome-inc' tenant"); -} else { - System.out.println("User is NOT PERMITTED to create a document in the 'awesome-inc' tenant"); -} +# Run a specific test +./gradlew test --tests "io.permit.sdk.examples.BasicPermissionCheckExampleTest" ``` -### Syncing users - -When the user first logins, and after you check if he authenticated successfully (i.e: **by checking the JWT access token**) - -you need to declare the user in the permission system so you can run `permit.check()` on that user. +## API Reference -To declare (or "sync") a user in the Permit.io API, use the `permit.api.users.sync()` method. +For complete API documentation, refer to +the [Javadoc reference](https://javadoc.io/doc/io.permit/permit-sdk-java/latest/index.html). -Follow the example below: +The recommended starting point is +the [Permit](https://javadoc.io/doc/io.permit/permit-sdk-java/latest/io/permit/sdk/Permit.html) class, which serves as +the main entry point for the SDK. -```java -import io.permit.sdk.api.models.CreateOrUpdateResult; -import io.permit.sdk.enforcement.User; +## Contributing -HashMap userAttributes = new HashMap<>(); -userAttributes.put("age", Integer.valueOf(50)); -userAttributes.put("fav_color", "red"); +We welcome contributions to the Permit.io Java SDK! Please see our [Contributing Guide](CONTRIBUTING.md) for details on: -CreateOrUpdateResult result = permit.api.users.sync( - (new User.Builder("auth0|elon")) - .withEmail("elonmusk@tesla.com") - .withFirstName("Elon") - .withLastName("Musk") - .withAttributes(userAttributes) - .build() -); -UserRead user = result.getResult(); -assertTrue(result.wasCreated()); -``` - -Most params to UserCreates are optional, and only the unique user key is needed. This is valid: +- Setting up your development environment +- Development workflow and code style guidelines +- How to regenerate OpenAPI models +- Submitting pull requests -```java -CreateOrUpdateResult result = permit.api.users.sync(new UserCreate("[USER KEY]")); -``` +## License -## Javadoc reference +This SDK is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details. -To view the javadoc reference, [click here](https://javadoc.io/doc/io.permit/permit-sdk-java/2.0.0/index.html). +## Support -It's easiest to start with the root [Permit](https://javadoc.io/static/io.permit/permit-sdk-java/2.0.0/io/permit/sdk/Permit.html) class. +- **Documentation**: [Permit.io Docs](https://docs.permit.io) +- **GitHub Issues**: [Report an issue](https://github.com/permitio/permit-java/issues) +- **Community**: [Permit.io Slack](https://permit.io/slack) diff --git a/build.gradle b/build.gradle index cc9bfec..66bd526 100644 --- a/build.gradle +++ b/build.gradle @@ -36,7 +36,7 @@ repositories { java { toolchain { - languageVersion = JavaLanguageVersion.of(8) + languageVersion = JavaLanguageVersion.of(8) } // sources are required by maven central in order to accept the package withSourcesJar() @@ -65,15 +65,21 @@ dependencies { implementation "jakarta.annotation:jakarta.annotation-api:1.3.5" // logger - implementation 'ch.qos.logback:logback-classic:1.4.14' - implementation 'ch.qos.logback:logback-core:1.4.14' - implementation 'org.slf4j:slf4j-api:1.7.33' - + // SLF4J API is the logging facade used by main code - consumers provide their own implementation + implementation 'org.slf4j:slf4j-api:2.0.9' + // Logback is only needed for tests (not used in main code, only SLF4J API is) + // Note: 1.3.x is required for Java 8 compatibility (1.4.x requires Java 11+) + testImplementation 'ch.qos.logback:logback-classic:1.3.14' + testImplementation 'ch.qos.logback:logback-core:1.3.14' // Use JUnit Jupiter for testing. testImplementation 'org.junit.jupiter:junit-jupiter:5.7.2' + // Mockito for unit testing with mocks (4.6.x supports Java 8 and works with older Gradle) + testImplementation 'org.mockito:mockito-core:4.6.1' + testImplementation 'org.mockito:mockito-junit-jupiter:4.6.1' + // These dependencies are used internally, and not exposed to consumers on their own compile classpath. // google standard java library implementation 'com.google.guava:guava:32.0.0-jre' @@ -228,6 +234,6 @@ tasks.named('test') { tasks.named('jar') { manifest { attributes('Implementation-Title': project.name, - 'Implementation-Version': project.version) + 'Implementation-Version': project.version) } -} \ No newline at end of file +} diff --git a/src/main/java/io/permit/sdk/examples/AdvancedPermissionCheckExample.java b/src/main/java/io/permit/sdk/examples/AdvancedPermissionCheckExample.java new file mode 100644 index 0000000..a271415 --- /dev/null +++ b/src/main/java/io/permit/sdk/examples/AdvancedPermissionCheckExample.java @@ -0,0 +1,213 @@ +package io.permit.sdk.examples; + +import io.permit.sdk.Permit; +import io.permit.sdk.PermitConfig; +import io.permit.sdk.api.PermitApiError; +import io.permit.sdk.enforcement.Resource; +import io.permit.sdk.enforcement.User; + +import java.io.IOException; +import java.util.HashMap; + +/** + * AdvancedPermissionCheckExample demonstrates advanced permission checking + * with user attributes, resource attributes, and explicit tenant specification. + * + * This example shows: + * - How to create users with attributes (for ABAC - Attribute-Based Access Control) + * - How to create resources with specific instances and tenants + * - How to perform permission checks with full context + */ +public class AdvancedPermissionCheckExample { + + private final Permit permit; + + /** + * Creates a new AdvancedPermissionCheckExample with the given Permit instance. + * This constructor allows dependency injection for testing. + * + * @param permit The Permit SDK instance to use for permission checks + */ + public AdvancedPermissionCheckExample(Permit permit) { + this.permit = permit; + } + + /** + * Creates a new AdvancedPermissionCheckExample with the given API key. + * + * @param apiKey Your Permit.io API key + * @param pdpAddress The address of your Policy Decision Point + */ + public AdvancedPermissionCheckExample(String apiKey, String pdpAddress) { + PermitConfig config = new PermitConfig.Builder(apiKey) + .withPdpAddress(pdpAddress) + .withDebugMode(false) + .build(); + this.permit = new Permit(config); + } + + /** + * Checks permission for a user with attributes against a resource in a specific tenant. + * + * @param userKey The unique identifier for the user + * @param userEmail The user's email address + * @param userAttributes Additional attributes for the user (for ABAC) + * @param action The action to check (e.g., "read", "write", "delete") + * @param resourceType The type of resource (e.g., "document", "folder") + * @param resourceKey The specific resource instance key (can be null for type-level check) + * @param tenant The tenant context for the permission check + * @return true if the user is permitted, false otherwise + * @throws IOException if there's a network error communicating with the PDP + * @throws PermitApiError if the PDP returns an error response + */ + public boolean checkPermissionWithContext( + String userKey, + String userEmail, + HashMap userAttributes, + String action, + String resourceType, + String resourceKey, + String tenant) throws IOException, PermitApiError { + + // Build a User with attributes using the Builder pattern + User.Builder userBuilder = new User.Builder(userKey); + if (userEmail != null) { + userBuilder.withEmail(userEmail); + } + if (userAttributes != null) { + userBuilder.withAttributes(userAttributes); + } + User user = userBuilder.build(); + + // Build a Resource with tenant and optional key using the Builder pattern + Resource.Builder resourceBuilder = new Resource.Builder(resourceType); + if (resourceKey != null) { + resourceBuilder.withKey(resourceKey); + } + if (tenant != null) { + resourceBuilder.withTenant(tenant); + } + Resource resource = resourceBuilder.build(); + + // Perform the permission check + return permit.check(user, action, resource); + } + + /** + * Checks permission for a user with attributes against a resource with attributes. + * This is useful for Attribute-Based Access Control (ABAC) scenarios. + * + * @param userKey The unique identifier for the user + * @param userAttributes Attributes for the user (for ABAC) + * @param action The action to check + * @param resourceType The type of resource + * @param resourceKey The specific resource instance key + * @param resourceAttributes Attributes for the resource (for ABAC) + * @param tenant The tenant context + * @return true if the user is permitted, false otherwise + * @throws IOException if there's a network error + * @throws PermitApiError if the PDP returns an error + */ + public boolean checkPermissionWithAttributes( + String userKey, + HashMap userAttributes, + String action, + String resourceType, + String resourceKey, + HashMap resourceAttributes, + String tenant) throws IOException, PermitApiError { + + // Build a User with attributes + User user = new User.Builder(userKey) + .withAttributes(userAttributes) + .build(); + + // Build a Resource with attributes and tenant + Resource.Builder resourceBuilder = new Resource.Builder(resourceType); + if (resourceKey != null) { + resourceBuilder.withKey(resourceKey); + } + if (resourceAttributes != null) { + resourceBuilder.withAttributes(resourceAttributes); + } + if (tenant != null) { + resourceBuilder.withTenant(tenant); + } + Resource resource = resourceBuilder.build(); + + // Perform the permission check + return permit.check(user, action, resource); + } + + /** + * Main method demonstrating advanced usage of the Permit SDK. + * + * @param args Command line arguments (not used) + */ + public static void main(String[] args) { + // Get API key from environment variable + String apiKey = System.getenv("PERMIT_API_KEY"); + if (apiKey == null || apiKey.isEmpty()) { + System.err.println("Please set the PERMIT_API_KEY environment variable"); + System.exit(1); + } + + String pdpAddress = System.getenv().getOrDefault("PDP_URL", "http://localhost:7766"); + AdvancedPermissionCheckExample example = new AdvancedPermissionCheckExample(apiKey, pdpAddress); + + try { + // Example 1: Check permission with user attributes and tenant + String userKey = "john@acme.com"; + String userEmail = "john@acme.com"; + + // User attributes for ABAC (e.g., department, clearance level) + HashMap userAttributes = new HashMap(); + userAttributes.put("department", "engineering"); + userAttributes.put("clearance_level", 3); + + String action = "edit"; + String resourceType = "document"; + String resourceKey = "doc-123"; + String tenant = "acme-corp"; + + boolean permitted = example.checkPermissionWithContext( + userKey, userEmail, userAttributes, + action, resourceType, resourceKey, tenant + ); + + System.out.println("Example 1 - Permission check with tenant:"); + System.out.println(" User: " + userKey); + System.out.println(" Action: " + action); + System.out.println(" Resource: " + resourceType + ":" + resourceKey); + System.out.println(" Tenant: " + tenant); + System.out.println(" Result: " + (permitted ? "PERMITTED" : "DENIED")); + System.out.println(); + + // Example 2: Check permission with both user and resource attributes + HashMap resourceAttributes = new HashMap(); + resourceAttributes.put("classification", "confidential"); + resourceAttributes.put("owner", "engineering"); + + boolean permitted2 = example.checkPermissionWithAttributes( + userKey, userAttributes, + "delete", resourceType, resourceKey, + resourceAttributes, tenant + ); + + System.out.println("Example 2 - Permission check with ABAC attributes:"); + System.out.println(" User: " + userKey + " (department=engineering, clearance=3)"); + System.out.println(" Action: delete"); + System.out.println(" Resource: " + resourceType + ":" + resourceKey + " (classification=confidential)"); + System.out.println(" Tenant: " + tenant); + System.out.println(" Result: " + (permitted2 ? "PERMITTED" : "DENIED")); + + } catch (IOException e) { + System.err.println("Network error while checking permission: " + e.getMessage()); + e.printStackTrace(); + } catch (PermitApiError e) { + System.err.println("Permit API error: " + e.getMessage()); + System.err.println("Response code: " + e.getResponseCode()); + e.printStackTrace(); + } + } +} diff --git a/src/main/java/io/permit/sdk/examples/BasicPermissionCheckExample.java b/src/main/java/io/permit/sdk/examples/BasicPermissionCheckExample.java new file mode 100644 index 0000000..1ac3605 --- /dev/null +++ b/src/main/java/io/permit/sdk/examples/BasicPermissionCheckExample.java @@ -0,0 +1,108 @@ +package io.permit.sdk.examples; + +import io.permit.sdk.Permit; +import io.permit.sdk.PermitConfig; +import io.permit.sdk.api.PermitApiError; +import io.permit.sdk.enforcement.Resource; +import io.permit.sdk.enforcement.User; + +import java.io.IOException; + +/** + * BasicPermissionCheckExample demonstrates how to initialize the Permit SDK + * and perform a basic permission check. + * + * This example shows: + * - How to create a PermitConfig with your API key + * - How to initialize the Permit SDK + * - How to create User and Resource objects + * - How to perform a permission check + */ +public class BasicPermissionCheckExample { + + private final Permit permit; + + /** + * Creates a new BasicPermissionCheckExample with the given Permit instance. + * This constructor allows dependency injection for testing. + * + * @param permit The Permit SDK instance to use for permission checks + */ + public BasicPermissionCheckExample(Permit permit) { + this.permit = permit; + } + + /** + * Creates a new BasicPermissionCheckExample with the given API key. + * Uses default PDP address (localhost:7766). + * + * @param apiKey Your Permit.io API key + */ + public BasicPermissionCheckExample(String apiKey) { + PermitConfig config = new PermitConfig.Builder(apiKey) + .withPdpAddress("http://localhost:7766") + .withDebugMode(false) + .build(); + this.permit = new Permit(config); + } + + /** + * Checks if a user is permitted to perform an action on a resource. + * + * @param userKey The unique identifier for the user + * @param action The action to check (e.g., "read", "write", "delete") + * @param resourceType The type of resource (e.g., "document", "folder") + * @return true if the user is permitted, false otherwise + * @throws IOException if there's a network error communicating with the PDP + * @throws PermitApiError if the PDP returns an error response + */ + public boolean checkPermission(String userKey, String action, String resourceType) + throws IOException, PermitApiError { + // Create a User object from the user key + User user = User.fromString(userKey); + + // Create a Resource object from the resource type + Resource resource = Resource.fromString(resourceType); + + // Perform the permission check + return permit.check(user, action, resource); + } + + /** + * Main method demonstrating basic usage of the Permit SDK. + * + * @param args Command line arguments (not used) + */ + public static void main(String[] args) { + // Get API key from environment variable or use a placeholder + String apiKey = System.getenv("PERMIT_API_KEY"); + if (apiKey == null || apiKey.isEmpty()) { + System.err.println("Please set the PERMIT_API_KEY environment variable"); + System.exit(1); + } + + BasicPermissionCheckExample example = new BasicPermissionCheckExample(apiKey); + + try { + // Example: Check if user "john@example.com" can "read" a "document" + String userKey = "john@example.com"; + String action = "read"; + String resourceType = "document"; + + boolean permitted = example.checkPermission(userKey, action, resourceType); + + if (permitted) { + System.out.println("User '" + userKey + "' is PERMITTED to " + action + " " + resourceType); + } else { + System.out.println("User '" + userKey + "' is DENIED to " + action + " " + resourceType); + } + } catch (IOException e) { + System.err.println("Network error while checking permission: " + e.getMessage()); + e.printStackTrace(); + } catch (PermitApiError e) { + System.err.println("Permit API error: " + e.getMessage()); + System.err.println("Response code: " + e.getResponseCode()); + e.printStackTrace(); + } + } +} diff --git a/src/main/java/io/permit/sdk/examples/UserSyncExample.java b/src/main/java/io/permit/sdk/examples/UserSyncExample.java new file mode 100644 index 0000000..af51d76 --- /dev/null +++ b/src/main/java/io/permit/sdk/examples/UserSyncExample.java @@ -0,0 +1,214 @@ +package io.permit.sdk.examples; + +import io.permit.sdk.Permit; +import io.permit.sdk.PermitConfig; +import io.permit.sdk.api.PermitApiError; +import io.permit.sdk.api.PermitContextError; +import io.permit.sdk.api.models.CreateOrUpdateResult; +import io.permit.sdk.enforcement.User; +import io.permit.sdk.openapi.models.UserRead; + +import java.io.IOException; +import java.util.HashMap; + +/** + * UserSyncExample demonstrates how to synchronize users with the Permit API. + * + * User synchronization is the process of creating or updating users in Permit. + * This is typically done when: + * - A new user signs up in your application + * - User information changes (email, name, attributes) + * - You want to pre-populate users before they first access your application + * + * This example shows: + * - How to sync a simple user by key + * - How to sync a user with full profile information + * - How to sync a user with custom attributes for ABAC + * - How to handle the sync result (created vs updated) + */ +public class UserSyncExample { + + private final Permit permit; + + /** + * Creates a new UserSyncExample with the given Permit instance. + * This constructor allows dependency injection for testing. + * + * @param permit The Permit SDK instance to use for user synchronization + */ + public UserSyncExample(Permit permit) { + this.permit = permit; + } + + /** + * Creates a new UserSyncExample with the given API key. + * + * @param apiKey Your Permit.io API key + * @param pdpAddress The address of your Policy Decision Point + */ + public UserSyncExample(String apiKey, String pdpAddress) { + PermitConfig config = new PermitConfig.Builder(apiKey) + .withPdpAddress(pdpAddress) + .withDebugMode(false) + .build(); + this.permit = new Permit(config); + } + + /** + * Syncs a simple user identified only by their key. + * If the user already exists, this will update them (no-op if no changes). + * If the user doesn't exist, this will create them. + * + * @param userKey The unique identifier for the user + * @return The result containing the user data and whether it was created + * @throws IOException if there's a network error + * @throws PermitApiError if the Permit API returns an error + * @throws PermitContextError if the API context is not properly configured + */ + public CreateOrUpdateResult syncSimpleUser(String userKey) + throws IOException, PermitApiError, PermitContextError { + User user = User.fromString(userKey); + return permit.api.users.sync(user); + } + + /** + * Syncs a user with full profile information. + * + * @param userKey The unique identifier for the user + * @param email The user's email address + * @param firstName The user's first name + * @param lastName The user's last name + * @return The result containing the user data and whether it was created + * @throws IOException if there's a network error + * @throws PermitApiError if the Permit API returns an error + * @throws PermitContextError if the API context is not properly configured + */ + public CreateOrUpdateResult syncUserWithProfile( + String userKey, + String email, + String firstName, + String lastName) throws IOException, PermitApiError, PermitContextError { + + User user = new User.Builder(userKey) + .withEmail(email) + .withFirstName(firstName) + .withLastName(lastName) + .build(); + + return permit.api.users.sync(user); + } + + /** + * Syncs a user with custom attributes for Attribute-Based Access Control (ABAC). + * Attributes can be used in permission policies to make fine-grained access decisions. + * + * @param userKey The unique identifier for the user + * @param email The user's email address + * @param firstName The user's first name + * @param lastName The user's last name + * @param attributes Custom attributes for ABAC (e.g., department, role, clearance) + * @return The result containing the user data and whether it was created + * @throws IOException if there's a network error + * @throws PermitApiError if the Permit API returns an error + * @throws PermitContextError if the API context is not properly configured + */ + public CreateOrUpdateResult syncUserWithAttributes( + String userKey, + String email, + String firstName, + String lastName, + HashMap attributes) throws IOException, PermitApiError, PermitContextError { + + User user = new User.Builder(userKey) + .withEmail(email) + .withFirstName(firstName) + .withLastName(lastName) + .withAttributes(attributes) + .build(); + + return permit.api.users.sync(user); + } + + /** + * Main method demonstrating user synchronization with the Permit SDK. + * + * @param args Command line arguments (not used) + */ + public static void main(String[] args) { + // Get API key from environment variable + String apiKey = System.getenv("PERMIT_API_KEY"); + if (apiKey == null || apiKey.isEmpty()) { + System.err.println("Please set the PERMIT_API_KEY environment variable"); + System.exit(1); + } + + String pdpAddress = System.getenv().getOrDefault("PDP_URL", "http://localhost:7766"); + UserSyncExample example = new UserSyncExample(apiKey, pdpAddress); + + try { + // Example 1: Sync a simple user + System.out.println("Example 1: Syncing a simple user..."); + CreateOrUpdateResult result1 = example.syncSimpleUser("user-001"); + + UserRead user1 = result1.getResult(); + System.out.println(" User key: " + user1.key); + System.out.println(" User ID: " + user1.id); + System.out.println(" Was created: " + result1.wasCreated()); + System.out.println(); + + // Example 2: Sync a user with profile information + System.out.println("Example 2: Syncing a user with profile..."); + CreateOrUpdateResult result2 = example.syncUserWithProfile( + "john.doe@example.com", + "john.doe@example.com", + "John", + "Doe" + ); + + UserRead user2 = result2.getResult(); + System.out.println(" User key: " + user2.key); + System.out.println(" Email: " + user2.email); + System.out.println(" First name: " + user2.firstName); + System.out.println(" Last name: " + user2.lastName); + System.out.println(" Was created: " + result2.wasCreated()); + System.out.println(); + + // Example 3: Sync a user with ABAC attributes + System.out.println("Example 3: Syncing a user with ABAC attributes..."); + HashMap attributes = new HashMap(); + attributes.put("department", "engineering"); + attributes.put("clearance_level", 5); + attributes.put("is_manager", true); + attributes.put("teams", new String[]{"backend", "devops"}); + + CreateOrUpdateResult result3 = example.syncUserWithAttributes( + "alice.smith@example.com", + "alice.smith@example.com", + "Alice", + "Smith", + attributes + ); + + UserRead user3 = result3.getResult(); + System.out.println(" User key: " + user3.key); + System.out.println(" Email: " + user3.email); + System.out.println(" First name: " + user3.firstName); + System.out.println(" Last name: " + user3.lastName); + System.out.println(" Attributes: " + user3.attributes); + System.out.println(" Was created: " + result3.wasCreated()); + + } catch (IOException e) { + System.err.println("Network error while syncing user: " + e.getMessage()); + e.printStackTrace(); + } catch (PermitApiError e) { + System.err.println("Permit API error: " + e.getMessage()); + System.err.println("Response code: " + e.getResponseCode()); + System.err.println("Raw response: " + e.getRawResponse()); + e.printStackTrace(); + } catch (PermitContextError e) { + System.err.println("Permit context error: " + e.getMessage()); + System.err.println("Make sure you are using an Environment-level API key"); + e.printStackTrace(); + } + } +} diff --git a/src/test/java/io/permit/sdk/examples/AdvancedPermissionCheckExampleTest.java b/src/test/java/io/permit/sdk/examples/AdvancedPermissionCheckExampleTest.java new file mode 100644 index 0000000..ca66621 --- /dev/null +++ b/src/test/java/io/permit/sdk/examples/AdvancedPermissionCheckExampleTest.java @@ -0,0 +1,309 @@ +package io.permit.sdk.examples; + +import io.permit.sdk.Permit; +import io.permit.sdk.PermitConfig; +import io.permit.sdk.api.PermitApiError; +import io.permit.sdk.enforcement.Resource; +import io.permit.sdk.enforcement.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.IOException; +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +/** + * Unit tests for AdvancedPermissionCheckExample. + * These tests use Mockito to mock the Permit class and verify that user attributes, + * resource attributes, and tenant context are handled correctly by the actual example class. + */ +@ExtendWith(MockitoExtension.class) +class AdvancedPermissionCheckExampleTest { + + @Mock + private Permit mockPermit; + + private AdvancedPermissionCheckExample example; + + @BeforeEach + void setUp() { + example = new AdvancedPermissionCheckExample(mockPermit); + } + + @Test + @DisplayName("Should check permission with user email and tenant") + void testCheckPermissionWithContext_FullContext() throws IOException, PermitApiError { + // Given + String userKey = "user-123"; + String userEmail = "user@example.com"; + HashMap userAttributes = new HashMap(); + userAttributes.put("department", "engineering"); + String action = "edit"; + String resourceType = "document"; + String resourceKey = "doc-456"; + String tenant = "acme-corp"; + + when(mockPermit.check(any(User.class), eq(action), any(Resource.class))) + .thenReturn(true); + + // When + boolean result = example.checkPermissionWithContext( + userKey, userEmail, userAttributes, + action, resourceType, resourceKey, tenant + ); + + // Then + assertTrue(result); + + // Verify that check was called with correct parameters + ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); + ArgumentCaptor resourceCaptor = ArgumentCaptor.forClass(Resource.class); + verify(mockPermit).check(userCaptor.capture(), eq(action), resourceCaptor.capture()); + + User capturedUser = userCaptor.getValue(); + assertEquals(userKey, capturedUser.getKey()); + assertEquals(userEmail, capturedUser.getEmail()); + assertEquals("engineering", capturedUser.getAttributes().get("department")); + + Resource capturedResource = resourceCaptor.getValue(); + assertEquals(resourceType, capturedResource.getType()); + assertEquals(resourceKey, capturedResource.getKey()); + assertEquals(tenant, capturedResource.getTenant()); + } + + @Test + @DisplayName("Should check permission with null optional fields") + void testCheckPermissionWithContext_NullOptionalFields() throws IOException, PermitApiError { + // Given + String userKey = "user-123"; + String action = "read"; + String resourceType = "document"; + + when(mockPermit.check(any(User.class), eq(action), any(Resource.class))) + .thenReturn(true); + + // When - email, attributes, resourceKey, and tenant are all null + boolean result = example.checkPermissionWithContext( + userKey, null, null, + action, resourceType, null, null + ); + + // Then + assertTrue(result); + + ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); + ArgumentCaptor resourceCaptor = ArgumentCaptor.forClass(Resource.class); + verify(mockPermit).check(userCaptor.capture(), eq(action), resourceCaptor.capture()); + + User capturedUser = userCaptor.getValue(); + assertEquals(userKey, capturedUser.getKey()); + assertNull(capturedUser.getEmail()); + assertNull(capturedUser.getAttributes()); + + Resource capturedResource = resourceCaptor.getValue(); + assertEquals(resourceType, capturedResource.getType()); + assertNull(capturedResource.getKey()); + assertNull(capturedResource.getTenant()); + } + + @Test + @DisplayName("Should check permission with ABAC attributes on both user and resource") + void testCheckPermissionWithAttributes_FullABAC() throws IOException, PermitApiError { + // Given + String userKey = "user-123"; + HashMap userAttributes = new HashMap(); + userAttributes.put("clearance_level", 5); + userAttributes.put("department", "security"); + + String action = "access"; + String resourceType = "classified-document"; + String resourceKey = "secret-doc-001"; + + HashMap resourceAttributes = new HashMap(); + resourceAttributes.put("classification", "top-secret"); + resourceAttributes.put("required_clearance", 5); + + String tenant = "gov-agency"; + + when(mockPermit.check(any(User.class), eq(action), any(Resource.class))) + .thenReturn(true); + + // When + boolean result = example.checkPermissionWithAttributes( + userKey, userAttributes, + action, resourceType, resourceKey, + resourceAttributes, tenant + ); + + // Then + assertTrue(result); + + ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); + ArgumentCaptor resourceCaptor = ArgumentCaptor.forClass(Resource.class); + verify(mockPermit).check(userCaptor.capture(), eq(action), resourceCaptor.capture()); + + User capturedUser = userCaptor.getValue(); + assertEquals(5, capturedUser.getAttributes().get("clearance_level")); + assertEquals("security", capturedUser.getAttributes().get("department")); + + Resource capturedResource = resourceCaptor.getValue(); + assertEquals("top-secret", capturedResource.getAttributes().get("classification")); + assertEquals(5, capturedResource.getAttributes().get("required_clearance")); + } + + @Test + @DisplayName("Should return false when permission is denied with ABAC") + void testCheckPermissionWithAttributes_Denied() throws IOException, PermitApiError { + // Given + String userKey = "user-123"; + HashMap userAttributes = new HashMap(); + userAttributes.put("clearance_level", 2); // Lower clearance + + HashMap resourceAttributes = new HashMap(); + resourceAttributes.put("required_clearance", 5); // Requires higher clearance + + when(mockPermit.check(any(User.class), eq("access"), any(Resource.class))) + .thenReturn(false); // Permission denied + + // When + boolean result = example.checkPermissionWithAttributes( + userKey, userAttributes, + "access", "classified-document", "doc-001", + resourceAttributes, "tenant" + ); + + // Then + assertFalse(result, "User with lower clearance should be denied"); + } + + @Test + @DisplayName("Should propagate IOException from Permit SDK") + void testCheckPermissionWithContext_IOException() throws IOException, PermitApiError { + // Given + when(mockPermit.check(any(User.class), any(String.class), any(Resource.class))) + .thenThrow(new IOException("Connection timeout")); + + // When/Then + IOException exception = assertThrows(IOException.class, new org.junit.jupiter.api.function.Executable() { + @Override + public void execute() throws Throwable { + example.checkPermissionWithContext( + "user", "user@example.com", null, + "read", "document", null, null + ); + } + }); + assertEquals("Connection timeout", exception.getMessage()); + } + + @Test + @DisplayName("Should propagate PermitApiError from Permit SDK") + void testCheckPermissionWithAttributes_PermitApiError() throws IOException, PermitApiError { + // Given + when(mockPermit.check(any(User.class), any(String.class), any(Resource.class))) + .thenThrow(new PermitApiError("Unauthorized", 401, "{\"error\":\"Invalid token\"}")); + + // When/Then + PermitApiError exception = assertThrows(PermitApiError.class, new org.junit.jupiter.api.function.Executable() { + @Override + public void execute() throws Throwable { + example.checkPermissionWithAttributes( + "user", new HashMap(), + "read", "document", null, + null, null + ); + } + }); + assertEquals(401, exception.getResponseCode()); + } + + @Test + @DisplayName("Should handle multiple permission checks with different tenants") + void testCheckPermissionWithContext_MultipleTenants() throws IOException, PermitApiError { + // Given - user is permitted in tenant1 but not in tenant2 + when(mockPermit.check(any(User.class), eq("edit"), any(Resource.class))) + .thenAnswer(invocation -> { + Resource resource = invocation.getArgument(2); + return "tenant1".equals(resource.getTenant()); + }); + + // When + boolean resultTenant1 = example.checkPermissionWithContext( + "user", null, null, "edit", "document", "doc-1", "tenant1" + ); + boolean resultTenant2 = example.checkPermissionWithContext( + "user", null, null, "edit", "document", "doc-1", "tenant2" + ); + + // Then + assertTrue(resultTenant1, "User should be permitted in tenant1"); + assertFalse(resultTenant2, "User should be denied in tenant2"); + } + + @Test + @DisplayName("Should handle complex user attributes") + void testCheckPermissionWithContext_ComplexAttributes() throws IOException, PermitApiError { + // Given + HashMap userAttributes = new HashMap(); + userAttributes.put("roles", new String[]{"admin", "editor"}); + userAttributes.put("is_active", true); + userAttributes.put("login_count", 42); + HashMap metadata = new HashMap(); + metadata.put("source", "oauth"); + metadata.put("provider", "google"); + userAttributes.put("metadata", metadata); + + when(mockPermit.check(any(User.class), eq("manage"), any(Resource.class))) + .thenReturn(true); + + // When + boolean result = example.checkPermissionWithContext( + "admin-user", "admin@example.com", userAttributes, + "manage", "settings", "global-settings", "main-tenant" + ); + + // Then + assertTrue(result); + + ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); + verify(mockPermit).check(userCaptor.capture(), eq("manage"), any(Resource.class)); + + User capturedUser = userCaptor.getValue(); + assertNotNull(capturedUser.getAttributes()); + assertTrue((Boolean) capturedUser.getAttributes().get("is_active")); + assertEquals(42, capturedUser.getAttributes().get("login_count")); + } + + @Test + @DisplayName("Should verify the actual example class can be instantiated with a real Permit instance") + void testAdvancedPermissionCheckExample_CanBeInstantiated() { + try { + // Given + PermitConfig config = new PermitConfig.Builder("test-api-key") + .withPdpAddress("http://localhost:7766") + .build(); + Permit permit = new Permit(config); + + // When + AdvancedPermissionCheckExample realExample = new AdvancedPermissionCheckExample(permit); + + // Then + assertNotNull(realExample, "Should be able to create example with real Permit instance"); + } catch (UnsupportedClassVersionError e) { + // Skip test if there's a class version mismatch (e.g., running with incompatible JVM) + System.out.println("Skipping test due to class version incompatibility: " + e.getMessage()); + } catch (NoClassDefFoundError e) { + // Skip test if there's a class version mismatch (e.g., running with incompatible JVM) + System.out.println("Skipping test due to class version incompatibility: " + e.getMessage()); + } + } +} diff --git a/src/test/java/io/permit/sdk/examples/BasicPermissionCheckExampleTest.java b/src/test/java/io/permit/sdk/examples/BasicPermissionCheckExampleTest.java new file mode 100644 index 0000000..110063f --- /dev/null +++ b/src/test/java/io/permit/sdk/examples/BasicPermissionCheckExampleTest.java @@ -0,0 +1,143 @@ +package io.permit.sdk.examples; + +import io.permit.sdk.Permit; +import io.permit.sdk.PermitConfig; +import io.permit.sdk.api.PermitApiError; +import io.permit.sdk.enforcement.Resource; +import io.permit.sdk.enforcement.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +/** + * Unit tests for BasicPermissionCheckExample. + * These tests use Mockito to mock the Permit class, testing the actual example class + * rather than a test double. + */ +@ExtendWith(MockitoExtension.class) +class BasicPermissionCheckExampleTest { + + @Mock + private Permit mockPermit; + + private BasicPermissionCheckExample example; + + @BeforeEach + void setUp() { + example = new BasicPermissionCheckExample(mockPermit); + } + + @Test + @DisplayName("Should return true when permission is granted") + void testCheckPermission_Granted() throws IOException, PermitApiError { + // Given + String userKey = "test-user"; + String action = "read"; + String resourceType = "document"; + + when(mockPermit.check(any(User.class), eq(action), any(Resource.class))) + .thenReturn(true); + + // When + boolean result = example.checkPermission(userKey, action, resourceType); + + // Then + assertTrue(result); + verify(mockPermit).check(any(User.class), eq(action), any(Resource.class)); + } + + @Test + @DisplayName("Should return false when permission is denied") + void testCheckPermission_Denied() throws IOException, PermitApiError { + // Given + String userKey = "test-user"; + String action = "delete"; + String resourceType = "document"; + + when(mockPermit.check(any(User.class), eq(action), any(Resource.class))) + .thenReturn(false); + + // When + boolean result = example.checkPermission(userKey, action, resourceType); + + // Then + assertFalse(result); + verify(mockPermit).check(any(User.class), eq(action), any(Resource.class)); + } + + @Test + @DisplayName("Should propagate IOException from Permit SDK") + void testCheckPermission_IOException() throws IOException, PermitApiError { + // Given + String userKey = "test-user"; + String action = "read"; + String resourceType = "document"; + + when(mockPermit.check(any(User.class), eq(action), any(Resource.class))) + .thenThrow(new IOException("Network error")); + + // When/Then + IOException exception = assertThrows(IOException.class, new org.junit.jupiter.api.function.Executable() { + @Override + public void execute() throws Throwable { + example.checkPermission(userKey, action, resourceType); + } + }); + assertEquals("Network error", exception.getMessage()); + } + + @Test + @DisplayName("Should propagate PermitApiError from Permit SDK") + void testCheckPermission_PermitApiError() throws IOException, PermitApiError { + // Given + String userKey = "test-user"; + String action = "read"; + String resourceType = "document"; + + when(mockPermit.check(any(User.class), eq(action), any(Resource.class))) + .thenThrow(new PermitApiError("API Error", 500, "{\"error\":\"Internal Server Error\"}")); + + // When/Then + PermitApiError exception = assertThrows(PermitApiError.class, new org.junit.jupiter.api.function.Executable() { + @Override + public void execute() throws Throwable { + example.checkPermission(userKey, action, resourceType); + } + }); + assertEquals(500, exception.getResponseCode()); + } + + @Test + @DisplayName("Should verify the actual example class can be instantiated with a real Permit instance") + void testBasicPermissionCheckExample_CanBeInstantiated() { + try { + // Given + PermitConfig config = new PermitConfig.Builder("test-api-key") + .withPdpAddress("http://localhost:7766") + .build(); + Permit permit = new Permit(config); + + // When + BasicPermissionCheckExample realExample = new BasicPermissionCheckExample(permit); + + // Then + assertNotNull(realExample, "Should be able to create example with real Permit instance"); + } catch (UnsupportedClassVersionError e) { + // Skip test if there's a class version mismatch (e.g., running with incompatible JVM) + System.out.println("Skipping test due to class version incompatibility: " + e.getMessage()); + } catch (NoClassDefFoundError e) { + // Skip test if there's a class version mismatch (e.g., running with incompatible JVM) + System.out.println("Skipping test due to class version incompatibility: " + e.getMessage()); + } + } +} diff --git a/src/test/java/io/permit/sdk/examples/UserSyncExampleTest.java b/src/test/java/io/permit/sdk/examples/UserSyncExampleTest.java new file mode 100644 index 0000000..3f6e843 --- /dev/null +++ b/src/test/java/io/permit/sdk/examples/UserSyncExampleTest.java @@ -0,0 +1,334 @@ +package io.permit.sdk.examples; + +import io.permit.sdk.Permit; +import io.permit.sdk.PermitConfig; +import io.permit.sdk.api.ApiClient; +import io.permit.sdk.api.PermitApiError; +import io.permit.sdk.api.PermitContextError; +import io.permit.sdk.api.UsersApi; +import io.permit.sdk.api.models.CreateOrUpdateResult; +import io.permit.sdk.enforcement.User; +import io.permit.sdk.openapi.models.UserRead; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * Unit tests for UserSyncExample. + * These tests use Mockito to mock the Permit class and its nested API objects, + * testing the actual UserSyncExample class rather than a test double. + */ +@ExtendWith(MockitoExtension.class) +class UserSyncExampleTest { + + @Mock + private Permit mockPermit; + + @Mock + private ApiClient mockApiClient; + + @Mock + private UsersApi mockUsersApi; + + private UserSyncExample example; + + @BeforeEach + void setUp() throws Exception { + // Set up the mock chain: mockPermit.api -> mockApiClient, mockApiClient.users -> mockUsersApi + // Since 'api' is a public final field, we need to use reflection to set it + Field apiField = Permit.class.getDeclaredField("api"); + apiField.setAccessible(true); + apiField.set(mockPermit, mockApiClient); + + Field usersField = ApiClient.class.getDeclaredField("users"); + usersField.setAccessible(true); + usersField.set(mockApiClient, mockUsersApi); + + example = new UserSyncExample(mockPermit); + } + + @Test + @DisplayName("Should sync a simple user and return created result") + void testSyncSimpleUser_Created() throws IOException, PermitApiError, PermitContextError { + // Given + String userKey = "new-user-001"; + UserRead expectedUser = new UserRead(userKey, "uuid-123", "org-1", "proj-1", "env-1"); + CreateOrUpdateResult expectedResult = new CreateOrUpdateResult(expectedUser, true); + + when(mockUsersApi.sync(any(User.class))).thenReturn(expectedResult); + + // When + CreateOrUpdateResult result = example.syncSimpleUser(userKey); + + // Then + assertNotNull(result); + assertTrue(result.wasCreated(), "User should be marked as created"); + assertEquals(userKey, result.getResult().key); + assertEquals("uuid-123", result.getResult().id); + + ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); + verify(mockUsersApi).sync(userCaptor.capture()); + assertEquals(userKey, userCaptor.getValue().getKey()); + } + + @Test + @DisplayName("Should sync a simple user and return updated result") + void testSyncSimpleUser_Updated() throws IOException, PermitApiError, PermitContextError { + // Given + String userKey = "existing-user-001"; + UserRead expectedUser = new UserRead(userKey, "uuid-456", "org-1", "proj-1", "env-1"); + CreateOrUpdateResult expectedResult = new CreateOrUpdateResult(expectedUser, false); + + when(mockUsersApi.sync(any(User.class))).thenReturn(expectedResult); + + // When + CreateOrUpdateResult result = example.syncSimpleUser(userKey); + + // Then + assertNotNull(result); + assertFalse(result.wasCreated(), "User should be marked as updated (not created)"); + assertEquals(userKey, result.getResult().key); + } + + @Test + @DisplayName("Should sync user with full profile information") + void testSyncUserWithProfile() throws IOException, PermitApiError, PermitContextError { + // Given + String userKey = "john.doe@example.com"; + String email = "john.doe@example.com"; + String firstName = "John"; + String lastName = "Doe"; + + UserRead expectedUser = new UserRead(userKey, "uuid-789", "org-1", "proj-1", "env-1") + .withEmail(email) + .withFirstName(firstName) + .withLastName(lastName); + CreateOrUpdateResult expectedResult = new CreateOrUpdateResult(expectedUser, true); + + when(mockUsersApi.sync(any(User.class))).thenReturn(expectedResult); + + // When + CreateOrUpdateResult result = example.syncUserWithProfile(userKey, email, firstName, lastName); + + // Then + assertNotNull(result); + assertTrue(result.wasCreated()); + assertEquals(email, result.getResult().email); + assertEquals(firstName, result.getResult().firstName); + assertEquals(lastName, result.getResult().lastName); + + // Verify the User object passed to sync had the correct values + ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); + verify(mockUsersApi).sync(userCaptor.capture()); + + User capturedUser = userCaptor.getValue(); + assertEquals(userKey, capturedUser.getKey()); + assertEquals(email, capturedUser.getEmail()); + assertEquals(firstName, capturedUser.getFirstName()); + assertEquals(lastName, capturedUser.getLastName()); + } + + @Test + @DisplayName("Should sync user with ABAC attributes") + void testSyncUserWithAttributes() throws IOException, PermitApiError, PermitContextError { + // Given + String userKey = "alice@example.com"; + String email = "alice@example.com"; + String firstName = "Alice"; + String lastName = "Smith"; + + HashMap attributes = new HashMap(); + attributes.put("department", "engineering"); + attributes.put("clearance_level", 5); + attributes.put("is_manager", true); + + UserRead expectedUser = new UserRead(userKey, "uuid-abc", "org-1", "proj-1", "env-1") + .withEmail(email) + .withFirstName(firstName) + .withLastName(lastName) + .withAttributes(attributes); + CreateOrUpdateResult expectedResult = new CreateOrUpdateResult(expectedUser, true); + + when(mockUsersApi.sync(any(User.class))).thenReturn(expectedResult); + + // When + CreateOrUpdateResult result = example.syncUserWithAttributes( + userKey, email, firstName, lastName, attributes + ); + + // Then + assertNotNull(result); + assertTrue(result.wasCreated()); + + UserRead syncedUser = result.getResult(); + assertEquals("engineering", syncedUser.attributes.get("department")); + assertEquals(5, ((Number) syncedUser.attributes.get("clearance_level")).intValue()); + assertTrue((Boolean) syncedUser.attributes.get("is_manager")); + + // Verify the User object passed to sync had the correct attributes + ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); + verify(mockUsersApi).sync(userCaptor.capture()); + + User capturedUser = userCaptor.getValue(); + assertEquals("engineering", capturedUser.getAttributes().get("department")); + assertEquals(5, capturedUser.getAttributes().get("clearance_level")); + assertTrue((Boolean) capturedUser.getAttributes().get("is_manager")); + } + + @Test + @DisplayName("Should propagate IOException from sync service") + void testSyncSimpleUser_IOException() throws IOException, PermitApiError, PermitContextError { + // Given + when(mockUsersApi.sync(any(User.class))) + .thenThrow(new IOException("Network unreachable")); + + // When/Then + IOException exception = assertThrows(IOException.class, new org.junit.jupiter.api.function.Executable() { + @Override + public void execute() throws Throwable { + example.syncSimpleUser("test-user"); + } + }); + assertEquals("Network unreachable", exception.getMessage()); + } + + @Test + @DisplayName("Should propagate PermitApiError from sync service") + void testSyncSimpleUser_PermitApiError() throws IOException, PermitApiError, PermitContextError { + // Given + when(mockUsersApi.sync(any(User.class))) + .thenThrow(new PermitApiError("Bad Request", 400, "{\"error\":\"Invalid user data\"}")); + + // When/Then + PermitApiError exception = assertThrows(PermitApiError.class, new org.junit.jupiter.api.function.Executable() { + @Override + public void execute() throws Throwable { + example.syncSimpleUser("test-user"); + } + }); + assertEquals(400, exception.getResponseCode()); + assertTrue(exception.getRawResponse().contains("Invalid user data")); + } + + @Test + @DisplayName("Should propagate PermitContextError from sync service") + void testSyncSimpleUser_PermitContextError() throws IOException, PermitApiError, PermitContextError { + // Given + when(mockUsersApi.sync(any(User.class))) + .thenThrow(new PermitContextError("Environment context required")); + + // When/Then + PermitContextError exception = assertThrows(PermitContextError.class, new org.junit.jupiter.api.function.Executable() { + @Override + public void execute() throws Throwable { + example.syncSimpleUser("test-user"); + } + }); + assertTrue(exception.getMessage().contains("Environment context required")); + } + + @Test + @DisplayName("Should handle syncing multiple users") + void testSyncMultipleUsers() throws IOException, PermitApiError, PermitContextError { + // Given + UserRead user1 = new UserRead("user1", "id1", "org", "proj", "env"); + UserRead user2 = new UserRead("user2", "id2", "org", "proj", "env"); + + when(mockUsersApi.sync(any(User.class))) + .thenReturn(new CreateOrUpdateResult(user1, true)) + .thenReturn(new CreateOrUpdateResult(user2, false)); + + // When + CreateOrUpdateResult result1 = example.syncSimpleUser("user1"); + CreateOrUpdateResult result2 = example.syncSimpleUser("user2"); + + // Then + assertTrue(result1.wasCreated(), "First user should be created"); + assertFalse(result2.wasCreated(), "Second user should be updated"); + + verify(mockUsersApi, times(2)).sync(any(User.class)); + } + + @Test + @DisplayName("Should handle null attributes gracefully") + void testSyncUserWithAttributes_NullAttributes() throws IOException, PermitApiError, PermitContextError { + // Given + UserRead expectedUser = new UserRead("user1", "id1", "org", "proj", "env"); + when(mockUsersApi.sync(any(User.class))) + .thenReturn(new CreateOrUpdateResult(expectedUser, true)); + + // When - passing null attributes + CreateOrUpdateResult result = example.syncUserWithAttributes( + "user1", "user1@example.com", "First", "Last", null + ); + + // Then + assertNotNull(result); + assertTrue(result.wasCreated()); + + ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); + verify(mockUsersApi).sync(userCaptor.capture()); + assertNull(userCaptor.getValue().getAttributes()); + } + + @Test + @DisplayName("Should handle empty attributes") + void testSyncUserWithAttributes_EmptyAttributes() throws IOException, PermitApiError, PermitContextError { + // Given + HashMap emptyAttributes = new HashMap(); + UserRead expectedUser = new UserRead("user1", "id1", "org", "proj", "env") + .withAttributes(emptyAttributes); + when(mockUsersApi.sync(any(User.class))) + .thenReturn(new CreateOrUpdateResult(expectedUser, true)); + + // When + CreateOrUpdateResult result = example.syncUserWithAttributes( + "user1", "user1@example.com", "First", "Last", emptyAttributes + ); + + // Then + assertNotNull(result); + assertTrue(result.wasCreated()); + + ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); + verify(mockUsersApi).sync(userCaptor.capture()); + assertNotNull(userCaptor.getValue().getAttributes()); + assertTrue(userCaptor.getValue().getAttributes().isEmpty()); + } + + @Test + @DisplayName("Should verify the actual example class can be instantiated with a real Permit instance") + void testUserSyncExample_CanBeInstantiated() { + try { + // Given + PermitConfig config = new PermitConfig.Builder("test-api-key") + .withPdpAddress("http://localhost:7766") + .build(); + Permit permit = new Permit(config); + + // When + UserSyncExample realExample = new UserSyncExample(permit); + + // Then + assertNotNull(realExample, "Should be able to create example with real Permit instance"); + } catch (UnsupportedClassVersionError e) { + // Skip test if there's a class version mismatch (e.g., running with incompatible JVM) + System.out.println("Skipping test due to class version incompatibility: " + e.getMessage()); + } catch (NoClassDefFoundError e) { + // Skip test if there's a class version mismatch (e.g., running with incompatible JVM) + System.out.println("Skipping test due to class version incompatibility: " + e.getMessage()); + } + } +}