- Kotlin 98.3%
- Java 1.7%
| .forgejo/workflows | ||
| bom | ||
| client | ||
| core | ||
| example | ||
| gradle/wrapper | ||
| gradle-plugin | ||
| maven-plugin | ||
| modeling-guide-tests | ||
| spring-boot-starter | ||
| src | ||
| .gitignore | ||
| build.gradle.kts | ||
| gradlew | ||
| gradlew.bat | ||
| LICENSE | ||
| README.md | ||
| settings.gradle.kts | ||
OpenFGA Kotlin Generator
The OpenFGA Kotlin Generator turns an OpenFGA DSL model into a type-safe Kotlin API.
Instead of assembling tuple strings such as documents__document:roadmap#viewer@iam__user:alice
by hand, applications work with generated objects and relation methods:
val alice = IamUser("alice")
val roadmap = DocumentsDocument("roadmap")
val tuple = roadmap.viewer(alice)
Invalid subject, relation, and target combinations are rejected by the Kotlin compiler. The generator uses the official OpenFGA language parser and generates helpers only for subjects that can be assigned directly through OpenFGA tuples.
Generated API at a glance
Given this model:
model
schema 1.2
type iam__user
type documents__folder
relations
define participant: [iam__user]
define viewer: [iam__user]
type documents__document
relations
define can_view: [documents__folder#participant, iam__user]
define parent: [documents__folder]
define owner: [iam__user]
define viewer: [iam__user] or viewer from parent
the generator creates:
- one Kotlin object class per OpenFGA type, for example
IamUser,DocumentsFolder, andDocumentsDocument; - typed relation enums for checks;
- typed methods on target objects for creating writable tuples;
- typed userset subjects such as
DocumentsFolderParticipantSubject; - conversions to OpenFGA SDK request objects and JSON.
Generated package names follow the type prefix before __. With the root package
com.example.authorization, documents__document is generated below
com.example.authorization.documents.
Working with direct relations
The relation:
define viewer: [iam__user]
produces a method accepting only IamUser:
val alice = IamUser("alice")
val folder = DocumentsFolder("product")
val tuple = folder.viewer(alice)
The tuple contains:
user: iam__user:alice
relation: viewer
object: documents__folder:product
It can be converted for the OpenFGA SDK or serialized for inspection:
val sdkTuple = tuple.toClientTupleKey()
val json = tuple.toJson()
Working with relation usersets
An assignable subject containing #participant refers to the participant userset of one concrete
folder:
define can_view: [documents__folder#participant]
For a folder with the ID ALL, the generated API is:
val participants = DocumentsFolder("ALL").participant()
val document = DocumentsDocument("roadmap")
val tuple = document.canView(participants)
This creates:
documents__document:roadmap#can_view@documents__folder:ALL#participant
ALL is an ordinary object ID here. It is not the OpenFGA wildcard *.
The userset is represented by its own Kotlin type. Therefore, if can_view accepts only
documents__folder#participant, these calls do not compile:
document.canView(DocumentsFolder("ALL")) // folder object instead of its participant userset
document.canView(alice) // user is not allowed by this relation definition
Relations with multiple allowed subject types
OpenFGA can allow multiple directly assignable subject types:
define can_view: [documents__folder#participant, iam__user]
The generator creates one type-safe overload for each alternative:
val alice = IamUser("alice")
val participants = DocumentsFolder("ALL").participant()
val document = DocumentsDocument("roadmap")
val directAccess = document.canView(alice)
val participantAccess = document.canView(participants)
These produce different subjects while using the same target relation:
documents__document:roadmap#can_view@iam__user:alice
documents__document:roadmap#can_view@documents__folder:ALL#participant
Bound relations
Every directly writable relation can also be obtained without creating a tuple. The returned value retains the relation type and the concrete target instance:
val parent = document.parent()
parent.relation // DocumentsDocument.FolderRelation.PARENT
parent.target // the same document instance
Tuple creation remains direct:
val tuple = document.parent(folder)
Relation enums are nested in their target type, for example
DocumentsDocument.FolderRelation.PARENT. If one relation accepts several subject types, its
parameterless accessors are subject-qualified, such as viewerUser() and
viewerGroupMember(). The tuple overloads remain viewer(user) and viewer(group.member()).
Computed relations
For a rewrite such as:
define viewer: [iam__user] or viewer from parent
only [iam__user] is directly writable on viewer. The viewer from parent branch is evaluated
by OpenFGA and does not create another writable tuple method. The application writes the parent
connection and the relevant folder permission:
val alice = IamUser("alice")
val folder = DocumentsFolder("product")
val document = DocumentsDocument("roadmap")
val tuples = listOf(
folder.viewer(alice),
document.parent(folder),
)
OpenFGA follows the model rewrite when checking whether Alice is a viewer of the document.
If a relation is computed only:
define viewer: viewer from parent
the generated method returns a check-only query:
val query: FgaAuthorizationQuery<IamUser, DocumentsDocument> = document.viewer(alice)
FgaTuple extends FgaAuthorizationQuery, so direct tuples support both check() and create().
A computed-only query supports check() but intentionally has no create() extension.
Checks
Add the Spring Boot starter to execute tuples as OpenFGA checks:
dependencies {
implementation("de.denktmit.openfga:openfga-kotlin-spring-boot-starter")
}
Provide an OpenFgaClient as a Spring bean. The starter resolves it automatically:
@Bean
fun openFgaClient(): OpenFgaClient = OpenFgaClient(configuration)
The starter exposes an OpenFgaConnection bean. Its util scope makes the tuple extensions
available without exposing the SDK client to OpenFgaUtil:
import de.denktmit.openfga.client.OpenFgaConnection
import de.denktmit.openfga.client.util
import de.denktmit.openfga.core.FgaAuthorizationQuery
@Service
class AuthorizationService(
private val openFga: OpenFgaConnection,
) {
fun mayView(query: FgaAuthorizationQuery<*, *>): Boolean =
openFga.util {
query.check()
}
}
Iterables use the OpenFGA batch-check endpoint:
import de.denktmit.openfga.client.FgaAuthorizationCheckResult
val results: List<FgaAuthorizationCheckResult> = openFga.util {
tuples.check()
}
results.forEach { result ->
println("${result.query.objectValue}: ${result.allowed}, error=${result.error}")
}
Complete typed tuples can also be written idempotently:
openFga.util {
document.viewer(user).create()
additionalTuples.create()
}
The iterable write uses one OpenFGA SDK request and ignores tuples that already exist. A successful
write returns Unit; SDK, transport, and configuration failures are propagated as exceptions.
Empty iterables return immediately without contacting OpenFGA.
When several clients exist, mark the default as @Primary or configure the client bean name or
qualifier:
openfga.kotlin.client-qualifier=authorization
The qualifier may be either a Spring bean name or an @Qualifier value. Applications can override
the auto-configured connection by providing their own OpenFgaConnection bean. The extensions are
blocking: they wait for the asynchronous OpenFGA SDK response before returning.
The Spring-free openfga-kotlin-client artifact can be used directly:
val connection = openFgaConnection(client)
connection.util {
tuple.check()
}
Gradle setup
Apply the plugin:
plugins {
kotlin("jvm")
id("de.denktmit.openfga.kotlin-generator") version "0.1.0-SNAPSHOT"
}
Add the generated-code runtime. The BOM keeps related artifacts on the same version:
dependencies {
implementation(platform("de.denktmit.openfga:openfga-kotlin-generator-bom:0.1.0-SNAPSHOT"))
implementation("de.denktmit.openfga:openfga-kotlin-generator-core")
implementation("de.denktmit.openfga:openfga-kotlin-spring-boot-starter")
}
Place the model at src/main/openfga/model.fga. Those are the default model and output locations,
so no configuration is required for the common case. To customize generation:
import de.denktmit.openfga.generator.gradle.OpenFgaKotlinGeneratorExtension
extensions.configure<OpenFgaKotlinGeneratorExtension>("openFgaKotlin") {
modelFile.set(layout.projectDirectory.file("src/main/openfga/model.fga"))
outputDirectory.set(layout.buildDirectory.dir("generated/source/openfga/main"))
packageName.set("com.example.authorization")
// Optional filters. With no filters, all model types are generated.
includeTypes.set(listOf("iam__user"))
includePrefixes.set(listOf("documents__"))
}
Run generation explicitly with:
./gradlew generateOpenFgaKotlin
The plugin also:
- adds the output directory to the Kotlin
mainsource set; - makes
compileKotlindepend ongenerateOpenFgaKotlin; - removes stale files from the generated package directory before generation.
Set cleanOutputDirectory to true only when the entire configured output directory belongs to
the generator.
Maven setup
Add the runtime dependency:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>de.denktmit.openfga</groupId>
<artifactId>openfga-kotlin-generator-bom</artifactId>
<version>0.1.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>de.denktmit.openfga</groupId>
<artifactId>openfga-kotlin-generator-core</artifactId>
</dependency>
</dependencies>
Configure the Maven plugin:
<plugin>
<groupId>de.denktmit.openfga</groupId>
<artifactId>openfga-kotlin-generator-maven-plugin</artifactId>
<version>0.1.0-SNAPSHOT</version>
<executions>
<execution>
<id>generate-openfga-kotlin</id>
<phase>generate-sources</phase>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<configuration>
<modelFile>${project.basedir}/src/main/openfga/model.fga</modelFile>
<outputDirectory>${project.build.directory}/generated-sources/openfga</outputDirectory>
<packageName>com.example.authorization</packageName>
</configuration>
</plugin>
The plugin registers the output as a Maven compile source root. If kotlin-maven-plugin uses
explicit <sourceDirs>, also add ${project.build.directory}/generated-sources/openfga.
CLI usage
Print generated files to standard output:
java -cp <generator-classpath> de.denktmit.openfga.generator.OpenFgaConnectionGenerator \
kotlin src/main/openfga/model.fga \
--package com.example.authorization
Write generated files to a source directory:
java -cp <generator-classpath> de.denktmit.openfga.generator.OpenFgaConnectionGenerator \
write-kotlin src/main/openfga/model.fga build/generated/source/openfga/main \
--package com.example.authorization
Generation can be restricted by exact type or prefix. Both options can be repeated:
--include-type iam__user \
--include-prefix documents__
Without filters, every model type is generated. The legacy command names --kotlin and
--write-kotlin remain supported.
By default, write-kotlin cleans only the generated package below the output directory. Use
--clean-output-directory only if the entire output directory may be deleted.
Running this repository
Build and test all modules:
./gradlew test
Generate, compile, and test the included example:
./gradlew :example:test
The relevant example files are:
example/src/main/openfga/model.fgaexample/src/main/kotlin/de/denktmit/openfga/example/DocumentPermissions.ktexample/src/test/kotlin/de/denktmit/openfga/example/DocumentPermissionsTest.kt
License
Licensed under the Apache License, Version 2.0. See LICENSE.