Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
17 changes: 17 additions & 0 deletions cedar-policy-core/src/ast/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,14 @@ pub trait RequestSchema {
request: &Request,
extensions: &Extensions<'_>,
) -> Result<(), Self::Error>;

/// Validate the given `context`, returning `Err` if it fails validation
fn validate_context<'a>(
&self,
context: &Context,
action: &EntityUID,
extensions: &Extensions<'a>,
) -> std::result::Result<(), Self::Error>;
}

/// A `RequestSchema` that does no validation and always reports a passing result
Expand All @@ -626,6 +634,15 @@ impl RequestSchema for RequestSchemaAllPass {
) -> Result<(), Self::Error> {
Ok(())
}

fn validate_context<'a>(
&self,
_context: &Context,
_action: &EntityUID,
_extensions: &Extensions<'a>,
) -> std::result::Result<(), Self::Error> {
Ok(())
}
}

/// Wrapper around `std::convert::Infallible` which also implements
Expand Down
61 changes: 45 additions & 16 deletions cedar-policy-core/src/validator/coreschema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,22 +231,8 @@ impl ast::RequestSchema for ValidatorSchema {
validator_action_id.check_resource_type(principal_type, action)?;
}
if let Some(context) = request.context() {
validate_euids_in_partial_value(
&CoreSchema::new(self),
&context.clone().into(),
)
.map_err(RequestValidationError::InvalidEnumEntity)?;
let expected_context_ty = validator_action_id.context_type();
if !expected_context_ty
.typecheck_partial_value(&context.clone().into(), extensions)
.map_err(RequestValidationError::TypeOfContext)?
{
return Err(request_validation_errors::InvalidContextError {
context: context.clone(),
action: Arc::clone(action),
}
.into());
}
self.validate_context(context, action, extensions)
.map_err(RequestValidationError::from)?;
}
}
EntityUIDEntry::Unknown { .. } => {
Expand All @@ -260,6 +246,39 @@ impl ast::RequestSchema for ValidatorSchema {
}
Ok(())
}

/// Validate a context against a schema for a specific action
fn validate_context<'a>(
&self,
context: &ast::Context,
action: &ast::EntityUID,
extensions: &Extensions<'a>,
) -> std::result::Result<(), RequestValidationError> {
// Get the action ID
let validator_action_id =
self.get_action_id(action)
.ok_or_else(|| request_validation_errors::UndeclaredActionError {
action: Arc::new(action.clone()),
})?;

// Validate entity UIDs in the context
validate_euids_in_partial_value(&CoreSchema::new(&self), &context.clone().into())
.map_err(RequestValidationError::InvalidEnumEntity)?;

// Typecheck the context against the expected context type
let expected_context_ty = validator_action_id.context_type();
if !expected_context_ty
.typecheck_partial_value(&context.clone().into(), extensions)
.map_err(RequestValidationError::TypeOfContext)?
{
return Err(request_validation_errors::InvalidContextError {
context: context.clone(),
action: Arc::new(action.clone()),
}
.into());
}
Ok(())
}
}

impl ValidatorActionId {
Expand Down Expand Up @@ -305,6 +324,16 @@ impl ast::RequestSchema for CoreSchema<'_> {
) -> Result<(), Self::Error> {
self.schema.validate_request(request, extensions)
}

/// Validate the given `context`, returning `Err` if it fails validation
fn validate_context<'a>(
&self,
context: &ast::Context,
action: &EntityUID,
extensions: &Extensions<'a>,
) -> std::result::Result<(), Self::Error> {
self.schema.validate_context(context, action, extensions)
}
}

/// Error when the request does not conform to the schema.
Expand Down
22 changes: 21 additions & 1 deletion cedar-policy/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ pub use ast::Effect;
pub use authorizer::Decision;
#[cfg(feature = "partial-eval")]
use cedar_policy_core::ast::BorrowedRestrictedExpr;
use cedar_policy_core::ast::{self, RestrictedExpr};
use cedar_policy_core::ast::{self, RequestSchema, RestrictedExpr};
use cedar_policy_core::authorizer;
use cedar_policy_core::entities::{ContextSchema, Dereference};
use cedar_policy_core::est::{self, TemplateLink};
Expand Down Expand Up @@ -4688,6 +4688,26 @@ impl Context {
) -> Result<Self, ContextCreationError> {
Self::from_pairs(self.into_iter().chain(other_context))
}

/// Validates this context against the provided schema
///
/// Returns Ok(()) if the context is valid according to the schema, or an error otherwise
///
/// This validation is already handled by `Request::new`, so there is no need to separately call
/// if you are validating the whole request
pub fn validate(
&self,
schema: &crate::Schema,
action: &EntityUid,
) -> std::result::Result<(), RequestValidationError> {
// Call the validate_context function from coreschema.rs
Ok(RequestSchema::validate_context(
&schema.0,
&self.0,
action.as_ref(),
Extensions::all_available(),
)?)
}
}

/// Utilities for implementing `IntoIterator` for `Context`
Expand Down
106 changes: 105 additions & 1 deletion cedar-policy/src/ffi/check_parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,21 @@ pub fn check_parse_context(call: ContextParsingCall) -> CheckParseAnswer {
};
}
};
call.context.parse(schema.as_ref(), action.as_ref()).into()

let parse_result = call.context.parse(schema.as_ref(), action.as_ref());

// Check if the parsed context is valid
if let Ok(context) = &parse_result {
if let (Some(schema_ref), Some(action_ref)) = (&schema, &action) {
if let Err(err) = context.validate(schema_ref, action_ref) {
return CheckParseAnswer::Failure {
errors: vec![miette::Report::msg(err).into()],
};
}
}
}
// Return the parse result if all other checks pass
parse_result.into()
}

/// Check whether a context successfully parses. Input is a JSON encoding of
Expand Down Expand Up @@ -534,4 +548,94 @@ mod test {
let errs = assert_check_parse_is_err(&answer);
assert_exactly_one_error(errs, "while parsing context, expected the record to have an attribute `referrer`, but it does not", None);
}

#[test]
fn check_parse_context_fails_for_invalid_context_type() {
let call = json!({
"context": {
"authenticated": "foo"
},
"action": {
"type": "PhotoApp::Action",
"id": "viewPhoto"
},
"schema": {
"PhotoApp": {
"commonTypes": {
"PersonType": {
"type": "Record",
"attributes": {
"age": {
"type": "Long"
},
"name": {
"type": "String"
}
}
},
"ContextType": {
"type": "Record",
"attributes": {
"ip": {
"type": "Extension",
"name": "ipaddr",
"required": false
},
"authenticated": {
"type": "Boolean",
"required": true
}
}
}
},
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {}
},
"memberOfTypes": [
"UserGroup"
]
},
"UserGroup": {
"shape": {
"type": "Record",
"attributes": {}
}
},
"Photo": {
"shape": {
"type": "Record",
"attributes": {}
},
}
},
"actions": {
"viewPhoto": {
"appliesTo": {
"principalTypes": [
"User",
"UserGroup"
],
"resourceTypes": [
"Photo"
],
"context": {
"type": "ContextType"
}
}
}
}
}
}
});
let answer = serde_json::from_value(check_parse_context_json(call).unwrap()).unwrap();
let errs = assert_check_parse_is_err(&answer);
assert_exactly_one_error(
errs,
"context `{authenticated: \"foo\"}` is not valid for `PhotoApp::Action::\"viewPhoto\"`",
None,
);
}
}
Loading