Skip to content

Commit

Permalink
feat(java): support validating a json based authorization model (#245)
Browse files Browse the repository at this point in the history
  • Loading branch information
rhamzeh authored Jun 12, 2024
2 parents 19f2c87 + 205c714 commit a3958b8
Show file tree
Hide file tree
Showing 11 changed files with 256 additions and 31 deletions.
2 changes: 1 addition & 1 deletion pkg/java/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ ext {

dependencies {
implementation 'org.antlr:antlr4:4.13.1'
implementation 'dev.openfga:openfga-sdk:0.3.1'
implementation 'dev.openfga:openfga-sdk:0.4.1'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.1'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.15.3'
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ public ErrorProperties(StartEnd line, StartEnd column, String message) {
}

String getFullMessage(String type) {
return String.format("%s error at line=%d, column=%d: %s", type, line.getStart(), column.getStart(), message);
if (line != null && column != null) {
return String.format(
"%s error at line=%d, column=%d: %s", type, line.getStart(), column.getStart(), message);
} else {
return String.format("%s error: %s", type, message);
}
}

public StartEnd getLine() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public class ModelValidationSingleError extends ParsingError {
public ModelValidationSingleError() {}

public ModelValidationSingleError(ErrorProperties properties, ValidationMetadata metadata) {
super("syntax", properties);
super("validation", properties);
this.metadata = metadata;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public StartEnd getLine() {
}

public StartEnd getLine(int offset) {
if (line == null) {
return null;
}
return line.withOffset(offset);
}

Expand All @@ -36,6 +39,9 @@ public StartEnd getColumn() {
}

public StartEnd getColumn(int offset) {
if (line == null) {
return null;
}
return column.withOffset(offset);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ public enum ValidationError {
SelfError("self-error"),
TuplesetNotDirect("tupleuserset-not-direct"),
TypeRestrictionCannotHaveWildcardAndRelation("type-wildcard-relation"),
InvalidRelationOnTupleset("invalid-relation-on-tupleset");
InvalidRelationOnTupleset("invalid-relation-on-tupleset"),
DifferentNestedConditionName("different-nested-condition-name"),
MultipleModulesInFile("multiple-modules-in-file");

private final String value;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ class Dsl {
}

private int findLine(Predicate<String> predicate, int skipIndex) {
if (lines == null) {
return -1;
}

return IntStream.range(skipIndex, lines.length)
.filter(index -> predicate.test(lines[index]))
.findFirst()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.openfga.language.validation;

import static dev.openfga.language.Utils.getNullSafe;
import static dev.openfga.language.Utils.getNullSafeList;
import static dev.openfga.language.validation.Dsl.*;
import static java.util.Collections.emptyList;
Expand All @@ -10,27 +11,37 @@
import java.io.IOException;
import java.util.*;

public class DslValidator {
public class ModelValidator {

private final ValidationOptions options;
private final AuthorizationModel authorizationModel;
private final Dsl dsl;
private final ValidationErrorsBuilder errors;
private ValidationRegex typeRegex;
private ValidationRegex relationRegex;
private Map<String, Set<String>> fileToModules = new HashMap<>();

public DslValidator(ValidationOptions options, AuthorizationModel authorizationModel, String[] lines) {
public ModelValidator(ValidationOptions options, AuthorizationModel authorizationModel, String[] lines) {
this.options = options;
this.authorizationModel = authorizationModel;
dsl = new Dsl(lines);
errors = new ValidationErrorsBuilder(lines);
}

public static void validate(String dsl) throws DslErrorsException, IOException {
validate(dsl, new ValidationOptions());
public static void validateJson(AuthorizationModel authorizationModel) throws DslErrorsException {
validateJson(authorizationModel, new ValidationOptions());
}

public static void validate(String dsl, ValidationOptions options) throws DslErrorsException {
public static void validateJson(AuthorizationModel authorizationModel, ValidationOptions options)
throws DslErrorsException {
new ModelValidator(options, authorizationModel, null).validate();
}

public static void validateDsl(String dsl) throws DslErrorsException, IOException {
validateDsl(dsl, new ValidationOptions());
}

public static void validateDsl(String dsl, ValidationOptions options) throws DslErrorsException {
var transformer = new DslToJsonTransformer();
var result = transformer.parseDsl(dsl);
if (result.IsFailure()) {
Expand All @@ -39,7 +50,7 @@ public static void validate(String dsl, ValidationOptions options) throws DslErr
var authorizationModel = result.getAuthorizationModel();
var lines = dsl.split("\n");

new DslValidator(options, authorizationModel, lines).validate();
new ModelValidator(options, authorizationModel, lines).validate();
}

private void validate() throws DslErrorsException {
Expand All @@ -53,20 +64,30 @@ private void validate() throws DslErrorsException {
errors.raiseSchemaVersionRequired(0, "");
}

if (schemaVersion != null && schemaVersion.equals("1.1")) {
if (schemaVersion != null && (schemaVersion.equals("1.1") || schemaVersion.equals("1.2"))) {
modelValidation();
} else if (schemaVersion != null) {
var lineIndex = dsl.getSchemaLineNumber(schemaVersion);
errors.raiseInvalidSchemaVersion(lineIndex, schemaVersion);
}

for (Map.Entry<String, Set<String>> entry : fileToModules.entrySet()) {
String file = entry.getKey();
Set<String> modules = entry.getValue();
if (modules.size() > 1) {
errors.raiseMultipleModulesInSingleFile(file, modules);
}
}

errors.throwIfNotEmpty();
}

private void populateRelations() {
authorizationModel.getTypeDefinitions().forEach(typeDef -> {
var typeName = typeDef.getType();

trackModulesInFile(typeDef.getMetadata());

if (typeName.equals(Keyword.SELF) || typeName.equals(Keyword.THIS)) {
var lineIndex = dsl.getTypeLineNumber(typeName);
errors.raiseReservedTypeName(lineIndex, typeName);
Expand Down Expand Up @@ -158,10 +179,15 @@ private void modelValidation() {
if (errors.isEmpty()) {
authorizationModel.getTypeDefinitions().forEach(typeDef -> {
var typeName = typeDef.getType();
var currentRelations = typeMap.get(typeName).getRelations();
var typeDefMetadata = typeDef.getMetadata();
var typeDefRelationsMetadata = getNullSafe(typeMap.get(typeName).getMetadata(), Metadata::getRelations);
for (var relationName : typeDef.getRelations().keySet()) {
var currentRelations = typeMap.get(typeName).getRelations();
var result = EntryPointOrLoop.compute(
typeMap, typeName, relationName, currentRelations.get(relationName), new HashMap<>());

trackModulesInFile(typeDefMetadata, typeDefRelationsMetadata.get(relationName));

if (!result.hasEntry()) {
var typeIndex = dsl.getTypeLineNumber(typeName);
var lineIndex = dsl.getRelationLineNumber(relationName, typeIndex);
Expand All @@ -176,6 +202,12 @@ private void modelValidation() {
}

authorizationModel.getConditions().forEach((conditionName, condition) -> {
trackModulesInFile(condition.getMetadata());

if (!conditionName.equals(condition.getName())) {
errors.raiseDifferentNestedConditionName(conditionName, condition.getName());
}

if (!usedConditionNamesSet.contains(conditionName)) {
var conditionIndex = dsl.getConditionLineNumber(conditionName);
errors.raiseUnusedCondition(conditionIndex, conditionName);
Expand Down Expand Up @@ -424,4 +456,50 @@ private static boolean isRelationSingle(Userset currentRelation) {
&& currentRelation.getIntersection() == null
&& currentRelation.getDifference() == null;
}

private void trackModulesInFile(Metadata metadata) {
if (metadata == null) {
return;
}

var sourceInfo = metadata.getSourceInfo();
var module = metadata.getModule();
trackModulesInFile(module, sourceInfo);
}

private void trackModulesInFile(Metadata metadata, RelationMetadata relationMetadata) {

String module = null;
SourceInfo sourceInfo = null;
if (relationMetadata != null) {
module = relationMetadata.getModule();
sourceInfo = relationMetadata.getSourceInfo();
}

if (module == null) {
module = metadata.getModule();
sourceInfo = metadata.getSourceInfo();
}

trackModulesInFile(module, sourceInfo);
}

private void trackModulesInFile(ConditionMetadata metadata) {
if (metadata == null) {
return;
}

var sourceInfo = metadata.getSourceInfo();
var module = metadata.getModule();
trackModulesInFile(module, sourceInfo);
}

private void trackModulesInFile(String module, SourceInfo sourceInfo) {
if (module == null || sourceInfo == null) {
return;
}
fileToModules
.computeIfAbsent(sourceInfo.getFile(), t -> new LinkedHashSet<>())
.add(module);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

class ValidationErrorsBuilder {
Expand All @@ -22,21 +23,26 @@ private ErrorProperties buildErrorProperties(String message, int lineIndex, Stri
private ErrorProperties buildErrorProperties(
String message, int lineIndex, String symbol, WordResolver wordResolver) {

var rawLine = lines[lineIndex];
var regex = Pattern.compile("\\b" + symbol + "\\b");
var wordIdx = 0;
var matcher = regex.matcher(rawLine);
if (matcher.find()) {
wordIdx = matcher.start();
}
var properties = new ErrorProperties(null, null, message);

if (lines != null) {
var rawLine = lines[lineIndex];
var regex = Pattern.compile("\\b" + symbol + "\\b");
var wordIdx = 0;
var matcher = regex.matcher(rawLine);
if (matcher.find()) {
wordIdx = matcher.start();
}

if (wordResolver != null) {
wordIdx = wordResolver.resolve(wordIdx, rawLine, symbol);
if (wordResolver != null) {
wordIdx = wordResolver.resolve(wordIdx, rawLine, symbol);
}

properties.setLine(new StartEnd(lineIndex, lineIndex));
properties.setColumn(new StartEnd(wordIdx, wordIdx + symbol.length()));
}

var line = new StartEnd(lineIndex, lineIndex);
var column = new StartEnd(wordIdx, wordIdx + symbol.length());
return new ErrorProperties(line, column, message);
return properties;
}

public void raiseSchemaVersionRequired(int lineIndex, String symbol) {
Expand Down Expand Up @@ -201,6 +207,23 @@ public void raiseUnusedCondition(int lineIndex, String symbol) {
errors.add(new ModelValidationSingleError(errorProperties, metadata));
}

public void raiseDifferentNestedConditionName(String conditionKey, String nestedConditionName) {
var message = "condition key is `" + conditionKey + "` but nested name property is " + nestedConditionName;
var errorProperties = buildErrorProperties(message, 0, nestedConditionName);
var metadata = new ValidationMetadata(
nestedConditionName, ValidationError.DifferentNestedConditionName, null, null, null);
errors.add(new ModelValidationSingleError(errorProperties, metadata));
}

public void raiseMultipleModulesInSingleFile(String file, Set<String> modules) {
var modulesString = String.join(", ", modules);
var message = "file " + file + " would contain multiple module definitions (" + modulesString
+ ") when transforming to DSL. Only one module can be defined per file.";
var errorProperties = buildErrorProperties(message, 0, file);
var metadata = new ValidationMetadata(file, ValidationError.MultipleModulesInFile, null, null, null);
errors.add(new ModelValidationSingleError(errorProperties, metadata));
}

public boolean isEmpty() {
return errors.isEmpty();
}
Expand Down
Loading

0 comments on commit a3958b8

Please sign in to comment.