Skip to main content

Examples (Java)

Schema-only check

// Allow creating any article (no conditions needed)
policy.can(create, article);

Condition-based check

// Check if the user can update THIS article (with conditions)
Article data = new Article(1, userId, "published");
Subject<Article> ref = article.wrap(data);
policy.can(update, ref);

Multiple conditions

new PolicyBuilder()
.allow(update, article, Map.of(
"$and", List.of(
Map.of("ownerId", userId),
Map.of("status", Map.of("$not", "archived"))
)
))
.build();

The same conditions, with Conditions

Conditions builds the same Map<String, Object> shape from method references, so a typo in a field name is caught by the compiler instead of failing silently at evaluation time:

new PolicyBuilder()
.allow(update, article, Conditions.and(
Conditions.eq(Article::getOwnerId, userId),
Conditions.ne(Article::getStatus, "archived")
))
.build();

Field mappers for renamed or computed fields

A SubjectFieldMapper resolves a condition field through an explicit getter instead of reflection — handy when a policy-facing field name doesn't match the instance's own shape, like flattening a nested post.getAuthor().getName() into a single top-level authorName field (recall conditions can only narrow one field deep — see Condition Operators):

SubjectFieldMapper<Post> authorNameMapper = SubjectFieldMapper.<Post>builder()
.field("authorName", (post) -> post.getAuthor().getName())
.build();

Subject<Post> post = SubjectFactory.create("Post", authorNameMapper);
Action<String> read = ActionFactory.create("Read");

// `Conditions`'s getter-based helpers can't reference "authorName" — it has
// no real Post.getAuthorName(), only a mapper entry — so build this one
// as a plain Map, keyed by the mapped field's synthetic name:
Policy policy = new PolicyBuilder()
.allow(read, post, Map.of("authorName", "Alice")) // resolved via the mapper
.build();

A field the mapper doesn't define still falls back to ordinary reflection, and a mapped field's own value can still use any non-field operator ($substr, $in, ...) — only narrowing is restricted to one level, not operator use.

KeycardConfig

Bundle actions/subjects/operators/field mappers into one object shared by PolicyBuilder and Policy, instead of keeping each in sync by hand:

SubjectFieldMapperCatalog mappers = SubjectFieldMapperCatalog.builder()
.register("Post", authorNameMapper)
.build();

KeycardConfig config = KeycardConfig.builder()
.subject(post)
.mapper(mappers)
.build();

PolicyDefinition def = new PolicyBuilder(config)
.allow(read, post, Map.of("authorName", "Alice"))
.buildDef();

// def.getMeta().getSubjects() includes "Post" even though no allow()/deny()
// call above needed to declare it separately.
Policy policy = Policy.from(def, config);

Custom error handling

try {
policy.require(delete, article);
} catch (PolicyException e) {
logger.warn("Access denied: {}", e.getMessage());
sendError(403, "You do not have permission to delete this article");
}

Cross-language usage

Policies can be serialized with Gson and shared across languages:

// Build policy in Java
Policy policy = new PolicyBuilder()
.allow(create, article)
.build();

// Serialize
Gson gson = new Gson();
String json = gson.toJson(policy.getDefinition());

// Can be loaded in JavaScript, Rust, etc.

Building and testing

Build with Maven:

mvn clean package
mvn test