├── simple-auth ├── src │ ├── main │ │ ├── resources │ │ │ ├── application.yml.example │ │ │ ├── META-INF │ │ │ │ └── spring │ │ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ │ ├── data │ │ │ │ ├── openfga-tuple.json │ │ │ │ └── openfga-schema.json │ │ │ ├── example-relationship-tuple.json │ │ │ ├── application.yml │ │ │ └── example-auth-model.json │ │ └── java │ │ │ └── com │ │ │ └── fga │ │ │ └── example │ │ │ ├── service │ │ │ ├── Document.java │ │ │ └── DocumentService.java │ │ │ ├── config │ │ │ ├── OpenFgaConnectionDetails.java │ │ │ ├── SecurityConfig.java │ │ │ ├── OpenFgaClientTupleKeyDeserializer.java │ │ │ ├── OpenFgaAutoConfig.java │ │ │ ├── OpenFgaProperties.java │ │ │ └── InitializeOpenFgaData.java │ │ │ ├── DemoApp.java │ │ │ ├── fga │ │ │ ├── PostReadDocumentCheck.java │ │ │ ├── PreReadDocumentCheck.java │ │ │ ├── FgaCheck.java │ │ │ ├── PreOpenFgaCheck.java │ │ │ ├── PostOpenFgaCheck.java │ │ │ ├── OpenFga.java │ │ │ └── FgaAspect.java │ │ │ └── controller │ │ │ └── DocumentController.java │ └── test │ │ ├── resources │ │ └── META-INF │ │ │ └── spring.factories │ │ └── java │ │ └── com │ │ └── fga │ │ └── example │ │ ├── TestDemoApp.java │ │ ├── WithEvilUser.java │ │ ├── WithHonestUser.java │ │ ├── OpenFgaContainerConfiguration.java │ │ ├── service │ │ └── connection │ │ │ └── OpenFgaContainerConnectionDetailsFactory.java │ │ └── DocumentServiceSecurityTest.java ├── settings.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── build.gradle ├── gradlew.bat ├── README.md └── gradlew ├── .gitignore ├── .github └── workflows │ └── checks.yml ├── example-auth-model.json └── README.md /simple-auth/src/main/resources/application.yml.example: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /simple-auth/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'simple-auth' 2 | 3 | -------------------------------------------------------------------------------- /simple-auth/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports: -------------------------------------------------------------------------------- 1 | com.fga.example.config.OpenFgaAutoConfig -------------------------------------------------------------------------------- /simple-auth/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimmyjames/fga-spring-examples/HEAD/simple-auth/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /simple-auth/src/main/java/com/fga/example/service/Document.java: -------------------------------------------------------------------------------- 1 | package com.fga.example.service; 2 | 3 | public record Document(String id, String content) { 4 | } 5 | -------------------------------------------------------------------------------- /simple-auth/src/main/resources/data/openfga-tuple.json: -------------------------------------------------------------------------------- 1 | { 2 | "writes": [ 3 | { 4 | "user": "user:honest_user", 5 | "relation": "reader", 6 | "object": "document:1" 7 | } 8 | ] 9 | } -------------------------------------------------------------------------------- /simple-auth/src/main/resources/example-relationship-tuple.json: -------------------------------------------------------------------------------- 1 | { 2 | "writes": [ 3 | { 4 | "user": "user:honest_user", 5 | "relation": "reader", 6 | "object": "document:1" 7 | } 8 | ] 9 | } -------------------------------------------------------------------------------- /simple-auth/src/test/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ 2 | com.fga.example.service.connection.OpenFgaContainerConnectionDetailsFactory 3 | -------------------------------------------------------------------------------- /simple-auth/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /simple-auth/src/main/java/com/fga/example/config/OpenFgaConnectionDetails.java: -------------------------------------------------------------------------------- 1 | package com.fga.example.config; 2 | 3 | import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; 4 | 5 | public interface OpenFgaConnectionDetails extends ConnectionDetails { 6 | 7 | String getFgaApiUrl(); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /simple-auth/src/test/java/com/fga/example/TestDemoApp.java: -------------------------------------------------------------------------------- 1 | package com.fga.example; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | 5 | 6 | class TestDemoApp { 7 | 8 | public static void main(String[] args) { 9 | SpringApplication 10 | .from(DemoApp::main) 11 | .with(OpenFgaContainerConfiguration.class) 12 | .run(args); 13 | } 14 | } -------------------------------------------------------------------------------- /simple-auth/src/main/java/com/fga/example/DemoApp.java: -------------------------------------------------------------------------------- 1 | package com.fga.example; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class DemoApp { 8 | public static void main(String[] args) { 9 | SpringApplication.run(DemoApp.class); 10 | } 11 | } -------------------------------------------------------------------------------- /simple-auth/src/test/java/com/fga/example/WithEvilUser.java: -------------------------------------------------------------------------------- 1 | package com.fga.example; 2 | 3 | import org.springframework.security.test.context.support.WithMockUser; 4 | 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | 8 | @WithMockUser("evil_user") 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface WithEvilUser { 11 | } 12 | -------------------------------------------------------------------------------- /simple-auth/src/test/java/com/fga/example/WithHonestUser.java: -------------------------------------------------------------------------------- 1 | package com.fga.example; 2 | 3 | import org.springframework.security.test.context.support.WithMockUser; 4 | 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | 8 | @WithMockUser("honest_user") 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface WithHonestUser { 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Package Files # 4 | *.jar 5 | *.war 6 | *.ear 7 | 8 | # exclude jar for gradle wrapper 9 | !**/gradle/wrapper/*.jar 10 | 11 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 12 | hs_err_pid* 13 | 14 | # build files 15 | **/target 16 | target 17 | .gradle 18 | build 19 | 20 | # JetBrains IDEs 21 | .idea/ 22 | *.iml 23 | 24 | .DS_Store 25 | -------------------------------------------------------------------------------- /simple-auth/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | openfga: 2 | fgaApiUrl: http://localhost:4000 3 | fgaStoreId: 4 | fgaStoreName: test 5 | # fgaAuthModelSchema: classpath:/example-auth-model.json 6 | # fgaInitialRelationshipTuple: classpath:/example-relationship-tuple.json 7 | 8 | logging: 9 | level: 10 | root: WARN 11 | com: 12 | fga: 13 | example: TRACE 14 | org: 15 | springframework: 16 | security: DEBUG 17 | -------------------------------------------------------------------------------- /simple-auth/src/main/java/com/fga/example/fga/PostReadDocumentCheck.java: -------------------------------------------------------------------------------- 1 | package com.fga.example.fga; 2 | 3 | import org.springframework.core.annotation.AliasFor; 4 | 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @PostOpenFgaCheck(userType="'user'", relation="'reader'", objectType="'document'", object="") 10 | public @interface PostReadDocumentCheck { 11 | @AliasFor(attribute = "object", annotation = PostOpenFgaCheck.class) 12 | String value(); 13 | } 14 | -------------------------------------------------------------------------------- /simple-auth/src/main/java/com/fga/example/fga/PreReadDocumentCheck.java: -------------------------------------------------------------------------------- 1 | package com.fga.example.fga; 2 | 3 | import org.springframework.core.annotation.AliasFor; 4 | 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @PreOpenFgaCheck(userType="'user'", relation="'reader'", objectType="'document'", object="") 10 | public @interface PreReadDocumentCheck { 11 | @AliasFor(attribute = "object", annotation = PreOpenFgaCheck.class) 12 | String value(); 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | build: 7 | name: Run Checks 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout the source 11 | uses: actions/checkout@v4 12 | - name: Validate Gradle wrapper 13 | uses: gradle/wrapper-validation-action@v2 14 | - name: Set up JDK 21 15 | uses: actions/setup-java@v4 16 | with: 17 | java-version: '21' 18 | distribution: 'temurin' 19 | - name: Run Gradle check task 20 | run: | 21 | cd simple-auth 22 | ./gradlew check --continue -------------------------------------------------------------------------------- /simple-auth/src/test/java/com/fga/example/OpenFgaContainerConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.fga.example; 2 | 3 | import org.springframework.boot.test.context.TestConfiguration; 4 | import org.springframework.boot.testcontainers.service.connection.ServiceConnection; 5 | import org.springframework.context.annotation.Bean; 6 | import org.testcontainers.openfga.OpenFGAContainer; 7 | 8 | @TestConfiguration(proxyBeanMethods = false) 9 | class OpenFgaContainerConfiguration { 10 | 11 | @Bean 12 | @ServiceConnection 13 | OpenFGAContainer openFgaContainer() { 14 | return new OpenFGAContainer("openfga/openfga:v1.4.3"); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /simple-auth/src/main/java/com/fga/example/fga/FgaCheck.java: -------------------------------------------------------------------------------- 1 | package com.fga.example.fga; 2 | 3 | import org.springframework.core.Ordered; 4 | import org.springframework.core.annotation.Order; 5 | 6 | import java.lang.annotation.ElementType; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import java.lang.annotation.Target; 10 | 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Target(ElementType.METHOD) 13 | @Order(Ordered.HIGHEST_PRECEDENCE) 14 | public @interface FgaCheck { 15 | String object(); 16 | String relation(); 17 | String userType(); 18 | String objectType(); 19 | 20 | String userId() default ""; 21 | } 22 | -------------------------------------------------------------------------------- /simple-auth/src/main/java/com/fga/example/fga/PreOpenFgaCheck.java: -------------------------------------------------------------------------------- 1 | package com.fga.example.fga; 2 | 3 | import org.springframework.core.Ordered; 4 | import org.springframework.core.annotation.Order; 5 | import org.springframework.security.access.prepost.PreAuthorize; 6 | 7 | import java.lang.annotation.ElementType; 8 | import java.lang.annotation.Retention; 9 | import java.lang.annotation.RetentionPolicy; 10 | import java.lang.annotation.Target; 11 | 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) 14 | @Order(Ordered.HIGHEST_PRECEDENCE) 15 | @PreAuthorize("@openFga.check({object}, {objectType}, {relation}, {userType}, {userId})") 16 | public @interface PreOpenFgaCheck { 17 | String object(); 18 | String relation(); 19 | String userType(); 20 | String objectType(); 21 | 22 | String userId() default "authentication.name"; 23 | } 24 | -------------------------------------------------------------------------------- /simple-auth/src/main/java/com/fga/example/fga/PostOpenFgaCheck.java: -------------------------------------------------------------------------------- 1 | package com.fga.example.fga; 2 | 3 | import org.springframework.core.Ordered; 4 | import org.springframework.core.annotation.Order; 5 | import org.springframework.security.access.prepost.PostAuthorize; 6 | import org.springframework.security.access.prepost.PreAuthorize; 7 | 8 | import java.lang.annotation.ElementType; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.RetentionPolicy; 11 | import java.lang.annotation.Target; 12 | 13 | @Retention(RetentionPolicy.RUNTIME) 14 | @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) 15 | @Order(Ordered.HIGHEST_PRECEDENCE) 16 | @PostAuthorize("@openFga.check({object}, {objectType}, {relation}, {userType}, {userId})") 17 | public @interface PostOpenFgaCheck { 18 | String object(); 19 | String relation(); 20 | String userType(); 21 | String objectType(); 22 | 23 | String userId() default "authentication.name"; 24 | } 25 | -------------------------------------------------------------------------------- /simple-auth/src/main/java/com/fga/example/controller/DocumentController.java: -------------------------------------------------------------------------------- 1 | package com.fga.example.controller; 2 | 3 | import com.fga.example.service.Document; 4 | import com.fga.example.service.DocumentService; 5 | import org.springframework.web.bind.annotation.*; 6 | 7 | @RestController 8 | public class DocumentController { 9 | 10 | private final DocumentService documentService; 11 | 12 | public DocumentController(DocumentService documentService) { 13 | this.documentService = documentService; 14 | } 15 | 16 | @GetMapping("/docs/{id}") 17 | public Document simpleBean(@PathVariable String id) { 18 | return documentService.getDocumentWithPreAuthorize(id); 19 | } 20 | 21 | @GetMapping("/docsaop/{id}") 22 | public Document customAnnotation(@PathVariable String id) { 23 | return documentService.getDocumentWithFgaCheck(id); 24 | } 25 | 26 | @PostMapping("/docs") 27 | public String createDoc(@RequestBody String id) { 28 | return documentService.createDoc(id); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /simple-auth/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '3.2.2' 4 | id 'io.spring.dependency-management' version '1.1.4' 5 | } 6 | 7 | group = 'org.example' 8 | version = '1.0-SNAPSHOT' 9 | 10 | ext['spring-security.version'] = '6.3.0-SNAPSHOT' 11 | ext['testcontainers.version'] = '1.19.7' 12 | 13 | repositories { 14 | mavenCentral() 15 | maven { url 'https://repo.spring.io/milestone' } 16 | maven { url 'https://repo.spring.io/snapshot' } 17 | } 18 | 19 | dependencies { 20 | implementation 'org.springframework.boot:spring-boot-starter-web' 21 | implementation 'org.springframework.boot:spring-boot-starter-security' 22 | 23 | // for custom annotation 24 | implementation 'org.springframework.boot:spring-boot-starter-aop' 25 | 26 | // for OpenFgaClient 27 | implementation 'dev.openfga:openfga-sdk:0.3.2' 28 | 29 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 30 | testImplementation 'org.springframework.security:spring-security-test' 31 | testImplementation 'org.springframework.boot:spring-boot-testcontainers' 32 | testImplementation 'org.testcontainers:openfga' 33 | } 34 | 35 | test { 36 | useJUnitPlatform() 37 | } -------------------------------------------------------------------------------- /simple-auth/src/test/java/com/fga/example/service/connection/OpenFgaContainerConnectionDetailsFactory.java: -------------------------------------------------------------------------------- 1 | package com.fga.example.service.connection; 2 | 3 | import com.fga.example.config.OpenFgaConnectionDetails; 4 | import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; 5 | import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; 6 | import org.testcontainers.openfga.OpenFGAContainer; 7 | 8 | class OpenFgaContainerConnectionDetailsFactory 9 | extends ContainerConnectionDetailsFactory { 10 | 11 | @Override 12 | protected OpenFgaConnectionDetails getContainerConnectionDetails( 13 | ContainerConnectionSource source) { 14 | return new OpenFgaContainerConnectionDetails(source); 15 | } 16 | 17 | private static final class OpenFgaContainerConnectionDetails extends ContainerConnectionDetails 18 | implements OpenFgaConnectionDetails { 19 | 20 | private OpenFgaContainerConnectionDetails(ContainerConnectionSource source) { 21 | super(source); 22 | } 23 | 24 | @Override 25 | public String getFgaApiUrl() { 26 | return getContainer().getHttpEndpoint(); 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /simple-auth/src/main/java/com/fga/example/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.fga.example.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.authorization.method.PrePostTemplateDefaults; 6 | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 7 | import org.springframework.security.core.userdetails.User; 8 | import org.springframework.security.core.userdetails.UserDetails; 9 | import org.springframework.security.core.userdetails.UserDetailsService; 10 | import org.springframework.security.provisioning.InMemoryUserDetailsManager; 11 | 12 | @Configuration 13 | @EnableMethodSecurity 14 | public class SecurityConfig { 15 | 16 | @Bean 17 | static PrePostTemplateDefaults prePostTemplateDefaults() { 18 | return new PrePostTemplateDefaults(); 19 | } 20 | 21 | @Bean 22 | public UserDetailsService users() { 23 | User.UserBuilder users = User.withDefaultPasswordEncoder() 24 | .password("password") 25 | .roles("USER"); 26 | UserDetails user = users 27 | .username("honest_user") 28 | .build(); 29 | UserDetails admin = users 30 | .username("evil_user") 31 | .build(); 32 | return new InMemoryUserDetailsManager(user, admin); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /simple-auth/src/main/java/com/fga/example/config/OpenFgaClientTupleKeyDeserializer.java: -------------------------------------------------------------------------------- 1 | package com.fga.example.config; 2 | 3 | import com.fasterxml.jackson.core.JsonParser; 4 | import com.fasterxml.jackson.databind.DeserializationContext; 5 | import com.fasterxml.jackson.databind.JsonDeserializer; 6 | import com.fasterxml.jackson.databind.JsonNode; 7 | import dev.openfga.sdk.api.client.model.ClientRelationshipCondition; 8 | import dev.openfga.sdk.api.client.model.ClientTupleKey; 9 | 10 | import java.io.IOException; 11 | 12 | public class OpenFgaClientTupleKeyDeserializer extends JsonDeserializer { 13 | @Override 14 | public ClientTupleKey deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { 15 | JsonNode node = p.getCodec().readTree(p); 16 | ClientTupleKey key = new ClientTupleKey(); 17 | if (node.has("user")) key.user(node.get("user").asText()); 18 | if (node.has("relation")) key.relation(node.get("relation").asText()); 19 | if (node.has("condition")) key.condition(makeCondition(node.get("condition"))); 20 | if (node.has("_object")) { 21 | key._object(node.get("_object").asText()); 22 | } else if (node.has("object")) { 23 | key._object(node.get("object").asText()); 24 | } 25 | return key; 26 | } 27 | 28 | private ClientRelationshipCondition makeCondition(JsonNode node) { 29 | ClientRelationshipCondition condition = new ClientRelationshipCondition(); 30 | if (node.has("name")) condition.name(node.get("name").asText()); 31 | if (node.has("context")) condition.context(node.get("context")); 32 | return condition; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /example-auth-model.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema_version": "1.1", 3 | "type_definitions": [ 4 | { 5 | "type": "user" 6 | }, 7 | { 8 | "type": "document", 9 | "relations": { 10 | "reader": { 11 | "this": {} 12 | }, 13 | "writer": { 14 | "this": {} 15 | }, 16 | "owner": { 17 | "this": {} 18 | } 19 | }, 20 | "metadata": { 21 | "relations": { 22 | "reader": { 23 | "directly_related_user_types": [ 24 | { 25 | "type": "user" 26 | } 27 | ] 28 | }, 29 | "writer": { 30 | "directly_related_user_types": [ 31 | { 32 | "type": "user" 33 | } 34 | ] 35 | }, 36 | "owner": { 37 | "directly_related_user_types": [ 38 | { 39 | "type": "user" 40 | } 41 | ] 42 | }, 43 | "conditional_reader": { 44 | "directly_related_user_types": [ 45 | { 46 | "condition": "name_starts_with_a", 47 | "type": "user" 48 | } 49 | ] 50 | } 51 | } 52 | } 53 | } 54 | ], 55 | "conditions": { 56 | "ViewCountLessThan200": { 57 | "name": "ViewCountLessThan200", 58 | "expression": "ViewCount < 200", 59 | "parameters": { 60 | "ViewCount": { 61 | "type_name": "TYPE_NAME_INT" 62 | }, 63 | "Type": { 64 | "type_name": "TYPE_NAME_STRING" 65 | }, 66 | "Name": { 67 | "type_name": "TYPE_NAME_STRING" 68 | } 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /simple-auth/src/main/resources/data/openfga-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema_version": "1.1", 3 | "type_definitions": [ 4 | { 5 | "type": "user" 6 | }, 7 | { 8 | "type": "document", 9 | "relations": { 10 | "reader": { 11 | "this": {} 12 | }, 13 | "writer": { 14 | "this": {} 15 | }, 16 | "owner": { 17 | "this": {} 18 | } 19 | }, 20 | "metadata": { 21 | "relations": { 22 | "reader": { 23 | "directly_related_user_types": [ 24 | { 25 | "type": "user" 26 | } 27 | ] 28 | }, 29 | "writer": { 30 | "directly_related_user_types": [ 31 | { 32 | "type": "user" 33 | } 34 | ] 35 | }, 36 | "owner": { 37 | "directly_related_user_types": [ 38 | { 39 | "type": "user" 40 | } 41 | ] 42 | }, 43 | "conditional_reader": { 44 | "directly_related_user_types": [ 45 | { 46 | "condition": "name_starts_with_a", 47 | "type": "user" 48 | } 49 | ] 50 | } 51 | } 52 | } 53 | } 54 | ], 55 | "conditions": { 56 | "ViewCountLessThan200": { 57 | "name": "ViewCountLessThan200", 58 | "expression": "ViewCount < 200", 59 | "parameters": { 60 | "ViewCount": { 61 | "type_name": "TYPE_NAME_INT" 62 | }, 63 | "Type": { 64 | "type_name": "TYPE_NAME_STRING" 65 | }, 66 | "Name": { 67 | "type_name": "TYPE_NAME_STRING" 68 | } 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /simple-auth/src/main/resources/example-auth-model.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema_version": "1.1", 3 | "type_definitions": [ 4 | { 5 | "type": "user" 6 | }, 7 | { 8 | "type": "document", 9 | "relations": { 10 | "reader": { 11 | "this": {} 12 | }, 13 | "writer": { 14 | "this": {} 15 | }, 16 | "owner": { 17 | "this": {} 18 | } 19 | }, 20 | "metadata": { 21 | "relations": { 22 | "reader": { 23 | "directly_related_user_types": [ 24 | { 25 | "type": "user" 26 | } 27 | ] 28 | }, 29 | "writer": { 30 | "directly_related_user_types": [ 31 | { 32 | "type": "user" 33 | } 34 | ] 35 | }, 36 | "owner": { 37 | "directly_related_user_types": [ 38 | { 39 | "type": "user" 40 | } 41 | ] 42 | }, 43 | "conditional_reader": { 44 | "directly_related_user_types": [ 45 | { 46 | "condition": "name_starts_with_a", 47 | "type": "user" 48 | } 49 | ] 50 | } 51 | } 52 | } 53 | } 54 | ], 55 | "conditions": { 56 | "ViewCountLessThan200": { 57 | "name": "ViewCountLessThan200", 58 | "expression": "ViewCount < 200", 59 | "parameters": { 60 | "ViewCount": { 61 | "type_name": "TYPE_NAME_INT" 62 | }, 63 | "Type": { 64 | "type_name": "TYPE_NAME_STRING" 65 | }, 66 | "Name": { 67 | "type_name": "TYPE_NAME_STRING" 68 | } 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /simple-auth/src/main/java/com/fga/example/fga/OpenFga.java: -------------------------------------------------------------------------------- 1 | package com.fga.example.fga; 2 | 3 | import dev.openfga.sdk.api.client.OpenFgaClient; 4 | import dev.openfga.sdk.api.client.model.ClientCheckRequest; 5 | import dev.openfga.sdk.api.client.model.ClientCheckResponse; 6 | import dev.openfga.sdk.errors.FgaInvalidParameterException; 7 | import org.springframework.security.core.Authentication; 8 | import org.springframework.security.core.context.SecurityContextHolder; 9 | import org.springframework.stereotype.Component; 10 | 11 | import java.util.concurrent.ExecutionException; 12 | 13 | /** 14 | * Simple bean that can be used to perform an FGA check. 15 | */ 16 | @Component 17 | public class OpenFga { 18 | 19 | private final OpenFgaClient fgaClient; 20 | 21 | // Inject OpenFga client 22 | public OpenFga(OpenFgaClient fgaClient) { 23 | this.fgaClient = fgaClient; 24 | } 25 | 26 | /** 27 | * Perform an FGA check. The user ID will be obtained from the authentication name in the {@link org.springframework.security.core.context.SecurityContext} 28 | * 29 | * @param objectId 30 | * @param objectType 31 | * @param relation 32 | * @param userType 33 | * @return true if the user has the required relation to the object, false otherwise 34 | */ 35 | public boolean check(String objectId, String objectType, String relation, String userType) { 36 | Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 37 | if (authentication == null) { 38 | throw new IllegalStateException("No user provided, and no authentication could be found in the security context"); 39 | } 40 | return check(objectId, objectType, relation, userType, authentication.getName()); 41 | } 42 | 43 | /** 44 | * Perform an FGA check. 45 | * @param objectId 46 | * @param objectType 47 | * @param relation 48 | * @param userType 49 | * @param userId 50 | * @return true if the user has the required relation to the object, false otherwise 51 | */ 52 | public boolean check(String objectId, String objectType, String relation, String userType, String userId) { 53 | var body = new ClientCheckRequest() 54 | .user(String.format("%s:%s", userType, userId)) 55 | .relation(relation) 56 | ._object(String.format("%s:%s", objectType, objectId)); 57 | 58 | ClientCheckResponse response = null; 59 | try { 60 | response = fgaClient.check(body).get(); 61 | } catch (InterruptedException | FgaInvalidParameterException | ExecutionException e) { 62 | throw new RuntimeException("Error performing FGA check", e); 63 | } 64 | 65 | return response.getAllowed(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /simple-auth/src/main/java/com/fga/example/config/OpenFgaAutoConfig.java: -------------------------------------------------------------------------------- 1 | package com.fga.example.config; 2 | 3 | import dev.openfga.sdk.api.client.OpenFgaClient; 4 | import dev.openfga.sdk.api.configuration.ClientConfiguration; 5 | import dev.openfga.sdk.errors.FgaInvalidParameterException; 6 | import org.springframework.boot.autoconfigure.AutoConfiguration; 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 9 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 10 | import org.springframework.context.annotation.Bean; 11 | 12 | /** 13 | * Creates an {@link OpenFgaClient} configured from application properties 14 | */ 15 | @AutoConfiguration 16 | @EnableConfigurationProperties(OpenFgaProperties.class) 17 | public class OpenFgaAutoConfig { 18 | 19 | private final OpenFgaProperties openFgaProperties; 20 | 21 | public OpenFgaAutoConfig(OpenFgaProperties openFgaProperties) { 22 | this.openFgaProperties = openFgaProperties; 23 | } 24 | 25 | @Bean 26 | @ConditionalOnMissingBean(OpenFgaConnectionDetails.class) 27 | public PropertiesOpenFgaConnectionDetails openFgaConnectionDetails() { 28 | return new PropertiesOpenFgaConnectionDetails(this.openFgaProperties); 29 | } 30 | 31 | @Bean 32 | @ConditionalOnMissingBean 33 | public ClientConfiguration openFgaConfig(OpenFgaConnectionDetails connectionDetails) { 34 | 35 | // For simplicity, this creates a client with NO AUTHENTICATION. It is NOT SUITABLE FOR PRODUCTION USE!! 36 | return new ClientConfiguration() 37 | .apiUrl(connectionDetails.getFgaApiUrl()) 38 | .storeId(openFgaProperties.getFgaStoreId()) 39 | .authorizationModelId(openFgaProperties.getFgaAuthorizationModelId()); 40 | } 41 | 42 | @Bean 43 | @ConditionalOnMissingBean 44 | public OpenFgaClient openFgaClient(ClientConfiguration configuration) { 45 | try { 46 | return new OpenFgaClient(configuration); 47 | } catch (FgaInvalidParameterException e) { 48 | // TODO how to best handle 49 | throw new RuntimeException(e); 50 | } 51 | } 52 | 53 | @Bean 54 | @ConditionalOnBean({OpenFgaClient.class}) 55 | public InitializeOpenFgaData initializeOpenFgaData(OpenFgaClient openFgaClient, OpenFgaProperties properties) { 56 | return new InitializeOpenFgaData(properties, openFgaClient); 57 | } 58 | 59 | private static class PropertiesOpenFgaConnectionDetails implements OpenFgaConnectionDetails { 60 | 61 | private final OpenFgaProperties openFgaProperties; 62 | 63 | public PropertiesOpenFgaConnectionDetails(OpenFgaProperties openFgaProperties) { 64 | this.openFgaProperties = openFgaProperties; 65 | } 66 | 67 | @Override 68 | public String getFgaApiUrl() { 69 | return this.openFgaProperties.getFgaApiUrl(); 70 | } 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /simple-auth/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /simple-auth/README.md: -------------------------------------------------------------------------------- 1 | # Spring Security Examples with OpenFGA 2 | 3 | Simple repository demonstrating use cases and possible solutions to integrate FGA with Spring Security. 4 | 5 | > NOTE: For simplicity, this sample **DOES NOT USE ANY AUTHENTICATION**, either for the Fga client or for Spring. This is NOT RECOMMENDED FOR PRODUCTION USE! 6 | 7 | ## About 8 | 9 | This is a WIP repo demonstrating various possible ways to use FGA in a Spring application. 10 | 11 | It demonstrates: 12 | - Using a simple properties configuration to configure and build an `OpenFgaClient` that can be injected into any Spring component. This can be used to perform any FGA API calls, but primary use case is likely to write authorization data and is shown here. 13 | - Using a simple Bean with `@PreAuthorize` to perform a method-level FGA check 14 | - Using a custom annotation (`@FgaCheck`) with an `@Aspect` implementation to perform a method-level FGA check 15 | 16 | ## Goals 17 | 18 | The goals of this repository are to: 19 | - Show how OpenFGA can be integrated with Spring today 20 | - Give insight into possible DX improvements, either through an FGA-owned starter/library, possible direct Spring Security integration, or customer guidance 21 | 22 | ## Usage 23 | 24 | ### Start the app 25 | 26 | This starts the application along with the openfga docker image. 27 | 28 | See the [OpenFGA docs](https://openfga.dev/docs/getting-started/setup-openfga/docker#step-by-step) for more information. 29 | 30 | ```bash 31 | ./gradlew bootTestRun 32 | ``` 33 | 34 | On startup, the application will create an in-memory store with a simple authorization model, and write a tuple representing the following relation: 35 | 36 | ``` 37 | user: user:123 38 | relation: reader 39 | object: document:1 40 | ``` 41 | 42 | The examples hard-code a userId of `user:123`. We should consider if we can provide a default of the currently authenticated principal when authentication is added. 43 | 44 | ### Successful API call using simple FGA bean 45 | 46 | Execute a GET request to obtain `document:1`: 47 | 48 | `curl -X GET http://localhost:8080/docs/1` 49 | 50 | You should see a simple success message in the console. 51 | 52 | ### Unauthorized API call using simple FGA bean 53 | 54 | Execute a GET request to obtain `document:2`, for which `user:2` does not have a `reader` relation: 55 | 56 | `curl -X http://localhost:8080/docs/2 -v` 57 | 58 | You should receive a 403 response as `user:123` does not have the `reader` relation to `document:2` 59 | 60 | ### Successful API call using FGA annotation/aop 61 | 62 | Execute a GET request to obtain `document:1`: 63 | 64 | `curl -X GET http://localhost:8080/docsaop/1` 65 | 66 | You should see a simple success message in the console. 67 | 68 | ### Unauthorized API call using FGA annotation/aop 69 | 70 | Execute a GET request to obtain `document:2`, for which `user:2` does not have a `reader` relation: 71 | 72 | `curl -X http://localhost:8080/docsaop/2 -v` 73 | 74 | You should receive a 403 response as `user:123` does not have the `reader` relation to `document:2` 75 | 76 | ### Execute a POST request to write to FGA 77 | 78 | Execute a POST request to create a relationship between `user:123` and `document:2`: 79 | 80 | `curl -X POST -H "Content-Type: text/plain" -d "2" http://localhost:8080/docs` 81 | 82 | You should see a message that document with ID `2` was created 83 | 84 | You can now execute either of the GET requests to verify that `user:123` now has access to `document:2` 85 | -------------------------------------------------------------------------------- /simple-auth/src/main/java/com/fga/example/config/OpenFgaProperties.java: -------------------------------------------------------------------------------- 1 | package com.fga.example.config; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | 5 | import java.util.List; 6 | 7 | @ConfigurationProperties(prefix="openfga") 8 | public class OpenFgaProperties { 9 | 10 | private String fgaApiUrl; 11 | private String fgaStoreId; 12 | private String fgaStoreName; 13 | private String fgaApiTokenIssuer; 14 | private String fgaApiAudience; 15 | private String fgaClientId; 16 | private String fgaClientSecret; 17 | private String fgaAuthorizationModelId; 18 | private String fgaAuthModelSchema; 19 | private List fgaInitialRelationshipTuple; 20 | 21 | private static final String FGA_MODEL_SCHEMA_DEFAULT = "classpath:data/openfga-schema.json"; 22 | private static final List FGA_RELATIONSHIP_TUPLE_DEFAULT = List.of("classpath:data/openfga-tuple.json"); 23 | 24 | public String getFgaStoreName() { 25 | return fgaStoreName; 26 | } 27 | 28 | public void setFgaStoreName(String fgaStoreName) { 29 | this.fgaStoreName = fgaStoreName; 30 | } 31 | 32 | public String getFgaAuthModelSchema() { 33 | return fgaAuthModelSchema == null ? FGA_MODEL_SCHEMA_DEFAULT : fgaAuthModelSchema; 34 | } 35 | 36 | public void setFgaAuthModelSchema(String fgaAuthModelSchema) { 37 | this.fgaAuthModelSchema = fgaAuthModelSchema; 38 | } 39 | 40 | public String getFgaApiUrl() { 41 | return fgaApiUrl; 42 | } 43 | 44 | public void setFgaApiUrl(String fgaApiUrl) { 45 | this.fgaApiUrl = fgaApiUrl; 46 | } 47 | 48 | public String getFgaStoreId() { 49 | return fgaStoreId; 50 | } 51 | 52 | public void setFgaStoreId(String fgaStoreId) { 53 | this.fgaStoreId = fgaStoreId; 54 | } 55 | 56 | public String getFgaApiTokenIssuer() { 57 | return fgaApiTokenIssuer; 58 | } 59 | 60 | public void setFgaApiTokenIssuer(String fgaApiTokenIssuer) { 61 | this.fgaApiTokenIssuer = fgaApiTokenIssuer; 62 | } 63 | 64 | public String getFgaApiAudience() { 65 | return fgaApiAudience; 66 | } 67 | 68 | public void setFgaApiAudience(String fgaApiAudience) { 69 | this.fgaApiAudience = fgaApiAudience; 70 | } 71 | 72 | public String getFgaClientId() { 73 | return fgaClientId; 74 | } 75 | 76 | public void setFgaClientId(String fgaClientId) { 77 | this.fgaClientId = fgaClientId; 78 | } 79 | 80 | public String getFgaClientSecret() { 81 | return fgaClientSecret; 82 | } 83 | 84 | public void setFgaClientSecret(String fgaClientSecret) { 85 | this.fgaClientSecret = fgaClientSecret; 86 | } 87 | 88 | public String getFgaAuthorizationModelId() { 89 | return fgaAuthorizationModelId; 90 | } 91 | 92 | public void setFgaAuthorizationModelId(String fgaAuthorizationModelId) { 93 | this.fgaAuthorizationModelId = fgaAuthorizationModelId; 94 | } 95 | 96 | public List getFgaInitialRelationshipTuple() { 97 | return fgaInitialRelationshipTuple == null ? FGA_RELATIONSHIP_TUPLE_DEFAULT : fgaInitialRelationshipTuple; 98 | } 99 | 100 | public void setFgaInitialRelationshipTuple(List fgaInitialRelationshipTuple) { 101 | this.fgaInitialRelationshipTuple = fgaInitialRelationshipTuple; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /simple-auth/src/main/java/com/fga/example/fga/FgaAspect.java: -------------------------------------------------------------------------------- 1 | package com.fga.example.fga; 2 | 3 | import dev.openfga.sdk.api.client.OpenFgaClient; 4 | import dev.openfga.sdk.api.client.model.ClientCheckRequest; 5 | import dev.openfga.sdk.api.client.model.ClientCheckResponse; 6 | import dev.openfga.sdk.errors.FgaInvalidParameterException; 7 | import org.aspectj.lang.JoinPoint; 8 | import org.aspectj.lang.annotation.Aspect; 9 | import org.aspectj.lang.annotation.Before; 10 | import org.aspectj.lang.reflect.MethodSignature; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.expression.ExpressionParser; 14 | import org.springframework.expression.spel.standard.SpelExpressionParser; 15 | import org.springframework.expression.spel.support.StandardEvaluationContext; 16 | import org.springframework.security.access.AccessDeniedException; 17 | import org.springframework.stereotype.Component; 18 | import org.springframework.util.ObjectUtils; 19 | 20 | import java.lang.reflect.Method; 21 | import java.util.concurrent.ExecutionException; 22 | 23 | @Aspect 24 | @Component 25 | public class FgaAspect { 26 | 27 | private final Logger logger = LoggerFactory.getLogger(FgaAspect.class); 28 | 29 | private final OpenFgaClient fgaClient; 30 | 31 | FgaAspect(OpenFgaClient fgaClient) { 32 | this.fgaClient = fgaClient; 33 | } 34 | 35 | @Before("@annotation(fga)") 36 | public void check(final JoinPoint jointPoint, final FgaCheck fga) { 37 | logger.debug("**** CUSTOM AOP CALLED *****"); 38 | 39 | MethodSignature signature = (MethodSignature) jointPoint.getSignature(); 40 | Method method = signature.getMethod(); 41 | FgaCheck fgaCheckAnnotation = method.getAnnotation(FgaCheck.class); 42 | 43 | ExpressionParser parser = new SpelExpressionParser(); 44 | StandardEvaluationContext context = new 45 | StandardEvaluationContext(); 46 | 47 | for (int i = 0; i < signature.getParameterNames().length; i++) { 48 | context.setVariable(signature.getParameterNames()[i], jointPoint.getArgs()[i]); 49 | } 50 | 51 | String obj = parser.parseExpression(fgaCheckAnnotation.object()).getValue(context, String.class); 52 | String relation = fgaCheckAnnotation.relation(); 53 | String objType = fgaCheckAnnotation.objectType(); 54 | String userType = fgaCheckAnnotation.userType(); 55 | String userId = null; 56 | 57 | if (ObjectUtils.isEmpty(fgaCheckAnnotation.userId())) { 58 | // TODO get authentication principal from context and use as default userId? 59 | userId = "123"; 60 | } else { 61 | userId = fgaCheckAnnotation.userId(); 62 | } 63 | 64 | if (!fgaCheck(String.format("%s:%s", userType, userId), relation, String.format("%s:%s", objType, obj))) { 65 | throw new AccessDeniedException("Access Denied"); 66 | } 67 | } 68 | 69 | private boolean fgaCheck(String user, String relation, String _object) { 70 | 71 | var body = new ClientCheckRequest() 72 | .user(user) 73 | .relation(relation) 74 | ._object(_object); 75 | 76 | ClientCheckResponse response = null; 77 | try { 78 | response = fgaClient.check(body).get(); 79 | } catch (InterruptedException | FgaInvalidParameterException | ExecutionException e) { 80 | throw new RuntimeException("Error performing FGA check", e); 81 | } 82 | 83 | return response.getAllowed(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /simple-auth/src/main/java/com/fga/example/service/DocumentService.java: -------------------------------------------------------------------------------- 1 | package com.fga.example.service; 2 | 3 | import com.fga.example.fga.*; 4 | import dev.openfga.sdk.api.client.OpenFgaClient; 5 | import dev.openfga.sdk.api.client.model.ClientTupleKey; 6 | import dev.openfga.sdk.api.client.model.ClientWriteRequest; 7 | import dev.openfga.sdk.errors.FgaInvalidParameterException; 8 | import org.springframework.security.access.prepost.PostAuthorize; 9 | import org.springframework.security.access.prepost.PreAuthorize; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.util.List; 13 | import java.util.concurrent.ExecutionException; 14 | 15 | @Service 16 | public class DocumentService { 17 | 18 | private final OpenFgaClient fgaClient; 19 | 20 | public DocumentService(OpenFgaClient fgaClient) { 21 | this.fgaClient = fgaClient; 22 | } 23 | 24 | /** 25 | * Simple Fga bean available for use in preauthorize. 26 | * - multiple params of same type with optional params (e.g., userId) could be unwieldy 27 | */ 28 | @PreAuthorize("@openFga.check(#id, 'document', 'reader', 'user', authentication?.name)") 29 | public Document getDocumentWithPreAuthorize(String id) { 30 | return new Document(id, "You have reader access to this document"); 31 | } 32 | 33 | /** 34 | * Uses the new PreAuthorize meta-annotation support 35 | */ 36 | @PreOpenFgaCheck(userType="'user'", relation="'reader'", objectType="'document'", object="#id") 37 | public Document getDocumentWithPreOpenFgaCheck(String id) { 38 | return new Document(id, "You have reader access to this document"); 39 | } 40 | 41 | /** 42 | * Uses the new PreAuthorize meta-annotation support 43 | */ 44 | @PreReadDocumentCheck("#id") 45 | public Document getDocumentWithPreReadDocumentCheck(String id) { 46 | return new Document(id, "You have reader access to this document"); 47 | } 48 | 49 | @PostOpenFgaCheck(userType="'user'", relation="'reader'", objectType="'document'", object="returnObject.id") 50 | public Document findByContentWithPostOpenFgaCheck(String content) { 51 | return new Document("1", "Found the content here: '" + content + "'"); 52 | } 53 | 54 | @PostReadDocumentCheck("returnObject.id") 55 | public Document findByContentWithPostReadDocumentCheck(String content) { 56 | return new Document("1", "Found the content here: '" + content + "'"); 57 | } 58 | 59 | /** 60 | * Fga custom annotation with a {@code @Before} pointcut available for use in preauthorize. TODOs: 61 | * - Can we make some inferences and defaults for the current principal so we don't have to pass the user ID? 62 | * - Do we need more flexibility in the pointcut? 63 | * - would https://github.com/spring-projects/spring-security/issues/14480 make this implementation easier, and would it support SpEL for fields like object and userId 64 | */ 65 | @FgaCheck(userType="user", userId="honest_user", relation="reader", objectType="document", object="#id") 66 | public Document getDocumentWithFgaCheck(String id) { 67 | return new Document(id, "You have reader access to this document!"); 68 | } 69 | 70 | /** 71 | * Demonstrates a simple example of using the injected fgaClient to write authorization data to FGA. 72 | */ 73 | public String createDoc(String id) { 74 | ClientWriteRequest writeRequest = new ClientWriteRequest() 75 | .writes(List.of(new ClientTupleKey() 76 | .user("user:123") 77 | .relation("reader") 78 | ._object(String.format("document:%s", id)))); 79 | 80 | try { 81 | fgaClient.write(writeRequest).get(); 82 | } catch (InterruptedException | ExecutionException | FgaInvalidParameterException e) { 83 | throw new RuntimeException("Error writing to FGA", e); 84 | } 85 | 86 | return String.format("Created doc with ID %s and associated user:123 as a reader FGA relationship", id); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /simple-auth/src/test/java/com/fga/example/DocumentServiceSecurityTest.java: -------------------------------------------------------------------------------- 1 | package com.fga.example; 2 | 3 | import com.fga.example.service.DocumentService; 4 | import org.assertj.core.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.context.annotation.Import; 9 | import org.springframework.security.access.AccessDeniedException; 10 | 11 | 12 | import static org.assertj.core.api.Assertions.assertThatCode; 13 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 14 | 15 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) 16 | @Import(OpenFgaContainerConfiguration.class) 17 | class DocumentServiceSecurityTest { 18 | 19 | public static final String DOCUMENT_GRANTED_ID = "1"; 20 | 21 | public static final String DOCUMENT_DENIED_ID = "2"; 22 | 23 | @Test 24 | @WithHonestUser 25 | void preAuthorizeWhenGranted(@Autowired DocumentService documentService) { 26 | assertThatCode(() -> documentService.getDocumentWithPreAuthorize(DOCUMENT_GRANTED_ID)) 27 | .doesNotThrowAnyException(); 28 | } 29 | 30 | @Test 31 | @WithHonestUser 32 | void preAuthorizeWhenNoDocumentThenDenied(@Autowired DocumentService documentService) { 33 | assertThatExceptionOfType(AccessDeniedException.class) 34 | .isThrownBy(() -> documentService.getDocumentWithPreAuthorize(DOCUMENT_DENIED_ID)); 35 | } 36 | 37 | @Test 38 | @WithEvilUser 39 | void preAuthorizeWhenWrongUserThenDenied(@Autowired DocumentService documentService) { 40 | assertThatExceptionOfType(AccessDeniedException.class) 41 | .isThrownBy(() -> documentService.getDocumentWithPreAuthorize(DOCUMENT_DENIED_ID)); 42 | } 43 | 44 | @Test 45 | @WithHonestUser 46 | void preOpenFgaCheckWhenGranted(@Autowired DocumentService documentService) { 47 | assertThatCode(() -> documentService.getDocumentWithPreOpenFgaCheck(DOCUMENT_GRANTED_ID)) 48 | .doesNotThrowAnyException(); 49 | } 50 | 51 | @Test 52 | @WithHonestUser 53 | void preOpenFgaCheckWhenNoDocumentThenDenied(@Autowired DocumentService documentService) { 54 | assertThatExceptionOfType(AccessDeniedException.class) 55 | .isThrownBy(() -> documentService.getDocumentWithPreOpenFgaCheck(DOCUMENT_DENIED_ID)); 56 | } 57 | 58 | @Test 59 | @WithHonestUser 60 | void postOpenFgaCheckWhenGranted(@Autowired DocumentService documentService) { 61 | assertThatCode(() -> documentService.findByContentWithPostOpenFgaCheck("Hello Spring Security!")) 62 | .doesNotThrowAnyException(); 63 | } 64 | 65 | @Test 66 | @WithEvilUser 67 | void postOpenFgaCheckWhenNoDocumentThenDenied(@Autowired DocumentService documentService) { 68 | assertThatExceptionOfType(AccessDeniedException.class) 69 | .isThrownBy(() -> documentService.findByContentWithPostOpenFgaCheck("Hello Spring Security!")); 70 | } 71 | 72 | @Test 73 | @WithHonestUser 74 | void postReadDocumentCheckWhenGranted(@Autowired DocumentService documentService) { 75 | assertThatCode(() -> documentService.findByContentWithPostReadDocumentCheck("Hello Spring Security!")) 76 | .doesNotThrowAnyException(); 77 | } 78 | 79 | @Test 80 | @WithEvilUser 81 | void postReadDocumentCheckWhenNoDocumentThenDenied(@Autowired DocumentService documentService) { 82 | assertThatExceptionOfType(AccessDeniedException.class) 83 | .isThrownBy(() -> documentService.findByContentWithPostReadDocumentCheck("Hello Spring Security!")); 84 | } 85 | 86 | @Test 87 | @WithEvilUser 88 | void preOpenFgaCheckWhenWrongUserDenied(@Autowired DocumentService documentService) { 89 | assertThatExceptionOfType(AccessDeniedException.class) 90 | .isThrownBy(() -> documentService.getDocumentWithPreOpenFgaCheck(DOCUMENT_DENIED_ID)); 91 | } 92 | 93 | @Test 94 | @WithHonestUser 95 | void preReadDocumentCheckWhenGranted(@Autowired DocumentService documentService) { 96 | assertThatCode(() -> documentService.getDocumentWithPreReadDocumentCheck(DOCUMENT_GRANTED_ID)) 97 | .doesNotThrowAnyException(); 98 | } 99 | 100 | @Test 101 | @WithEvilUser 102 | void preReadDocumentCheckWhenDenied(@Autowired DocumentService documentService) { 103 | Assertions.setMaxStackTraceElementsDisplayed(Integer.MAX_VALUE); 104 | assertThatExceptionOfType(AccessDeniedException.class) 105 | .isThrownBy(() -> documentService.getDocumentWithPreReadDocumentCheck(DOCUMENT_DENIED_ID)); 106 | } 107 | 108 | @Test 109 | void fgaCheckWhenGranted(@Autowired DocumentService documentService) { 110 | assertThatCode(() -> documentService.getDocumentWithFgaCheck(DOCUMENT_GRANTED_ID)) 111 | .doesNotThrowAnyException(); 112 | } 113 | 114 | @Test 115 | void fgaCheckWhenDenied(@Autowired DocumentService documentService) { 116 | assertThatExceptionOfType(AccessDeniedException.class) 117 | .isThrownBy(() -> documentService.getDocumentWithFgaCheck(DOCUMENT_DENIED_ID)); 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Security Examples with OpenFGA 2 | 3 | Repository demonstrating use cases and possible solutions to integrate FGA with Spring Security. 4 | 5 | ## Goals 6 | 7 | The goals of this repository are to: 8 | - Show how OpenFGA can be integrated with Spring today 9 | - Give insight into possible DX improvements, either through an FGA-owned starter/library, possible direct Spring Security integration, or customer guidance 10 | 11 | ## Samples 12 | 13 | - `simple-auth` is a sample FGA integration that has a basic Spring security configured. It is a simple example that makes assumptions about users and principals. 14 | - `resource-server` and `client-restclient` demonstrate a resource server with JWT authorization using the `okta-spring-boot-starter` and a client credentials flow to obtain a JWT to make API calls. The API's in `resource-server` are protected both by JWT and FGA checks, and are called by `client-restclient`. 15 | 16 | ## Prerequisites 17 | 18 | - Docker 19 | - Java 17 20 | - [OpenFGA CLI](https://github.com/openfga/cli) 21 | 22 | ## Usage 23 | 24 | ### Simple no-auth sample 25 | 26 | To run the `simple-auth` sample, see the [README](./simple-auth/README.md). 27 | 28 | ### Client credentials sample 29 | 30 | This sample comprises of two parts: 31 | - A resource server configured with the `okta-spring-boot-starter` to secure endpoints with JWTs issued by Auth0. It protects APIs with JWT authorization and uses FGA to protect endpoints and write authorization data. 32 | - A client that uses the client credentials flow to obtain a JWT to call the resource server. 33 | 34 | #### Create Auth0 application and API 35 | 36 | - Create a new Auth0 API and note the API identifier 37 | - Create a new Auth0 machine-to-machine application, and note the client ID and secret 38 | 39 | #### Start OpenFGA and create a store and authorization model 40 | 41 | This will start an in-memory database OpenFGA server: 42 | 43 | 44 | ```bash 45 | docker pull openfga/openfga:latest 46 | docker run --rm -e OPENFGA_HTTP_ADDR=0.0.0.0:4000 -p 4000:4000 -p 8081:8081 -p 3000:3000 openfga/openfga run 47 | ``` 48 | 49 | Create a store: 50 | 51 | ```bash 52 | fga store create --name "Example Store" --api-url http://localhost:4000 53 | ``` 54 | 55 | You should receive a response like this. Note the store ID value: 56 | 57 | ```json 58 | { 59 | "store": { 60 | "created_at":"2024-02-16T16:56:21.162910175Z", 61 | "id":"01HPSDHYXAD9HS906YFG9CQM02", 62 | "name":"Test Store", 63 | "updated_at":"2024-02-16T16:56:21.162910175Z" 64 | } 65 | } 66 | ``` 67 | 68 | Create an authorization model: 69 | 70 | ```bash 71 | fga model write --api-url http://localhost:4000 --store-id STORE-ID-FROM-ABOVE --file ./example-auth-model.json 72 | ``` 73 | 74 | You should receive a response like this. Note the `authorization_model_id`: 75 | 76 | 77 | ```json 78 | { 79 | "authorization_model_id":"01HPSDPTTC209FQ0P4AMK3AZPE" 80 | } 81 | ``` 82 | 83 | #### Configure resource server 84 | 85 | Configure the application properties: 86 | 87 | ```bash 88 | cd resource-server 89 | cp src/main/resources/application.yml.example src/main/resources/application.yml 90 | ``` 91 | 92 | In `application.yml`, replace the `oauth2` properties with the values from your Auth0 application and API. 93 | 94 | Also replace the values for `fga-store-id` and `fga-authorization-model-id` with the values created above. 95 | 96 | #### Run resource server 97 | 98 | ```bash 99 | ./gradlew bootRun 100 | ``` 101 | 102 | This will start the server on port 8082. 103 | 104 | #### Configure the client 105 | 106 | Configure the application properties: 107 | 108 | ```bash 109 | cd client-restclient 110 | cp src/main/resources/application.yml.example src/main/resources/application.yml 111 | ``` 112 | 113 | Replace the oauth2 values and `auth0-audience` with the values of your Auth0 application and API identifier. 114 | 115 | #### Start the application 116 | 117 | ``` 118 | ./gradlew bootRun 119 | ``` 120 | 121 | This will start the application, execute the client credentials grant to obtain a JWT, and then makes calls to the resource server: 122 | 123 | - Attempt to `GET` a "document" for which the current principal does **not** have an FGA relation to. This request should fail with a `403`. 124 | - A call to create a "document", which will create an FGA relationship associated with the principal. 125 | - Another attempt to get the document, which should now return successfully as there is a `reader` relationship between the principal and the document. 126 | 127 | You can see the results of these calls in the application logs. 128 | 129 | ## Implementation details 130 | 131 | The samples demonstrate the following: 132 | 133 | ### Auto-configuration of `OpenFgaClient` 134 | 135 | Uses custom application property values to create and make available to components an `OpenFgaClient`. This can be used by applications to interact with the FGA API directly, e.g., to write authorization data. 136 | 137 | An application can configure the client in application properties for their usage: 138 | 139 | ```yaml 140 | openfga.fgaApiUrl=FGA_API_URL 141 | openfga.fgaStoreId=FGA_STORE_ID 142 | openfga.fgaAuthorizationModelId=FGA_AUTHORIZATION_MODEL_ID 143 | openfga.fgaApiAudience=FGA_API_AUDIENCE 144 | openfga.fgaClientId=FGA_CLIENT_ID 145 | openfga.fgaClientSecret=FGA_CLIENT_SECRET 146 | ... 147 | ``` 148 | 149 | Note that for simplicity purposes, this sample does not support FGA authorization, thus is NOT suitable for production use. 150 | 151 | ### Simple FGA check bean definition 152 | 153 | A simple bean is defined to perform an authorization check: 154 | 155 | ```java 156 | @PreAuthorize("@openFga.check('#id', 'document', 'reader', 'user')") 157 | public String getDocumentWithSimpleFgaBean(@PathVariable String id) { 158 | return "You have access!"; 159 | } 160 | ``` 161 | 162 | In the example above, the currently authenticated principal's name is used as the user ID by default. It can also be explicitly passed. 163 | 164 | ### Custom `FgaCheck` annotation and aspect 165 | 166 | A custom `@FgaCheck` annotation was created to demonstrate using an explicit FGA annotation and aspect to execute an FGA check prior to the method execution: 167 | 168 | ```java 169 | @FgaCheck(userType="user", relation="reader", objectType="document", object="#id") 170 | public String customAnnotation(@PathVariable String id) { 171 | return "You have access!"; 172 | } 173 | ``` 174 | 175 | Similar to the bean definition, it uses the currently authenticated principal by default for the user ID. 176 | -------------------------------------------------------------------------------- /simple-auth/src/main/java/com/fga/example/config/InitializeOpenFgaData.java: -------------------------------------------------------------------------------- 1 | package com.fga.example.config; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.databind.module.SimpleModule; 5 | import dev.openfga.sdk.api.client.OpenFgaClient; 6 | import dev.openfga.sdk.api.client.model.ClientReadAuthorizationModelResponse; 7 | import dev.openfga.sdk.api.client.model.ClientTupleKey; 8 | import dev.openfga.sdk.api.client.model.ClientWriteRequest; 9 | import dev.openfga.sdk.api.configuration.ClientReadAuthorizationModelOptions; 10 | import dev.openfga.sdk.api.configuration.ClientWriteOptions; 11 | import dev.openfga.sdk.api.model.CreateStoreRequest; 12 | import dev.openfga.sdk.api.model.Store; 13 | import dev.openfga.sdk.api.model.WriteAuthorizationModelRequest; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.beans.factory.InitializingBean; 17 | import org.springframework.context.ResourceLoaderAware; 18 | import org.springframework.core.io.Resource; 19 | import org.springframework.core.io.ResourceLoader; 20 | import org.springframework.http.HttpStatus; 21 | import org.springframework.util.StringUtils; 22 | 23 | import java.nio.charset.StandardCharsets; 24 | import java.util.Objects; 25 | 26 | /** 27 | * Creates an FGA store, writes a simple authorization model, and adds a single tuple of form: 28 | * user: user:123 29 | * relation: can_read 30 | * object: document:1 31 | *

32 | * This is for sample purposes only; would not be necessary in a real application. 33 | */ 34 | public class InitializeOpenFgaData implements ResourceLoaderAware, InitializingBean { 35 | 36 | Logger logger = LoggerFactory.getLogger(InitializeOpenFgaData.class); 37 | private volatile ResourceLoader resourceLoader; 38 | private final OpenFgaProperties openFgaProperties; 39 | private final OpenFgaClient openFgaClient; 40 | 41 | public InitializeOpenFgaData(OpenFgaProperties openFgaProperties, OpenFgaClient openFgaClient) { 42 | this.openFgaProperties = openFgaProperties; 43 | this.openFgaClient = openFgaClient; 44 | } 45 | 46 | @Override 47 | public void afterPropertiesSet() throws Exception { 48 | doInitialization(); 49 | } 50 | 51 | private void doInitialization() throws Exception { 52 | if (StringUtils.hasText(openFgaProperties.getFgaStoreId())) { 53 | validateStore(openFgaProperties.getFgaStoreId()); 54 | if (StringUtils.hasText(openFgaProperties.getFgaAuthorizationModelId())) { 55 | validateAuthorizationModelId(openFgaProperties.getFgaAuthorizationModelId()); 56 | } else { 57 | var latestAuthModel = getLatestAuthorizationModelId(); 58 | logger.debug("Authorization Check resolved with a Status Code of {} and {}", latestAuthModel.getStatusCode(), latestAuthModel.getAuthorizationModel()); 59 | if (latestAuthModel.getStatusCode() == 200) { 60 | openFgaProperties.setFgaAuthorizationModelId(latestAuthModel.getAuthorizationModel().getId()); 61 | } 62 | } 63 | } else { 64 | // This is messy 65 | openFgaProperties.setFgaStoreId(makeStore()); 66 | logger.debug("Failed to find an Authorization Model, checking for a Schema at {}", openFgaProperties.getFgaAuthModelSchema()); 67 | var script = resourceLoader.getResource(openFgaProperties.getFgaAuthModelSchema()); 68 | var mapper = new ObjectMapper().registerModule(new SimpleModule().addDeserializer(ClientTupleKey.class, new OpenFgaClientTupleKeyDeserializer())); // Wrong thing to do for a proper init, but this works for now. 69 | logger.trace("Writing the following Authorization Model Schema: \n{}", script.getContentAsString(StandardCharsets.UTF_8)); 70 | var authWriteResponse = openFgaClient.writeAuthorizationModel(mapper.readValue(script.getContentAsByteArray(), WriteAuthorizationModelRequest.class)).get(); 71 | logger.debug("Authorization Model Creation Request responded with a Status Code of {} and {}", authWriteResponse.getStatusCode(), authWriteResponse.getAuthorizationModelId()); 72 | openFgaProperties.setFgaAuthorizationModelId(authWriteResponse.getAuthorizationModelId()); 73 | for (String relationshipFile : openFgaProperties.getFgaInitialRelationshipTuple()) { 74 | Resource resource = resourceLoader.getResource(relationshipFile); 75 | logger.debug("Adding new Relationship Tuple, \n{}", resource.getContentAsString(StandardCharsets.UTF_8)); 76 | var clientWriteRequest = mapper.readValue(resource.getContentAsByteArray(), ClientWriteRequest.class); 77 | setUpRelationshipTuples(clientWriteRequest); 78 | } 79 | } 80 | } 81 | 82 | private ClientReadAuthorizationModelResponse getLatestAuthorizationModelId() throws Exception { // Way too broad, but the fail cases are extreme in this. 83 | return openFgaClient.readLatestAuthorizationModel().get(); 84 | } 85 | 86 | private void setUpRelationshipTuples(ClientWriteRequest request) { 87 | // Write 88 | try { 89 | logger.debug("Writing Tuples"); 90 | openFgaClient 91 | .write(request, 92 | new ClientWriteOptions() 93 | .disableTransactions(true) 94 | .authorizationModelId(openFgaProperties.getFgaAuthorizationModelId())) 95 | .get(); 96 | logger.debug("Done Writing Tuples"); 97 | } catch (Exception e) { 98 | logger.error("Failed to write {} due to Exception {}, application will now fail to start.", request, e.getMessage(), e); 99 | throw new RuntimeException(e); 100 | } 101 | } 102 | 103 | private void validateStore(String storeId) throws Exception { 104 | Objects.requireNonNull(storeId); 105 | var invalidStore = openFgaClient.listStores().get() 106 | .getStores() 107 | .stream() 108 | .map(Store::getId) 109 | .noneMatch(storeId::equals); 110 | if (invalidStore) { 111 | throw new IllegalArgumentException("The Store ID: " + storeId + " does not exist."); 112 | } 113 | } 114 | 115 | private void validateAuthorizationModelId(String authId) throws Exception { 116 | Objects.requireNonNull(authId); 117 | var storeAuthModelResponse = openFgaClient.readAuthorizationModel(new ClientReadAuthorizationModelOptions().authorizationModelId(authId)) 118 | .get(); 119 | logger.debug("Validating Authorization Model {}, Status Code {}, Response {}", authId, storeAuthModelResponse.getStatusCode(), storeAuthModelResponse.getAuthorizationModel()); 120 | switch (HttpStatus.valueOf(storeAuthModelResponse.getStatusCode())) { 121 | case HttpStatus.BAD_REQUEST, HttpStatus.NOT_FOUND -> 122 | throw new IllegalStateException("Failed to find the Authorization Model for " + authId); 123 | case HttpStatus.CONFLICT -> 124 | throw new IllegalStateException("Transaction Conflict within OpenFGA for Authorization Model: " + authId); 125 | case HttpStatus.INTERNAL_SERVER_ERROR -> 126 | throw new IllegalStateException("OpenFGA Server had an internal failure when checking for the Authorization Model of " + authId); 127 | } 128 | } 129 | 130 | private String makeStore() { 131 | String storeName = openFgaProperties.getFgaStoreName(); 132 | Objects.requireNonNull(storeName, "Failed to have a Store ID or Store Name provided, OpenFGA will fail to start."); 133 | String newStoreId; 134 | try { 135 | logger.debug("Created a new Store with the name {}", storeName); 136 | var newStore = openFgaClient.createStore(new CreateStoreRequest().name(storeName)).get(); 137 | logger.debug("Store Name {}, Status Code {}, Response {}", storeName, newStore.getStatusCode(), newStore.getRawResponse()); 138 | openFgaClient.setStoreId(newStore.getId()); 139 | newStoreId = newStore.getId(); 140 | } catch (Exception e) { 141 | logger.error("Failed to create a new OpenFGA Store", e); 142 | throw new RuntimeException(e); 143 | } 144 | return newStoreId; 145 | } 146 | 147 | @Override 148 | public void setResourceLoader(ResourceLoader resourceLoader) { 149 | this.resourceLoader = resourceLoader; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /simple-auth/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | --------------------------------------------------------------------------------