Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cherry-pick #202 #208

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -14,11 +14,12 @@
* limitations under the License.
*/

use cedar_policy::entities_errors::EntitiesError;
#[cfg(feature = "partial-eval")]
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 @@ -28,6 +29,7 @@ use jni::{
};
use jni_fn::jni_fn;
use serde::{Deserialize, Serialize};
use serde_json::{from_str, Value};
use std::{error::Error, str::FromStr, thread};

use crate::{
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