Skip to content

Commit

Permalink
feat: add example application
Browse files Browse the repository at this point in the history
  • Loading branch information
jimmyjames committed Apr 8, 2024
1 parent ef72f07 commit 106b976
Show file tree
Hide file tree
Showing 11 changed files with 459 additions and 0 deletions.
64 changes: 64 additions & 0 deletions examples/servlet/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Spring Boot OpenFGA Example

An example Spring Boot application that demonstrates using OpenFGA Spring Boot Starter.

## Requirements

- Java 17
- Docker

## Configuration

This example is configured to connect to the OpenFGA server running on port 4000.
To use a different FGA server, update `src/main/resources/application.yaml` accordingly.

## Usage

### Start OpenFGA server

```bash
docker pull openfga/openfga:latest
docker run --rm -e OPENFGA_HTTP_ADDR=0.0.0.0:4000 -p 4000:4000 -p 8081:8081 -p 3000:3000 openfga/openfga run
```

### Start the example application:

```bash
./gradlew bootRun
```

This will start the application on port 8080. As part of the application startup, some data is loaded:

- Two documents, with IDs `1` and `2`
- A simple FGA authorization model, along with an authorization tuple that grants user `anne` viewer access to document `1`.

### Make requests

Execute a GET request for document 1, for which user `anne` has viewer access:

```bash
curl http://localhost:8080/documents/1
```

You should receive a 200 response with the document:

```json
{
"id": "1",
"content": "this is document 1 content"
}
```

Execute a request for document 2, for which user `anne` does **not** have viewer access to:

```bash
curl http://localhost:8080/documents/2
```

You should receive a 403 response, as user `anne` does not have the required relation to document 2.

You can also create a document, for which user `anne` will be granted the owner relation for the document:

```bash
curl -d '{"id": "10", "content": "new document content"}' -H 'Content-Type: application/json' http://localhost:8080/documents
```
3 changes: 3 additions & 0 deletions examples/servlet/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@ plugins {

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation project(':openfga-spring-boot-starter')
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package dev.openfga.example.config;

import dev.openfga.example.model.Document;
import dev.openfga.example.service.DocumentRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* Preloads database with two documents.
*/
@Configuration
public class LoadDocuments {
private static final Logger log = LoggerFactory.getLogger(LoadDocuments.class);

@Bean
CommandLineRunner initDatabase(DocumentRepository repository) {

return args -> {
log.info("Preloading " + repository.save(new Document("1", "this is document 1 content")));
log.info("Preloading " + repository.save(new Document("2", "this is document 2 content")));
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package dev.openfga.example.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import dev.openfga.sdk.api.client.OpenFgaClient;
import dev.openfga.sdk.api.client.model.ClientReadRequest;
import dev.openfga.sdk.api.client.model.ClientTupleKey;
import dev.openfga.sdk.api.client.model.ClientWriteRequest;
import dev.openfga.sdk.api.configuration.ClientWriteOptions;
import dev.openfga.sdk.api.model.CreateStoreRequest;
import dev.openfga.sdk.api.model.WriteAuthorizationModelRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

/**
* Creates an FGA store, writes a simple authorization model, and adds a single tuple of form:
* user: user:anne
* relation: viewer
* object: document:1
*
* This is for sample purposes only; would not be necessary in a real application.
*/
@Configuration
public class LoadFgaData implements CommandLineRunner {

Logger logger = LoggerFactory.getLogger(LoadFgaData.class);

@Autowired
private OpenFgaClient openFgaClient;

@Autowired
private ObjectMapper objectMapper;

@Override
public void run(String... args) throws Exception {
loadFgaData();
}

private void loadFgaData() throws Exception {
// CreateStore
logger.debug("Creating Test Store");
var store = openFgaClient
.createStore(new CreateStoreRequest().name("Demo Store"))
.get();
logger.debug("Test Store ID: " + store.getId());

// Set the store id
openFgaClient.setStoreId(store.getId());

// ListStores after Create
logger.debug("Listing Stores");
var stores = openFgaClient.listStores().get();
logger.debug("Stores Count: " + stores.getStores().size());

// GetStore
logger.debug("Getting Current Store");
var currentStore = openFgaClient.getStore().get();
logger.debug("Current Store Name: " + currentStore.getName());

var authModelJson = loadResource();
var authorizationModel = openFgaClient
.writeAuthorizationModel(objectMapper.readValue(authModelJson, WriteAuthorizationModelRequest.class))
.get();
logger.debug("Authorization Model ID " + authorizationModel.getAuthorizationModelId());

// Set the model ID
openFgaClient.setAuthorizationModelId(authorizationModel.getAuthorizationModelId());

// Write
logger.debug("Writing Tuples");
openFgaClient
.write(
new ClientWriteRequest()
.writes(List.of(new ClientTupleKey()
.user("user:anne")
.relation("viewer")
._object("document:1"))),
new ClientWriteOptions()
.disableTransactions(true)
.authorizationModelId(authorizationModel.getAuthorizationModelId()))
.get();
logger.debug("Done Writing Tuples");

// Read
logger.debug("Reading Tuples");
var readTuples = openFgaClient.read(new ClientReadRequest()).get();
logger.debug("Read Tuples" + objectMapper.writeValueAsString(readTuples));
}

private String loadResource() {
try {
return new ClassPathResource("example-auth-model.json").getContentAsString(StandardCharsets.UTF_8);
} catch (IOException ioe) {
throw new RuntimeException("Unable to load resource: " + "example-auth-model.json", ioe);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package dev.openfga.example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
// For simple demonstration purposes, do not require requests to be authenticated
.authorizeHttpRequests(customizer -> customizer.anyRequest().permitAll())
.csrf(CsrfConfigurer::disable)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package dev.openfga.example.controllers;

import dev.openfga.example.model.Document;
import dev.openfga.example.service.DocumentService;
import dev.openfga.sdk.api.model.Tuple;
import java.util.List;
import java.util.Optional;
import org.springframework.web.bind.annotation.*;

@RestController
public class DocumentController {

private final DocumentService documentService;

public DocumentController(DocumentService documentService) {
this.documentService = documentService;
}

@GetMapping("/documents/{id}")
public Optional<Document> getDocument(@PathVariable String id) {
return documentService.getDocumentWithPreAuthorize(id);
}

@PostMapping("/documents")
public Document createDocument(@RequestBody Document document) {
return documentService.createDoc(document);
}

/**
* For convenience only; lists the authorization tuples.
*
* @return authorization tuples
*/
@GetMapping("/tuples")
public List<Tuple> getTuples() throws Exception {
return documentService.getTuples().getTuples();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package dev.openfga.example.model;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
public class Document {

private @Id String id;

private String content;

public Document() {}

public Document(String id, String content) {
this.id = id;
this.content = content;
}

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getContent() {
return content;
}

public void setContent(String content) {
this.content = content;
}

@Override
public String toString() {
return "Document{" + "id=" + this.id + ", content='" + this.content + "'}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package dev.openfga.example.service;

import dev.openfga.example.model.Document;
import org.springframework.data.jpa.repository.JpaRepository;

public interface DocumentRepository extends JpaRepository<Document, String> {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package dev.openfga.example.service;

import dev.openfga.example.model.Document;
import dev.openfga.sdk.api.client.OpenFgaClient;
import dev.openfga.sdk.api.client.model.ClientReadRequest;
import dev.openfga.sdk.api.client.model.ClientReadResponse;
import dev.openfga.sdk.api.client.model.ClientTupleKey;
import dev.openfga.sdk.api.client.model.ClientWriteRequest;
import dev.openfga.sdk.errors.FgaInvalidParameterException;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service
public class DocumentService {

private final OpenFgaClient fgaClient;
private final DocumentRepository repository;

public DocumentService(OpenFgaClient fgaClient, DocumentRepository repository) {
this.fgaClient = fgaClient;
this.repository = repository;
}

/**
* Ensure user has permission in PreAuthorize
*/
@PreAuthorize("@fga.check('document', #id, 'can_read', 'user', 'anne')")
public Optional<Document> getDocumentWithPreAuthorize(String id) {
return repository.findById(id);
}

public ClientReadResponse getTuples() throws Exception {
return fgaClient.read(new ClientReadRequest()).get();
}

/**
* Demonstrates a simple example of using the injected fgaClient to write authorization data to FGA.
*/
public Document createDoc(Document document) {

// write to fga
ClientWriteRequest writeRequest = new ClientWriteRequest()
.writes(List.of(new ClientTupleKey()
.user("user:anne")
.relation("owner")
._object(String.format("document:%s", document.getId()))));

try {
fgaClient.write(writeRequest).get();
} catch (InterruptedException | ExecutionException | FgaInvalidParameterException e) {
throw new RuntimeException("Error writing to FGA", e);
}

// create doc
return repository.save(document);
}
}
11 changes: 11 additions & 0 deletions examples/servlet/src/main/resources/application.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
logging:
level:
org:
# springframework: DEBUG
example: TRACE

########## FGA CONFIG #########

### NO AUTH - for example purposes only!! ###
openfga:
api-url: http://localhost:4000
Loading

0 comments on commit 106b976

Please sign in to comment.