Skip to content

Commit

Permalink
Add support for cedar entities validation.
Browse files Browse the repository at this point in the history
Signed-off-by: Mohamed Amine Ouali <mdamine@amazon.com>
  • Loading branch information
amzn-mdamine committed Sep 12, 2024
1 parent 41536ce commit 3a9208a
Show file tree
Hide file tree
Showing 7 changed files with 733 additions and 1 deletion.
11 changes: 11 additions & 0 deletions CedarJava/src/main/java/com/cedarpolicy/AuthorizationEngine.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,17 @@ PartialAuthorizationResponse isAuthorizedPartial(PartialAuthorizationRequest req
*/
ValidationResponse validate(ValidationRequest request) throws AuthException;

/**
* Asks whether the entities in the given {@link EntityValidationRequest} <code>q</code> are correct
* when validated against the schema it describes.
*
* @param request The request containing the entities to validate and the schema to validate them
* against.
* @throws BadRequestException if any errors were found in the syntax of the entities.
* @throws AuthException if any internal errors occurred while validating the entities.
*/
void validateEntities(EntityValidationRequest request) throws AuthException;

/**
* Get the Cedar language major version (e.g., "1.2") used by this CedarJava library.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,20 @@
import com.cedarpolicy.loader.LibraryLoader;
import com.cedarpolicy.model.*;
import com.cedarpolicy.model.exception.AuthException;
import com.cedarpolicy.model.exception.BadRequestException;
import com.cedarpolicy.model.exception.InternalException;
import com.cedarpolicy.model.exception.MissingExperimentalFeatureException;
import com.cedarpolicy.model.entity.Entity;
import com.cedarpolicy.model.policy.PolicySet;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

import java.util.List;
import java.util.Set;

/** An authorization engine that is compiled in process. Communicated with via JNI. */
Expand Down Expand Up @@ -73,6 +78,18 @@ public ValidationResponse validate(ValidationRequest q) throws AuthException {
return call("ValidateOperation", ValidationResponse.class, q);
}

@Override
public void validateEntities(EntityValidationRequest q) throws AuthException {
EntityValidationResponse entityValidationResponse = call("ValidateEntities", EntityValidationResponse.class, q);
if (!entityValidationResponse.success) {
if (entityValidationResponse.isInternal) {
throw new InternalException(entityValidationResponse.errors.toArray(new String[0]));
} else {
throw new BadRequestException(entityValidationResponse.errors.toArray(new String[0]));
}
}
}

private static <REQ, RESP> RESP call(String operation, Class<RESP> responseClass, REQ request)
throws AuthException {
try {
Expand Down Expand Up @@ -100,6 +117,40 @@ private static <REQ, RESP> RESP call(String operation, Class<RESP> responseClass
}
}

/**
* The result of processing an EntityValidationRequest.
*/
@JsonIgnoreProperties({"result"}) // Ignore only the 'result' field
private static final class EntityValidationResponse {

/** A string that indicates if the operation was successful.*/
private final boolean success;

/** A boolean flag that indicates whether the error is internal.*/
private final boolean isInternal;

/** A list of error messages encountered during the operation.*/
private final List<String> errors;

/**
* Parameterized constructor for initializing all fields of EntityValidationResponse.
*
* @param success A boolean indicating success status.
* @param isInternal A boolean indicating if the error is internal.
* @param errors A list of error messages.
*/
@JsonCreator
@SuppressFBWarnings
EntityValidationResponse(
@JsonProperty("success") boolean success,
@JsonProperty("isInternal") boolean isInternal,
@JsonProperty("errors") List<String> errors) {
this.success = success;
this.isInternal = isInternal;
this.errors = errors;
}
}

private static class AuthorizationRequest extends com.cedarpolicy.model.AuthorizationRequest {
@JsonProperty private final PolicySet policies;
@JsonProperty private final Set<Entity> entities;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright Cedar Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.cedarpolicy.model;

import java.util.List;
import java.util.Objects;

import com.cedarpolicy.model.entity.Entity;
import com.cedarpolicy.model.schema.Schema;
import com.fasterxml.jackson.annotation.JsonProperty;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

/**
* Information passed to Cedar for entities validation.
*/
public final class EntityValidationRequest {
@JsonProperty("schema")
private final Schema schema;
@JsonProperty("entities")
private final List<Entity> entities;

/**
* Construct a validation request.
*
* @param schema Schema for the request
* @param entities Map.
*/
@SuppressFBWarnings
public EntityValidationRequest(Schema schema, List<Entity> entities) {
if (schema == null) {
throw new NullPointerException("schema");
}

if (entities == null) {
throw new NullPointerException("entities");
}

this.schema = schema;
this.entities = entities;
}

/**
* Test equality.
*/
@Override
public boolean equals(final Object o) {
if (!(o instanceof EntityValidationRequest)) {
return false;
}

final EntityValidationRequest other = (EntityValidationRequest) o;
return schema.equals(other.schema) && entities.equals(other.entities);
}

/**
* Hash.
*/
@Override
public int hashCode() {
return Objects.hash(schema, entities);
}

/**
* Get readable string representation.
*/
public String toString() {
return "EntityValidationRequest(schema=" + schema + ", entities=" + entities + ")";
}
}
109 changes: 109 additions & 0 deletions CedarJava/src/test/java/com/cedarpolicy/EntityValidationTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright Cedar Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.cedarpolicy;

import static com.cedarpolicy.TestUtil.loadSchemaResource;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.BeforeAll;

import org.junit.jupiter.api.Test;

import java.util.List;


import com.cedarpolicy.model.EntityValidationRequest;
import com.cedarpolicy.model.entity.Entity;
import com.cedarpolicy.model.exception.AuthException;
import com.cedarpolicy.model.exception.BadRequestException;
import com.cedarpolicy.model.schema.Schema;
import com.cedarpolicy.pbt.EntityGen;
import com.cedarpolicy.value.EntityTypeName;
import com.cedarpolicy.value.PrimBool;


/**
* Tests for entity validator
*/
public class EntityValidationTests {
private static EntityGen entityGen;
private static AuthorizationEngine engine;

/**
* Test that a valid entity is accepted.
*/
@Test
public void testValidEntity() throws AuthException {
Entity entity = EntityValidationTests.entityGen.arbitraryEntity();

EntityValidationRequest r = new EntityValidationRequest(
ROLE_SCHEMA, List.of(entity));

engine.validateEntities(r);
}

/**
* Test that an entity with an attribute not specified in the schema throws an exception.
*/
@Test
public void testEntityWithUnknownAttribute() throws AuthException {
Entity entity = EntityValidationTests.entityGen.arbitraryEntity();
entity.attrs.put("test", new PrimBool(true));

EntityValidationRequest request = new EntityValidationRequest(ROLE_SCHEMA, List.of(entity));

BadRequestException exception = assertThrows(BadRequestException.class, () -> engine.validateEntities(request));

String errMsg = exception.getErrors().get(0);
assertTrue(errMsg.matches("attribute `test` on `Role::\".*\"` should not exist according to the schema"),
"Expected to match regex but was: '%s'".formatted(errMsg));
}

/**
* Test that entities with a cyclic parent relationship throw an exception.
*/
@Test
public void testEntitiesWithCyclicParentRelationship() throws AuthException {
// Arrange
Entity childEntity = EntityValidationTests.entityGen.arbitraryEntity();
Entity parentEntity = EntityValidationTests.entityGen.arbitraryEntity();

// Create a cyclic parent relationship between the entities
childEntity.parentsEUIDs.add(parentEntity.getEUID());
parentEntity.parentsEUIDs.add(childEntity.getEUID());

EntityValidationRequest request = new EntityValidationRequest(ROLE_SCHEMA, List.of(parentEntity, childEntity));

BadRequestException exception = assertThrows(BadRequestException.class, () -> engine.validateEntities(request));

String errMsg = exception.getErrors().get(0);
assertTrue(errMsg.matches("input graph has a cycle containing vertex `Role::\".*\"`"),
"Expected to match regex but was: '%s'".formatted(errMsg));
}

@BeforeAll
public static void setUp() {

engine = new BasicAuthorizationEngine();
EntityTypeName user = EntityTypeName.parse("Role").get();

EntityValidationTests.entityGen = new EntityGen(user);
}

private static final Schema ROLE_SCHEMA = loadSchemaResource("/role_schema.json");
}
12 changes: 12 additions & 0 deletions CedarJava/src/test/resources/role_schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"": {
"entityTypes": {
"Role": {
"memberOfTypes": [
"Role"
]
}
},
"actions": {}
}
}
43 changes: 42 additions & 1 deletion CedarJavaFFI/src/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
use cedar_policy::ffi::is_authorized_partial_json_str;
use cedar_policy::{
ffi::{is_authorized_json_str, validate_json_str},
EntityUid, Policy, PolicySet, Schema, Template,
Entities, EntityUid, Policy, PolicySet, Schema, Template,
};
use cedar_policy_formatter::{policies_str_to_pretty, Config};
use jni::{
Expand All @@ -29,6 +29,8 @@ use jni::{
use jni_fn::jni_fn;
use serde::{Deserialize, Serialize};
use std::{error::Error, str::FromStr, thread};
use cedar_policy::entities_errors::EntitiesError;
use serde_json::{from_str, Value};

use crate::{
answer::Answer,
Expand All @@ -43,6 +45,7 @@ const V0_AUTH_OP: &str = "AuthorizationOperation";
#[cfg(feature = "partial-eval")]
const V0_AUTH_PARTIAL_OP: &str = "AuthorizationPartialOperation";
const V0_VALIDATE_OP: &str = "ValidateOperation";
const V0_VALIDATE_ENTITIES: &str = "ValidateEntities";

fn build_err_obj(env: &JNIEnv<'_>, err: &str) -> jstring {
env.new_string(
Expand Down Expand Up @@ -115,6 +118,7 @@ pub(crate) fn call_cedar(call: &str, input: &str) -> String {
#[cfg(feature = "partial-eval")]
V0_AUTH_PARTIAL_OP => is_authorized_partial_json_str(input),
V0_VALIDATE_OP => validate_json_str(input),
V0_VALIDATE_ENTITIES => json_validate_entities(&input),
_ => {
let ires = Answer::fail_internally(format!("unsupported operation: {}", call));
serde_json::to_string(&ires)
Expand All @@ -125,6 +129,43 @@ pub(crate) fn call_cedar(call: &str, input: &str) -> String {
})
}

#[derive(Serialize, Deserialize)]
struct ValidateEntityCall {
schema: Value,
entities: Value,
}

pub fn json_validate_entities(input: &str) -> serde_json::Result<String> {
let ans = validate_entities(input)?;
serde_json::to_string(&ans)
}

/// public string-based JSON interface to be invoked by FFIs. Takes in a `ValidateEntityCall` and (if successful)
/// returns unit value () which is null value when serialized to json.
pub fn validate_entities(input: &str) -> serde_json::Result<Answer> {
let validate_entity_call = from_str::<ValidateEntityCall>(&input)?;
match Schema::from_json_value(validate_entity_call.schema) {
Err(e) => Ok(Answer::fail_bad_request(vec![e.to_string()])),
Ok(schema) => {
match Entities::from_json_value(validate_entity_call.entities, Some(&schema)) {
Err(error) => {
let err_message = match error {
EntitiesError::Serialization(err) => err.to_string(),
EntitiesError::Deserialization(err) => err.to_string(),
EntitiesError::Duplicate(err) => err.to_string(),
EntitiesError::TransitiveClosureError(err) => err.to_string(),
EntitiesError::InvalidEntity(err) => err.to_string(),
};
Ok(Answer::fail_bad_request(vec![err_message]))
},
Ok(_entities) => {
Ok(Answer::Success { result: "null".to_string() })
}
}
}
}
}

#[derive(Debug, Serialize, Deserialize)]
struct JavaInterfaceCall {
pub call: String,
Expand Down
Loading

0 comments on commit 3a9208a

Please sign in to comment.