use std::{collections::BTreeMap, sync::Arc};
use crate::ast::{EntityUIDEntry, RequestSchema};
use crate::entities::conformance::err::InvalidEnumEntityError;
use crate::tpe::err::{
InconsistentActionError, InconsistentPrincipalEidError, InconsistentPrincipalTypeError,
InconsistentResourceEidError, InconsistentResourceTypeError, NoMatchingReqEnvError,
RequestConsistencyError,
};
use crate::validator::request_validation_errors::{
UndeclaredActionError, UndeclaredPrincipalTypeError, UndeclaredResourceTypeError,
};
use crate::validator::{
types::RequestEnv, RequestValidationError, ValidationMode, ValidatorEntityTypeKind,
ValidatorSchema,
};
use crate::{
ast::{Context, Eid, EntityType, EntityUID, Request, Value},
entities::conformance::is_valid_enumerated_entity,
extensions::Extensions,
};
use smol_str::SmolStr;
#[derive(Debug, Clone)]
pub struct PartialEntityUID {
pub ty: EntityType,
pub eid: Option<Eid>,
}
#[derive(Debug)]
enum PartialEUIDConsistencyError {
Unknown,
InconsistentType(EntityType, EntityType),
InconsistentEid(Eid, Eid),
}
impl PartialEUIDConsistencyError {
pub fn into_resource_error(self) -> RequestConsistencyError {
match self {
PartialEUIDConsistencyError::Unknown => RequestConsistencyError::UnknownResource,
PartialEUIDConsistencyError::InconsistentType(partial, concrete) => {
InconsistentResourceTypeError { partial, concrete }.into()
}
PartialEUIDConsistencyError::InconsistentEid(partial, concrete) => {
InconsistentResourceEidError { partial, concrete }.into()
}
}
}
pub fn into_principal_error(self) -> RequestConsistencyError {
match self {
PartialEUIDConsistencyError::Unknown => RequestConsistencyError::UnknownPrincipal,
PartialEUIDConsistencyError::InconsistentType(partial, concrete) => {
InconsistentPrincipalTypeError { partial, concrete }.into()
}
PartialEUIDConsistencyError::InconsistentEid(partial, concrete) => {
InconsistentPrincipalEidError { partial, concrete }.into()
}
}
}
}
#[derive(Debug)]
enum PartialEUIDValidationError {
UndeclaredType(EntityType),
InvalidEnum(InvalidEnumEntityError),
}
impl PartialEUIDValidationError {
pub fn into_resource_error(self) -> RequestValidationError {
match self {
PartialEUIDValidationError::UndeclaredType(resource_ty) => {
UndeclaredResourceTypeError { resource_ty }.into()
}
PartialEUIDValidationError::InvalidEnum(enum_err) => enum_err.into(),
}
}
pub fn into_principal_error(self) -> RequestValidationError {
match self {
PartialEUIDValidationError::UndeclaredType(principal_ty) => {
UndeclaredPrincipalTypeError { principal_ty }.into()
}
PartialEUIDValidationError::InvalidEnum(enum_err) => enum_err.into(),
}
}
}
impl PartialEntityUID {
fn check_type(
&self,
schema: &ValidatorSchema,
uid: Option<&EntityUID>,
) -> Result<(), PartialEUIDValidationError> {
let entity_ty = schema
.get_entity_type(&self.ty)
.ok_or_else(|| PartialEUIDValidationError::UndeclaredType(self.ty.clone()))?;
if let (ValidatorEntityTypeKind::Enum(choices), Some(uid)) = (&entity_ty.kind, uid) {
is_valid_enumerated_entity(choices, uid)
.map_err(PartialEUIDValidationError::InvalidEnum)?;
}
Ok(())
}
fn validate(&self, schema: &ValidatorSchema) -> Result<(), PartialEUIDValidationError> {
self.check_type(schema, EntityUID::try_from(self.clone()).ok().as_ref())
}
fn check_consistency(&self, entry: &EntityUIDEntry) -> Result<(), PartialEUIDConsistencyError> {
let EntityUIDEntry::Known { euid, .. } = entry else {
return Err(PartialEUIDConsistencyError::Unknown);
};
if euid.entity_type() != &self.ty {
return Err(PartialEUIDConsistencyError::InconsistentType(
self.ty.clone(),
euid.entity_type().clone(),
));
}
if let Some(eid) = &self.eid {
if eid != euid.eid() {
return Err(PartialEUIDConsistencyError::InconsistentEid(
eid.clone(),
euid.eid().clone(),
));
}
}
Ok(())
}
}
impl TryFrom<PartialEntityUID> for EntityUID {
type Error = ();
fn try_from(value: PartialEntityUID) -> Result<EntityUID, ()> {
if let Some(eid) = value.eid {
Ok(EntityUID::from_components(value.ty, eid, None))
} else {
Err(())
}
}
}
impl From<EntityUID> for PartialEntityUID {
fn from(value: EntityUID) -> Self {
let (ty, eid) = value.components();
Self { ty, eid: Some(eid) }
}
}
#[derive(Debug, Clone)]
pub struct PartialRequest {
principal: PartialEntityUID,
action: EntityUID,
resource: PartialEntityUID,
context: Option<Arc<BTreeMap<SmolStr, Value>>>,
}
impl PartialRequest {
pub fn new(
principal: PartialEntityUID,
action: EntityUID,
resource: PartialEntityUID,
context: Option<Arc<BTreeMap<SmolStr, Value>>>,
schema: &ValidatorSchema,
) -> Result<Self, RequestValidationError> {
let req = Self {
principal,
action,
resource,
context,
};
req.validate(schema)?;
Ok(req)
}
pub(crate) fn find_request_env<'s>(
&self,
schema: &'s ValidatorSchema,
) -> Result<RequestEnv<'s>, NoMatchingReqEnvError> {
#[expect(
clippy::unwrap_used,
reason = "strict validation should produce concrete action entity uid"
)]
schema
.unlinked_request_envs(ValidationMode::Strict)
.find(|env| {
env.action_entity_uid().unwrap() == &self.action
&& env.principal_entity_type() == Some(&self.principal.ty)
&& env.resource_entity_type() == Some(&self.resource.ty)
})
.ok_or(NoMatchingReqEnvError)
}
pub(crate) fn validate(&self, schema: &ValidatorSchema) -> Result<(), RequestValidationError> {
if let Some(action_id) = schema.get_action_id(&self.action) {
action_id.check_principal_type(&self.principal.ty, &self.action.clone().into())?;
action_id.check_resource_type(&self.resource.ty, &self.action.clone().into())?;
self.principal
.validate(schema)
.map_err(|e| e.into_principal_error())?;
self.resource
.validate(schema)
.map_err(|e| e.into_resource_error())?;
if let Some(m) = &self.context {
schema.validate_context(
&Context::Value(m.clone()),
&self.action,
Extensions::all_available(),
)?;
}
Ok(())
} else {
Err(UndeclaredActionError {
action: self.action.clone().into(),
}
.into())
}
}
pub fn check_consistency(&self, request: &Request) -> Result<(), RequestConsistencyError> {
self.principal
.check_consistency(&request.principal)
.map_err(|e| e.into_principal_error())?;
self.resource
.check_consistency(&request.resource)
.map_err(|e| e.into_resource_error())?;
match &request.action {
EntityUIDEntry::Unknown { .. } => {
return Err(RequestConsistencyError::UnknownAction);
}
EntityUIDEntry::Known { euid, .. } => {
if euid.as_ref() != &self.action {
return Err(InconsistentActionError {
partial: self.action.clone(),
concrete: euid.as_ref().clone(),
}
.into());
}
}
}
match &request.context {
Some(Context::Value(c)) => {
if let Some(m) = &self.context {
if c != m {
return Err(RequestConsistencyError::InconsistentContext);
}
}
}
Some(Context::RestrictedResidual { .. }) => {
return Err(RequestConsistencyError::ConcreteContextContainsUnknowns);
}
None => {
return Err(RequestConsistencyError::UnknownContext);
}
}
Ok(())
}
pub fn principal_type(&self) -> &EntityType {
&self.principal.ty
}
pub fn resource_type(&self) -> &EntityType {
&self.resource.ty
}
pub fn principal(&self) -> &PartialEntityUID {
&self.principal
}
pub fn resource(&self) -> &PartialEntityUID {
&self.resource
}
pub fn action(&self) -> &EntityUID {
&self.action
}
pub fn context_attrs(&self) -> Option<&Arc<BTreeMap<SmolStr, Value>>> {
self.context.as_ref()
}
}
#[cfg(test)]
mod invalid_requests {
use std::{collections::BTreeMap, sync::Arc};
use crate::{
ast::Value,
extensions::Extensions,
test_utils::{expect_err, ExpectedErrorMessage, ExpectedErrorMessageBuilder},
tpe::request::PartialRequest,
tpe::test_utils::parse_partial_euid,
validator::ValidatorSchema,
};
#[track_caller]
fn schema() -> ValidatorSchema {
ValidatorSchema::from_cedarschema_str(
r#"
entity A enum ["foo"];
entity B;
entity C;
action a appliesTo {
principal: A,
resource: B,
context: {
"" : A,
}
};
action b appliesTo {
principal: B,
resource: A,
};
"#,
Extensions::all_available(),
)
.unwrap()
.0
}
#[track_caller]
fn expect_validation_err(
principal: &str,
action: &str,
resource: &str,
context: Option<Arc<BTreeMap<smol_str::SmolStr, Value>>>,
msg: &ExpectedErrorMessage<'_>,
) {
let err = PartialRequest::new(
parse_partial_euid(principal),
action.parse().unwrap(),
parse_partial_euid(resource),
context,
&schema(),
)
.expect_err("should fail to validate");
expect_err("", &miette::Report::new(err), msg);
}
#[test]
fn unknown_action() {
expect_validation_err(
"A",
r#"Action::"c""#,
"B",
None,
&ExpectedErrorMessageBuilder::error(
r#"request's action `Action::"c"` is not declared in the schema"#,
)
.exactly_one_underline(r#"Action::"c""#)
.build(),
);
}
#[test]
fn unknown_principal() {
expect_validation_err(
"D",
r#"Action::"a""#,
"B",
None,
&ExpectedErrorMessageBuilder::error(
r#"principal type `D` is not valid for `Action::"a"`"#,
)
.help(r#"valid principal types for `Action::"a"`: `A`"#)
.exactly_one_underline("D")
.build(),
);
}
#[test]
fn unknown_resource() {
expect_validation_err(
"A",
r#"Action::"a""#,
"D",
None,
&ExpectedErrorMessageBuilder::error(
r#"resource type `D` is not valid for `Action::"a"`"#,
)
.help(r#"valid resource types for `Action::"a"`: `B`"#)
.exactly_one_underline("D")
.build(),
);
}
#[test]
fn invalid_principal_for_action() {
expect_validation_err(
"C",
r#"Action::"a""#,
"B",
None,
&ExpectedErrorMessageBuilder::error(
r#"principal type `C` is not valid for `Action::"a"`"#,
)
.help(r#"valid principal types for `Action::"a"`: `A`"#)
.exactly_one_underline("C")
.build(),
);
}
#[test]
fn invalid_resource_for_action() {
expect_validation_err(
"A",
r#"Action::"a""#,
"C",
None,
&ExpectedErrorMessageBuilder::error(
r#"resource type `C` is not valid for `Action::"a"`"#,
)
.help(r#"valid resource types for `Action::"a"`: `B`"#)
.exactly_one_underline("C")
.build(),
);
}
#[test]
fn invalid_principal_enum() {
expect_validation_err(
r#"A::"bar""#,
r#"Action::"a""#,
"B",
None,
&ExpectedErrorMessageBuilder::error(
r#"entity `A::"bar"` is of an enumerated entity type, but `"bar"` is not declared as a valid eid"#,
)
.help(r#"valid entity eids: "foo""#)
.build(),
);
}
#[test]
fn invalid_resource_enum() {
expect_validation_err(
"B",
r#"Action::"b""#,
r#"A::"bar""#,
None,
&ExpectedErrorMessageBuilder::error(
r#"entity `A::"bar"` is of an enumerated entity type, but `"bar"` is not declared as a valid eid"#,
)
.help(r#"valid entity eids: "foo""#)
.build(),
);
}
#[test]
fn invalid_context() {
expect_validation_err(
"A",
r#"Action::"a""#,
"B",
Some(Arc::new(BTreeMap::from_iter([("".into(), 1.into())]))),
&ExpectedErrorMessageBuilder::error(
r#"context `{"": 1}` is not valid for `Action::"a"`"#,
)
.build(),
);
}
}
#[cfg(test)]
mod inconsistent_requests {
use std::{collections::BTreeMap, sync::Arc};
use crate::{
ast::{Context, EntityUIDEntry, Request, Value},
extensions::Extensions,
test_utils::{expect_err, ExpectedErrorMessageBuilder},
tpe::{request::PartialRequest, test_utils::parse_partial_euid},
validator::ValidatorSchema,
};
#[track_caller]
fn schema() -> ValidatorSchema {
ValidatorSchema::from_cedarschema_str(
r#"
entity A;
entity B;
action a appliesTo {
principal: A,
resource: B,
context: {
"foo" : Long,
}
};
action b appliesTo {
principal: A,
resource: B,
};
"#,
Extensions::all_available(),
)
.unwrap()
.0
}
#[track_caller]
fn request() -> PartialRequest {
PartialRequest::new(
parse_partial_euid(r#"A::"p""#),
r#"Action::"a""#.parse().unwrap(),
parse_partial_euid(r#"B::"r""#),
Some(Arc::new(BTreeMap::from_iter([("foo".into(), 0.into())]))),
&schema(),
)
.unwrap()
}
#[track_caller]
fn concrete_request(
principal: &str,
action: &str,
resource: &str,
context: BTreeMap<smol_str::SmolStr, Value>,
) -> Request {
Request::new_unchecked(
EntityUIDEntry::known(principal.parse().unwrap(), None),
EntityUIDEntry::known(action.parse().unwrap(), None),
EntityUIDEntry::known(resource.parse().unwrap(), None),
Some(Context::Value(Arc::new(context))),
)
}
#[track_caller]
fn ctx() -> BTreeMap<smol_str::SmolStr, Value> {
BTreeMap::from_iter([("foo".into(), 0.into())])
}
#[track_caller]
fn expect_inconsistency(concrete: &Request, error: &str) {
let err = request()
.check_consistency(concrete)
.expect_err("should be inconsistent");
expect_err(
"",
&miette::Report::new(err),
&ExpectedErrorMessageBuilder::error(error).build(),
);
}
#[test]
fn unknown_principal() {
let concrete = Request::new_unchecked(
EntityUIDEntry::unknown(),
EntityUIDEntry::known(r#"Action::"a""#.parse().unwrap(), None),
EntityUIDEntry::known(r#"B::"r""#.parse().unwrap(), None),
Some(Context::Value(Arc::new(ctx()))),
);
expect_inconsistency(&concrete, "the concrete request's principal is unknown");
}
#[test]
fn unknown_resource() {
let concrete = Request::new_unchecked(
EntityUIDEntry::known(r#"A::"p""#.parse().unwrap(), None),
EntityUIDEntry::known(r#"Action::"a""#.parse().unwrap(), None),
EntityUIDEntry::unknown(),
Some(Context::Value(Arc::new(ctx()))),
);
expect_inconsistency(&concrete, "the concrete request's resource is unknown");
}
#[test]
fn unknown_action() {
let concrete = Request::new_unchecked(
EntityUIDEntry::known(r#"A::"p""#.parse().unwrap(), None),
EntityUIDEntry::unknown(),
EntityUIDEntry::known(r#"B::"r""#.parse().unwrap(), None),
Some(Context::Value(Arc::new(ctx()))),
);
expect_inconsistency(&concrete, "the concrete request's action is unknown");
}
#[test]
fn unknown_context() {
let concrete = Request::new_unchecked(
EntityUIDEntry::known(r#"A::"p""#.parse().unwrap(), None),
EntityUIDEntry::known(r#"Action::"a""#.parse().unwrap(), None),
EntityUIDEntry::known(r#"B::"r""#.parse().unwrap(), None),
None,
);
expect_inconsistency(&concrete, "the concrete request's context is unknown");
}
#[test]
fn principal_type() {
let concrete = concrete_request(r#"B::"p""#, r#"Action::"a""#, r#"B::"r""#, ctx());
expect_inconsistency(
&concrete,
"partial request principal type `A` does not match concrete request principal type `B`",
);
}
#[test]
fn principal_id() {
let concrete = concrete_request(r#"A::"other""#, r#"Action::"a""#, r#"B::"r""#, ctx());
expect_inconsistency(
&concrete,
"partial request principal id `p` does not match concrete request principal id `other`",
);
}
#[test]
fn action_type() {
let concrete = concrete_request(r#"A::"p""#, r#"Foo::"a""#, r#"B::"r""#, ctx());
expect_inconsistency(
&concrete,
r#"partial request action `Action::"a"` does not match concrete request action `Foo::"a"`"#,
);
}
#[test]
fn action_id() {
let concrete = concrete_request(r#"A::"p""#, r#"Action::"b""#, r#"B::"r""#, ctx());
expect_inconsistency(
&concrete,
r#"partial request action `Action::"a"` does not match concrete request action `Action::"b"`"#,
);
}
#[test]
fn resource_type() {
let concrete = concrete_request(r#"A::"p""#, r#"Action::"a""#, r#"A::"r""#, ctx());
expect_inconsistency(
&concrete,
"partial request resource type `B` does not match concrete request resource type `A`",
);
}
#[test]
fn resource_id() {
let concrete = concrete_request(r#"A::"p""#, r#"Action::"a""#, r#"B::"other""#, ctx());
expect_inconsistency(
&concrete,
"partial request resource id `r` does not match concrete request resource id `other`",
);
}
#[test]
fn context() {
let concrete = concrete_request(
r#"A::"p""#,
r#"Action::"a""#,
r#"B::"r""#,
BTreeMap::from_iter([("foo".into(), 1.into())]),
);
expect_inconsistency(
&concrete,
"the partial and concrete request contexts do not match",
);
}
#[test]
fn concrete_context_contains_unknowns() {
use crate::ast::{Expr, Unknown};
let residual =
BTreeMap::from_iter([("foo".into(), Expr::unknown(Unknown::new_untyped("foo")))]);
let concrete = Request::new_unchecked(
EntityUIDEntry::known(r#"A::"p""#.parse().unwrap(), None),
EntityUIDEntry::known(r#"Action::"a""#.parse().unwrap(), None),
EntityUIDEntry::known(r#"B::"r""#.parse().unwrap(), None),
Some(Context::RestrictedResidual(Arc::new(residual))),
);
expect_inconsistency(
&concrete,
"the concrete request's context contains unknowns",
);
}
}