Skip to content

Commit 4083ac1

Browse files
sungwysnazy
andauthored
Use POJOs for OPA JSON schema construction and publish schema (#3031)
Co-authored-by: Robert Stupp <snazy@snazy.de>
1 parent 7c93c87 commit 4083ac1

File tree

14 files changed

+879
-92
lines changed

14 files changed

+879
-92
lines changed

extensions/auth/opa/impl/SCHEMA.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<!--
2+
Licensed to the Apache Software Foundation (ASF) under one
3+
or more contributor license agreements. See the NOTICE file
4+
distributed with this work for additional information
5+
regarding copyright ownership. The ASF licenses this file
6+
to you under the Apache License, Version 2.0 (the
7+
"License"); you may not use this file except in compliance
8+
with the License. You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing,
13+
software distributed under the License is distributed on an
14+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
KIND, either express or implied. See the License for the
16+
specific language governing permissions and limitations
17+
under the License.
18+
-->
19+
20+
# OPA Input Schema Management
21+
22+
This document describes how the OPA authorization input schema is managed in Apache Polaris.
23+
24+
## Overview
25+
26+
The OPA input schema follows a **schema-as-code** approach where:
27+
28+
1. **Java model classes** (in `model/` package) are the single source of truth
29+
2. **JSON Schema** is automatically generated from these classes
30+
3. **CI validation** ensures the schema stays in sync with the code
31+
32+
## Developer Workflow
33+
34+
### Modifying the Schema
35+
36+
When you need to add/modify fields in the OPA input:
37+
38+
1. **Update the model classes** in `src/main/java/org/apache/polaris/extension/auth/opa/model/`
39+
```java
40+
@PolarisImmutable
41+
public interface Actor {
42+
String principal();
43+
List<String> roles();
44+
// Add new field here
45+
}
46+
```
47+
48+
2. **Regenerate the JSON Schema**
49+
```bash
50+
./gradlew :polaris-extensions-auth-opa:generateOpaSchema
51+
```
52+
53+
3. **Commit both changes**
54+
- The updated Java files
55+
- The updated `opa-input-schema.json`
56+
57+
4. **CI will validate** that the schema matches the code
58+
59+
### CI Validation
60+
61+
The `validateOpaSchema` task automatically runs during `./gradlew check`:
62+
63+
```bash
64+
./gradlew :polaris-extensions-auth-opa:check
65+
```
66+
67+
This task:
68+
1. Generates schema from current code to a temp file
69+
2. Compares it with the committed `opa-input-schema.json`
70+
3. **Fails the build** if they don't match
71+
72+
#### What happens if validation fails?
73+
74+
You'll see an error like:
75+
76+
```
77+
❌ OPA Schema validation failed!
78+
79+
The committed opa-input-schema.json does not match the generated schema.
80+
This means the schema is out of sync with the model classes.
81+
82+
To fix this, run:
83+
./gradlew :polaris-extensions-auth-opa:generateOpaSchema
84+
85+
Then commit the updated opa-input-schema.json file.
86+
```
87+
88+
Simply run the suggested command and commit the regenerated schema.
89+
90+
## Gradle Tasks
91+
92+
### `generateOpaSchema`
93+
Generates the JSON Schema from model classes.
94+
95+
```bash
96+
./gradlew :polaris-extensions-auth-opa:generateOpaSchema
97+
```
98+
99+
**Output**: `extensions/auth/opa/impl/opa-input-schema.json`
100+
101+
### `validateOpaSchema`
102+
Validates that committed schema matches the code.
103+
104+
```bash
105+
./gradlew :polaris-extensions-auth-opa:validateOpaSchema
106+
```
107+
108+
**Runs automatically** as part of `:check` task.
109+
110+
## For OPA Policy Developers
111+
112+
The generated `opa-input-schema.json` documents the structure of authorization requests sent from Polaris to OPA.
113+
114+
## Model Classes Reference
115+
116+
| Class | Purpose | Key Fields |
117+
|-------|---------|------------|
118+
| `OpaRequest` | Top-level wrapper | `input` |
119+
| `OpaAuthorizationInput` | Complete auth context | `actor`, `action`, `resource`, `context` |
120+
| `Actor` | Principal information | `principal`, `roles` |
121+
| `Resource` | Resources being accessed | `targets`, `secondaries` |
122+
| `ResourceEntity` | Individual resource | `type`, `name`, `parents` |
123+
| `Context` | Request metadata | `request_id` |
124+
125+
See the [model package README](src/main/java/org/apache/polaris/extension/auth/opa/model/README.md) for detailed usage examples.

extensions/auth/opa/impl/build.gradle.kts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,15 @@
1717
* under the License.
1818
*/
1919

20+
import java.io.OutputStream
21+
2022
plugins {
2123
id("polaris-server")
2224
id("org.kordamp.gradle.jandex")
2325
}
2426

27+
val jsonSchemaGenerator = sourceSets.create("jsonSchemaGenerator")
28+
2529
dependencies {
2630
implementation(project(":polaris-core"))
2731
implementation(libs.apache.httpclient5)
@@ -33,6 +37,13 @@ dependencies {
3337
implementation(libs.auth0.jwt)
3438
implementation(project(":polaris-async-api"))
3539

40+
add(jsonSchemaGenerator.implementationConfigurationName, project(":polaris-extensions-auth-opa"))
41+
add(jsonSchemaGenerator.implementationConfigurationName, platform(libs.jackson.bom))
42+
add(
43+
jsonSchemaGenerator.implementationConfigurationName,
44+
"com.fasterxml.jackson.module:jackson-module-jsonSchema",
45+
)
46+
3647
// Iceberg dependency for ForbiddenException
3748
implementation(platform(libs.iceberg.bom))
3849
implementation("org.apache.iceberg:iceberg-api")
@@ -58,3 +69,95 @@ dependencies {
5869
testImplementation(project(":polaris-async-java"))
5970
testImplementation(project(":polaris-idgen-mocks"))
6071
}
72+
73+
// Task to generate JSON Schema from model classes
74+
tasks.register<JavaExec>("generateOpaSchema") {
75+
group = "documentation"
76+
description = "Generates JSON Schema for OPA authorization input"
77+
78+
dependsOn(tasks.compileJava, tasks.named("jandex"))
79+
80+
// Only execute generation if anything changed
81+
outputs.cacheIf { true }
82+
outputs.file("${projectDir}/opa-input-schema.json")
83+
inputs.files(jsonSchemaGenerator.runtimeClasspath)
84+
85+
classpath = jsonSchemaGenerator.runtimeClasspath
86+
mainClass.set("org.apache.polaris.extension.auth.opa.model.OpaSchemaGenerator")
87+
args("${projectDir}/opa-input-schema.json")
88+
}
89+
90+
// Task to validate that the committed schema matches the generated schema
91+
tasks.register<JavaExec>("validateOpaSchema") {
92+
group = "verification"
93+
description = "Validates that the committed OPA schema matches the generated schema"
94+
95+
dependsOn(tasks.compileJava, tasks.named("jandex"))
96+
97+
val tempSchemaFile = layout.buildDirectory.file("opa-schema/opa-input-schema-generated.json")
98+
val committedSchemaFile = file("${projectDir}/opa-input-schema.json")
99+
val logFile = layout.buildDirectory.file("opa-schema/generator.log")
100+
101+
// Only execute validation if anything changed
102+
outputs.cacheIf { true }
103+
outputs.file(tempSchemaFile)
104+
inputs.file(committedSchemaFile)
105+
inputs.files(jsonSchemaGenerator.runtimeClasspath)
106+
107+
classpath = jsonSchemaGenerator.runtimeClasspath
108+
mainClass.set("org.apache.polaris.extension.auth.opa.model.OpaSchemaGenerator")
109+
args(tempSchemaFile.get().asFile.absolutePath)
110+
isIgnoreExitValue = true
111+
112+
var outStream: OutputStream? = null
113+
doFirst {
114+
// Ensure temp directory exists
115+
tempSchemaFile.get().asFile.parentFile.mkdirs()
116+
outStream = logFile.get().asFile.outputStream()
117+
standardOutput = outStream
118+
errorOutput = outStream
119+
}
120+
121+
doLast {
122+
outStream?.close()
123+
124+
if (executionResult.get().exitValue != 0) {
125+
throw GradleException(
126+
"""
127+
|OPA Schema validation failed!
128+
|
129+
|${logFile.get().asFile.readText()}
130+
"""
131+
.trimMargin()
132+
)
133+
}
134+
135+
val generatedContent = tempSchemaFile.get().asFile.readText().trim()
136+
val committedContent = committedSchemaFile.readText().trim()
137+
138+
if (generatedContent != committedContent) {
139+
throw GradleException(
140+
"""
141+
|OPA Schema validation failed!
142+
|
143+
|The committed opa-input-schema.json does not match the generated schema.
144+
|This means the schema is out of sync with the model classes.
145+
|
146+
|To fix this, run:
147+
| ./gradlew :polaris-extensions-auth-opa:generateOpaSchema
148+
|
149+
|Then commit the updated opa-input-schema.json file.
150+
|
151+
|Committed file: ${committedSchemaFile.absolutePath}
152+
|Generated file: ${tempSchemaFile.get().asFile.absolutePath}
153+
"""
154+
.trimMargin()
155+
)
156+
}
157+
158+
logger.info("OPA schema validation passed - schema is up to date")
159+
}
160+
}
161+
162+
// Add schema validation to the check task
163+
tasks.named("check") { dependsOn("validateOpaSchema") }
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
{
2+
"type" : "object",
3+
"id" : "urn:jsonschema:org:apache:polaris:extension:auth:opa:model:OpaAuthorizationInput",
4+
"properties" : {
5+
"actor" : {
6+
"type" : "object",
7+
"id" : "urn:jsonschema:org:apache:polaris:extension:auth:opa:model:Actor",
8+
"required" : true,
9+
"properties" : {
10+
"principal" : {
11+
"type" : "string",
12+
"required" : true
13+
},
14+
"roles" : {
15+
"type" : "array",
16+
"items" : {
17+
"type" : "string"
18+
}
19+
}
20+
}
21+
},
22+
"action" : {
23+
"type" : "string",
24+
"required" : true
25+
},
26+
"resource" : {
27+
"type" : "object",
28+
"id" : "urn:jsonschema:org:apache:polaris:extension:auth:opa:model:Resource",
29+
"required" : true,
30+
"properties" : {
31+
"targets" : {
32+
"type" : "array",
33+
"items" : {
34+
"type" : "object",
35+
"id" : "urn:jsonschema:org:apache:polaris:extension:auth:opa:model:ResourceEntity",
36+
"properties" : {
37+
"type" : {
38+
"type" : "string",
39+
"required" : true
40+
},
41+
"name" : {
42+
"type" : "string",
43+
"required" : true
44+
},
45+
"parents" : {
46+
"type" : "array",
47+
"items" : {
48+
"type" : "object",
49+
"$ref" : "urn:jsonschema:org:apache:polaris:extension:auth:opa:model:ResourceEntity"
50+
}
51+
}
52+
}
53+
}
54+
},
55+
"secondaries" : {
56+
"type" : "array",
57+
"items" : {
58+
"type" : "object",
59+
"$ref" : "urn:jsonschema:org:apache:polaris:extension:auth:opa:model:ResourceEntity"
60+
}
61+
}
62+
}
63+
},
64+
"context" : {
65+
"type" : "object",
66+
"id" : "urn:jsonschema:org:apache:polaris:extension:auth:opa:model:Context",
67+
"required" : true,
68+
"properties" : {
69+
"request_id" : {
70+
"type" : "string",
71+
"required" : true
72+
}
73+
}
74+
}
75+
}
76+
}

0 commit comments

Comments
 (0)