Skip to content
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
13 changes: 13 additions & 0 deletions CedarJava/src/main/java/com/cedarpolicy/AuthorizationEngine.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import com.cedarpolicy.model.exception.AuthException;
import com.cedarpolicy.model.exception.BadRequestException;
import com.cedarpolicy.model.policy.PolicySet;
import com.cedarpolicy.model.LevelValidationRequest;

/**
* Implementations of the AuthorizationEngine interface invoke Cedar to respond to an authorization
Expand Down Expand Up @@ -120,6 +121,18 @@ PartialAuthorizationResponse isAuthorizedPartial(PartialAuthorizationRequest req
*/
ValidationResponse validate(ValidationRequest request) throws AuthException;

/**
* Asks whether the policies in the given {@link LevelValidationRequest} <code>q</code> are correct
* when validated against the schema it describes. If validation passes, run level validation (RFC 76)
*
* @param request The request containing the policies to validate, the schema to validate them
* against and maximum dereferencing level.
* @return A {@link ValidationResponse} describing any validation errors found in the policies.
* @throws BadRequestException if any errors were found in the syntax of the policies.
* @throws AuthException if any internal errors occurred while validating the policies.
*/
ValidationResponse validateWithLevel(LevelValidationRequest request) throws AuthException;

/**
* Asks whether the entities in the given {@link EntityValidationRequest} <code>q</code> are correct
* when validated against the schema it describes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.cedarpolicy.loader.LibraryLoader;
import com.cedarpolicy.model.AuthorizationResponse;
import com.cedarpolicy.model.EntityValidationRequest;
import com.cedarpolicy.model.LevelValidationRequest;
import com.cedarpolicy.model.PartialAuthorizationResponse;
import com.cedarpolicy.model.ValidationRequest;
import com.cedarpolicy.model.ValidationResponse;
Expand Down Expand Up @@ -57,7 +58,7 @@ public BasicAuthorizationEngine() {

@Override
public AuthorizationResponse isAuthorized(com.cedarpolicy.model.AuthorizationRequest q,
PolicySet policySet, Set<Entity> entities) throws AuthException {
PolicySet policySet, Set<Entity> entities) throws AuthException {
final AuthorizationRequest request = new AuthorizationRequest(q, policySet, entities);
return call("AuthorizationOperation", AuthorizationResponse.class, request);
}
Expand All @@ -67,14 +68,14 @@ public AuthorizationResponse isAuthorized(com.cedarpolicy.model.AuthorizationReq
*/
@Override
public AuthorizationResponse isAuthorized(com.cedarpolicy.model.AuthorizationRequest q,
PolicySet policySet, Entities entities) throws AuthException {
PolicySet policySet, Entities entities) throws AuthException {
return isAuthorized(q, policySet, entities.getEntities());
}

@Experimental(ExperimentalFeature.PARTIAL_EVALUATION)
@Override
public PartialAuthorizationResponse isAuthorizedPartial(com.cedarpolicy.model.PartialAuthorizationRequest q,
PolicySet policySet, Set<Entity> entities) throws AuthException {
PolicySet policySet, Set<Entity> entities) throws AuthException {
try {
final PartialAuthorizationRequest request = new PartialAuthorizationRequest(q, policySet, entities);
return call("AuthorizationPartialOperation", PartialAuthorizationResponse.class, request);
Expand All @@ -93,7 +94,7 @@ public PartialAuthorizationResponse isAuthorizedPartial(com.cedarpolicy.model.Pa
@Experimental(ExperimentalFeature.PARTIAL_EVALUATION)
@Override
public PartialAuthorizationResponse isAuthorizedPartial(com.cedarpolicy.model.PartialAuthorizationRequest q,
PolicySet policySet, Entities entities) throws AuthException {
PolicySet policySet, Entities entities) throws AuthException {
return isAuthorizedPartial(q, policySet, entities.getEntities());
}

Expand All @@ -102,6 +103,11 @@ public ValidationResponse validate(ValidationRequest q) throws AuthException {
return call("ValidateOperation", ValidationResponse.class, q);
}

@Override
public ValidationResponse validateWithLevel(LevelValidationRequest q) throws AuthException {
return call("ValidateWithLevelOperation", ValidationResponse.class, q);
}

@Override
public void validateEntities(EntityValidationRequest q) throws AuthException {
EntityValidationResponse entityValidationResponse = call("ValidateEntities", EntityValidationResponse.class, q);
Expand Down Expand Up @@ -199,12 +205,12 @@ private static final class PartialAuthorizationRequest extends com.cedarpolicy.m

PartialAuthorizationRequest(com.cedarpolicy.model.PartialAuthorizationRequest request, PolicySet policySet, Set<Entity> entities) {
super(
request.principal,
request.action,
request.resource,
request.context,
request.schema,
request.enableRequestValidation);
request.principal,
request.action,
request.resource,
request.context,
request.schema,
request.enableRequestValidation);
this.policies = policySet;
this.entities = entities;
}
Expand Down
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.model;

import com.cedarpolicy.model.schema.Schema;
import com.cedarpolicy.model.policy.PolicySet;
import com.fasterxml.jackson.annotation.JsonProperty;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

import java.util.Objects;

/** Information passed to Cedar for level validation. */
public final class LevelValidationRequest {
private final Schema schema;
private final PolicySet policies;
private final long maxDerefLevel; // Must be non-negative (>=0)

/**
* Construct a validation request.
*
* @param schema Schema for the request
* @param policies Map of Policy ID to policy
* @param maxDerefLevel Maximum level of dereferencing allowed for validation. Must be non-negative (>=0)
*/
@SuppressFBWarnings
public LevelValidationRequest(Schema schema, PolicySet policies, long maxDerefLevel) {
if (schema == null) {
throw new NullPointerException("schema");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit as an aside: Looks like this is how we do it in most places, but here it looks like we print it differently.

I wonder what our appetite for using something like Lombok to improve boilerplate like this and getters/setters is: https://projectlombok.org/features/NonNull

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, there is some inconsistency in the message. I followed the message that we use in ValidationRequest constructor.

Maybe we can do a Lombok "campaign" to avoid inconsistency across CedarJava before the release.

}

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

if (maxDerefLevel < 0) {
throw new IllegalArgumentException("maxDerefLevel must be non-negative");
}

this.schema = schema;
this.policies = policies;
this.maxDerefLevel = maxDerefLevel;
}

/**
* Get the schema.
*
* @return The schema.
*/
public Schema getSchema() {
return this.schema;
}

/**
* Get the policy set.
*
* @return A `PolicySet` object
*/
@JsonProperty("policies")
public PolicySet getPolicySet() {
return this.policies;
}

/**
* Get the maximum deref level.
*
* @return The maximum deref level value for validation
*/
public long getMaxDerefLevel() {
return this.maxDerefLevel;
}

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

final LevelValidationRequest other = (LevelValidationRequest) o;
return schema.equals(other.schema) && policies.equals(other.policies) && maxDerefLevel == other.maxDerefLevel;
}

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

/** Get readable string representation. */
public String toString() {
return "ValidationRequest(schema=" + schema + ", policies=" + policies + ", maxDerefLevel=" + maxDerefLevel
+ ")";
}
}
82 changes: 72 additions & 10 deletions CedarJava/src/test/java/com/cedarpolicy/ValidationTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.cedarpolicy.model.DetailedError;
import com.cedarpolicy.model.LevelValidationRequest;
import com.cedarpolicy.model.ValidationRequest;
import com.cedarpolicy.model.ValidationResponse;
import com.cedarpolicy.model.ValidationResponse.SuccessOrFailure;
Expand Down Expand Up @@ -56,6 +57,8 @@ public void givenEmptySchemaAndNoPolicyReturnsValid() {
givenSchema(EMPTY_SCHEMA);
ValidationResponse response = whenValidated();
thenIsValid(response);
ValidationResponse levelResponse = whenLevelValidated(1);
thenIsValid(levelResponse);
}

/** Test. */
Expand All @@ -71,6 +74,8 @@ public void givenExampleSchemaAndCorrectPolicyReturnsValid() {
+ ");");
ValidationResponse response = whenValidated();
thenIsValid(response);
ValidationResponse levelResponse = whenLevelValidated(1);
thenIsValid(levelResponse);
}

/** Test. */
Expand All @@ -86,6 +91,8 @@ public void givenExampleSchemaAndIncorrectPolicyReturnsValid() {
+ ");");
ValidationResponse response = whenValidated();
thenIsNotValid(response);
ValidationResponse levelResponse = whenLevelValidated(1);
thenIsNotValid(levelResponse);
}

/** Test. */
Expand All @@ -95,6 +102,8 @@ public void givenInvalidPolicyThrowsBadRequestError() {
givenPolicy("policy0", "permit { }");
ValidationResponse response = whenValidated();
thenValidationFailed(response);
ValidationResponse levelResponse = whenLevelValidated(1);
thenValidationFailed(levelResponse);
}

@Test
Expand All @@ -119,6 +128,8 @@ public void validateTemplateLinkedPolicySuccessTest() {
this.policies = new PolicySet(new HashSet<>(), templates, templateLinks);
ValidationResponse response = whenValidated();
thenIsValid(response);
ValidationResponse levelResponse = whenLevelValidated(1);
thenIsValid(levelResponse);
}

@Test
Expand All @@ -136,34 +147,79 @@ public void validateTemplateLinkedPolicyFailsWhenExpected() {
// fails if we provide a value for the wrong slot
this.policies = new PolicySet(new HashSet<>(), templates,
List.of(new TemplateLink("template1", "policy", List.of(principalLink))));
ValidationResponse response2 = whenValidated();
thenValidationFailed(response2);
ValidationResponse response1 = whenValidated();
thenValidationFailed(response1);
ValidationResponse levelResponse1 = whenLevelValidated(1);
thenValidationFailed(levelResponse1);

// fails if we provide a value for too many slots
this.policies = new PolicySet(new HashSet<>(), templates,
List.of(new TemplateLink("template2", "policy", List.of(principalLink, resourceLink))));
ValidationResponse response3 = whenValidated();
thenValidationFailed(response3);
ValidationResponse response2 = whenValidated();
thenValidationFailed(response2);
ValidationResponse levelResponse2 = whenLevelValidated(1);
thenValidationFailed(levelResponse2);

// fails if we don't provide a value for all slots
this.policies = new PolicySet(new HashSet<>(), templates,
List.of(new TemplateLink("template0", "policy", List.of(resourceLink))));
ValidationResponse response4 = whenValidated();
thenValidationFailed(response4);
ValidationResponse response3 = whenValidated();
thenValidationFailed(response3);
ValidationResponse levelResponse3 = whenLevelValidated(1);
thenValidationFailed(levelResponse3);


// validation returns an error if we provide a link with the wrong type
LinkValue badLink1 = new LinkValue("?resource", EntityUID.parse("Library::User::\"Victor\"").get());
this.policies = new PolicySet(new HashSet<>(), templates,
List.of(new TemplateLink("template1", "policy", List.of(badLink1))));
ValidationResponse response5 = whenValidated();
thenIsNotValid(response5);
ValidationResponse response4 = whenValidated();
thenIsNotValid(response4);
ValidationResponse levelResponse4 = whenLevelValidated(1);
thenIsNotValid(levelResponse4);

// validation returns an error if we provide a link with an invalid type
LinkValue badLink2 = new LinkValue("?resource", EntityUID.parse("Library::BOOK::\"The black Swan\"").get());
this.policies = new PolicySet(new HashSet<>(), templates,
List.of(new TemplateLink("template1", "policy", List.of(badLink2))));
ValidationResponse response6 = whenValidated();
thenIsNotValid(response6);
ValidationResponse response5 = whenValidated();
thenIsNotValid(response5);
ValidationResponse levelResponse5 = whenLevelValidated(1);
thenIsNotValid(levelResponse5);
}

@Test
public void validateLevelPolicySuccessTest() {
givenSchema(LEVEL_SCHEMA);
givenPolicy(
"policy0",
"permit(\n"
+ " principal in UserGroup::\"alice_friends\",\n"
+ " action == Action::\"viewPhoto\",\n"
+ " resource\n"
+ ") when {principal in resource.owner.friend};");

ValidationResponse response = whenValidated();
thenIsValid(response);
ValidationResponse levelResponse = whenLevelValidated(2);
thenIsValid(levelResponse);
}

@Test
public void validateLevelPolicyFailsWhenExpected() {
givenSchema(LEVEL_SCHEMA);
givenPolicy(
"policy0",
"permit(\n"
+ " principal in UserGroup::\"alice_friends\",\n"
+ " action == Action::\"viewPhoto\",\n"
+ " resource\n"
+ ") when {principal in resource.owner.friend};");

ValidationResponse response = whenValidated();
thenIsValid(response);
ValidationResponse levelResponse = whenLevelValidated(1);
thenIsNotValid(levelResponse);
}

private void givenSchema(Schema testSchema) {
Expand All @@ -180,6 +236,11 @@ private ValidationResponse whenValidated() {
return assertDoesNotThrow(() -> engine.validate(request));
}

private ValidationResponse whenLevelValidated(long maxDerefLevel) {
LevelValidationRequest request = new LevelValidationRequest(schema, policies, maxDerefLevel);
return assertDoesNotThrow(() -> engine.validateWithLevel(request));
}

private void thenIsValid(ValidationResponse response) {
assertEquals(response.type, SuccessOrFailure.Success);
final ValidationSuccessResponse success = assertDoesNotThrow(() -> response.success.get());
Expand Down Expand Up @@ -223,4 +284,5 @@ private void reset() {
private static final Schema EMPTY_SCHEMA = loadSchemaResource("/empty_schema.json");
private static final Schema PHOTOFLASH_SCHEMA = loadSchemaResource("/photoflash_schema.json");
private static final Schema LIBRARY_SCHEMA = loadSchemaResource("/library_schema.json");
private static final Schema LEVEL_SCHEMA = loadSchemaResource("/level_schema.json");
}
Loading