1#![allow(
19 clippy::missing_panics_doc,
20 clippy::missing_errors_doc,
21 clippy::similar_names,
22 clippy::result_large_err, reason = "this module doesn't currently comply with these lints"
24)]
25
26mod id;
27#[cfg(feature = "entity-manifest")]
28use cedar_policy_core::validator::entity_manifest;
29#[cfg(feature = "entity-manifest")]
31pub use cedar_policy_core::validator::entity_manifest::{
32 AccessTrie, EntityManifest, EntityRoot, Fields, RootAccessTrie,
33};
34use cedar_policy_core::validator::json_schema;
35use cedar_policy_core::validator::typecheck::{PolicyCheck, Typechecker};
36pub use id::*;
37
38#[cfg(feature = "deprecated-schema-compat")]
39mod deprecated_schema_compat;
40
41mod err;
42pub use err::*;
43
44pub use ast::Effect;
45pub use authorizer::Decision;
46#[cfg(feature = "partial-eval")]
47use cedar_policy_core::ast::BorrowedRestrictedExpr;
48use cedar_policy_core::ast::{self, RequestSchema, RestrictedExpr};
49use cedar_policy_core::authorizer::{self};
50use cedar_policy_core::entities::{ContextSchema, Dereference};
51use cedar_policy_core::est::{self, TemplateLink};
52use cedar_policy_core::evaluator::Evaluator;
53#[cfg(feature = "partial-eval")]
54use cedar_policy_core::evaluator::RestrictedEvaluator;
55use cedar_policy_core::extensions::Extensions;
56use cedar_policy_core::parser;
57use cedar_policy_core::FromNormalizedStr;
58use itertools::{Either, Itertools};
59use linked_hash_map::LinkedHashMap;
60use miette::Diagnostic;
61use ref_cast::RefCast;
62use serde::{Deserialize, Serialize};
63use smol_str::SmolStr;
64use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
65use std::io::Read;
66use std::str::FromStr;
67use std::sync::Arc;
68
69#[expect(
70 clippy::unwrap_used,
71 reason = "`CARGO_PKG_VERSION` should return a valid SemVer version string"
72)]
73pub(crate) mod version {
74 use semver::Version;
75 use std::sync::LazyLock;
76
77 static SDK_VERSION: LazyLock<Version> =
79 LazyLock::new(|| env!("CARGO_PKG_VERSION").parse().unwrap());
80 static LANG_VERSION: LazyLock<Version> = LazyLock::new(|| Version::new(4, 4, 0));
83
84 pub fn get_sdk_version() -> Version {
86 SDK_VERSION.clone()
87 }
88 pub fn get_lang_version() -> Version {
90 LANG_VERSION.clone()
91 }
92}
93
94#[repr(transparent)]
96#[derive(Debug, Clone, PartialEq, Eq, RefCast, Hash)]
97pub struct Entity(pub(crate) ast::Entity);
98
99#[doc(hidden)] impl AsRef<ast::Entity> for Entity {
101 fn as_ref(&self) -> &ast::Entity {
102 &self.0
103 }
104}
105
106#[doc(hidden)]
107impl From<ast::Entity> for Entity {
108 fn from(entity: ast::Entity) -> Self {
109 Self(entity)
110 }
111}
112
113impl Entity {
114 pub fn new(
136 uid: EntityUid,
137 attrs: HashMap<String, RestrictedExpression>,
138 parents: HashSet<EntityUid>,
139 ) -> Result<Self, EntityAttrEvaluationError> {
140 Self::new_with_tags(uid, attrs, parents, [])
141 }
142
143 pub fn new_no_attrs(uid: EntityUid, parents: HashSet<EntityUid>) -> Self {
148 Self(ast::Entity::new_with_attr_partial_value(
151 uid.into(),
152 [],
153 HashSet::new(),
154 parents.into_iter().map(EntityUid::into).collect(),
155 [],
156 ))
157 }
158
159 pub fn new_with_tags(
164 uid: EntityUid,
165 attrs: impl IntoIterator<Item = (String, RestrictedExpression)>,
166 parents: impl IntoIterator<Item = EntityUid>,
167 tags: impl IntoIterator<Item = (String, RestrictedExpression)>,
168 ) -> Result<Self, EntityAttrEvaluationError> {
169 Ok(Self(ast::Entity::new(
172 uid.into(),
173 attrs.into_iter().map(|(k, v)| (k.into(), v.0)),
174 HashSet::new(),
175 parents.into_iter().map(EntityUid::into).collect(),
176 tags.into_iter().map(|(k, v)| (k.into(), v.0)),
177 Extensions::all_available(),
178 )?))
179 }
180
181 pub fn with_uid(uid: EntityUid) -> Self {
192 Self(ast::Entity::with_uid(uid.into()))
193 }
194
195 pub fn deep_eq(&self, other: &Self) -> bool {
204 self.0.deep_eq(&other.0)
205 }
206
207 pub fn uid(&self) -> EntityUid {
218 self.0.uid().clone().into()
219 }
220
221 pub fn attr(&self, attr: &str) -> Option<Result<EvalResult, PartialValueToValueError>> {
242 match ast::Value::try_from(self.0.get(attr)?.clone()) {
243 Ok(v) => Some(Ok(EvalResult::from(v))),
244 Err(e) => Some(Err(e)),
245 }
246 }
247
248 pub fn attrs(
253 &self,
254 ) -> impl Iterator<Item = (&str, Result<EvalResult, PartialValueToValueError>)> {
255 self.0.attrs().map(|(k, v)| {
256 (
257 k.as_ref(),
258 ast::Value::try_from(v.clone()).map(EvalResult::from),
259 )
260 })
261 }
262
263 pub fn tag(&self, tag: &str) -> Option<Result<EvalResult, PartialValueToValueError>> {
268 match ast::Value::try_from(self.0.get_tag(tag)?.clone()) {
269 Ok(v) => Some(Ok(EvalResult::from(v))),
270 Err(e) => Some(Err(e)),
271 }
272 }
273
274 pub fn tags(
279 &self,
280 ) -> impl Iterator<Item = (&str, Result<EvalResult, PartialValueToValueError>)> {
281 self.0.tags().map(|(k, v)| {
282 (
283 k.as_ref(),
284 ast::Value::try_from(v.clone()).map(EvalResult::from),
285 )
286 })
287 }
288
289 pub fn into_inner(
291 self,
292 ) -> (
293 EntityUid,
294 HashMap<String, RestrictedExpression>,
295 HashSet<EntityUid>,
296 ) {
297 let (uid, attrs, ancestors, mut parents, _) = self.0.into_inner();
298 parents.extend(ancestors);
299
300 let attrs = attrs
301 .into_iter()
302 .map(|(k, v)| {
303 (
304 k.to_string(),
305 match v {
306 ast::PartialValue::Value(val) => {
307 RestrictedExpression(ast::RestrictedExpr::from(val))
308 }
309 ast::PartialValue::Residual(exp) => {
310 RestrictedExpression(ast::RestrictedExpr::new_unchecked(exp))
311 }
312 },
313 )
314 })
315 .collect();
316
317 (
318 uid.into(),
319 attrs,
320 parents.into_iter().map(Into::into).collect(),
321 )
322 }
323
324 pub fn from_json_value(
327 value: serde_json::Value,
328 schema: Option<&Schema>,
329 ) -> Result<Self, EntitiesError> {
330 let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
331 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
332 schema.as_ref(),
333 Extensions::all_available(),
334 cedar_policy_core::entities::TCComputation::ComputeNow,
335 );
336 eparser.single_from_json_value(value).map(Self)
337 }
338
339 pub fn from_json_str(
342 src: impl AsRef<str>,
343 schema: Option<&Schema>,
344 ) -> Result<Self, EntitiesError> {
345 let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
346 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
347 schema.as_ref(),
348 Extensions::all_available(),
349 cedar_policy_core::entities::TCComputation::ComputeNow,
350 );
351 eparser.single_from_json_str(src).map(Self)
352 }
353
354 pub fn from_json_file(f: impl Read, schema: Option<&Schema>) -> Result<Self, EntitiesError> {
357 let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
358 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
359 schema.as_ref(),
360 Extensions::all_available(),
361 cedar_policy_core::entities::TCComputation::ComputeNow,
362 );
363 eparser.single_from_json_file(f).map(Self)
364 }
365
366 pub fn write_to_json(&self, f: impl std::io::Write) -> Result<(), EntitiesError> {
374 self.0.write_to_json(f)
375 }
376
377 pub fn to_json_value(&self) -> Result<serde_json::Value, EntitiesError> {
385 self.0.to_json_value()
386 }
387
388 pub fn to_json_string(&self) -> Result<String, EntitiesError> {
396 self.0.to_json_string()
397 }
398}
399
400impl std::fmt::Display for Entity {
401 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
402 write!(f, "{}", self.0)
403 }
404}
405
406#[repr(transparent)]
409#[derive(Debug, Clone, Default, PartialEq, Eq, RefCast)]
410pub struct Entities(pub(crate) cedar_policy_core::entities::Entities);
411
412#[doc(hidden)] impl AsRef<cedar_policy_core::entities::Entities> for Entities {
414 fn as_ref(&self) -> &cedar_policy_core::entities::Entities {
415 &self.0
416 }
417}
418
419#[doc(hidden)]
420impl From<cedar_policy_core::entities::Entities> for Entities {
421 fn from(entities: cedar_policy_core::entities::Entities) -> Self {
422 Self(entities)
423 }
424}
425
426use entities_errors::EntitiesError;
427
428impl Entities {
429 pub fn empty() -> Self {
436 Self(cedar_policy_core::entities::Entities::new())
437 }
438
439 pub fn get(&self, uid: &EntityUid) -> Option<&Entity> {
441 match self.0.entity(uid.as_ref()) {
442 Dereference::Residual(_) | Dereference::NoSuchEntity => None,
443 Dereference::Data(e) => Some(Entity::ref_cast(e)),
444 }
445 }
446
447 #[doc = include_str!("../experimental_warning.md")]
451 #[must_use]
452 #[cfg(feature = "partial-eval")]
453 pub fn partial(self) -> Self {
454 Self(self.0.partial())
455 }
456
457 pub fn iter(&self) -> impl Iterator<Item = &Entity> {
459 self.0.iter().map(Entity::ref_cast)
460 }
461
462 pub fn deep_eq(&self, other: &Self) -> bool {
468 self.0.deep_eq(&other.0)
469 }
470
471 pub fn from_entities(
490 entities: impl IntoIterator<Item = Entity>,
491 schema: Option<&Schema>,
492 ) -> Result<Self, EntitiesError> {
493 cedar_policy_core::entities::Entities::from_entities(
494 entities.into_iter().map(|e| e.0),
495 schema
496 .map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0))
497 .as_ref(),
498 cedar_policy_core::entities::TCComputation::ComputeNow,
499 Extensions::all_available(),
500 )
501 .map(Entities)
502 }
503
504 pub fn add_entities(
521 self,
522 entities: impl IntoIterator<Item = Entity>,
523 schema: Option<&Schema>,
524 ) -> Result<Self, EntitiesError> {
525 Ok(Self(
526 self.0.add_entities(
527 entities.into_iter().map(|e| Arc::new(e.0)),
528 schema
529 .map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0))
530 .as_ref(),
531 cedar_policy_core::entities::TCComputation::ComputeNow,
532 Extensions::all_available(),
533 )?,
534 ))
535 }
536
537 pub fn remove_entities(
544 self,
545 entity_ids: impl IntoIterator<Item = EntityUid>,
546 ) -> Result<Self, EntitiesError> {
547 Ok(Self(self.0.remove_entities(
548 entity_ids.into_iter().map(|euid| euid.0),
549 cedar_policy_core::entities::TCComputation::ComputeNow,
550 )?))
551 }
552
553 pub fn upsert_entities(
568 self,
569 entities: impl IntoIterator<Item = Entity>,
570 schema: Option<&Schema>,
571 ) -> Result<Self, EntitiesError> {
572 Ok(Self(
573 self.0.upsert_entities(
574 entities.into_iter().map(|e| Arc::new(e.0)),
575 schema
576 .map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0))
577 .as_ref(),
578 cedar_policy_core::entities::TCComputation::ComputeNow,
579 Extensions::all_available(),
580 )?,
581 ))
582 }
583
584 pub fn add_entities_from_json_str(
605 self,
606 json: &str,
607 schema: Option<&Schema>,
608 ) -> Result<Self, EntitiesError> {
609 let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
610 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
611 schema.as_ref(),
612 Extensions::all_available(),
613 cedar_policy_core::entities::TCComputation::ComputeNow,
614 );
615 let new_entities = eparser.iter_from_json_str(json)?.map(Arc::new);
616 Ok(Self(self.0.add_entities(
617 new_entities,
618 schema.as_ref(),
619 cedar_policy_core::entities::TCComputation::ComputeNow,
620 Extensions::all_available(),
621 )?))
622 }
623
624 pub fn add_entities_from_json_value(
645 self,
646 json: serde_json::Value,
647 schema: Option<&Schema>,
648 ) -> Result<Self, EntitiesError> {
649 let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
650 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
651 schema.as_ref(),
652 Extensions::all_available(),
653 cedar_policy_core::entities::TCComputation::ComputeNow,
654 );
655 let new_entities = eparser.iter_from_json_value(json)?.map(Arc::new);
656 Ok(Self(self.0.add_entities(
657 new_entities,
658 schema.as_ref(),
659 cedar_policy_core::entities::TCComputation::ComputeNow,
660 Extensions::all_available(),
661 )?))
662 }
663
664 pub fn add_entities_from_json_file(
686 self,
687 json: impl std::io::Read,
688 schema: Option<&Schema>,
689 ) -> Result<Self, EntitiesError> {
690 let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
691 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
692 schema.as_ref(),
693 Extensions::all_available(),
694 cedar_policy_core::entities::TCComputation::ComputeNow,
695 );
696 let new_entities = eparser.iter_from_json_file(json)?.map(Arc::new);
697 Ok(Self(self.0.add_entities(
698 new_entities,
699 schema.as_ref(),
700 cedar_policy_core::entities::TCComputation::ComputeNow,
701 Extensions::all_available(),
702 )?))
703 }
704
705 pub fn from_json_str(json: &str, schema: Option<&Schema>) -> Result<Self, EntitiesError> {
756 let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
757 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
758 schema.as_ref(),
759 Extensions::all_available(),
760 cedar_policy_core::entities::TCComputation::ComputeNow,
761 );
762 eparser.from_json_str(json).map(Entities)
763 }
764
765 pub fn from_json_value(
811 json: serde_json::Value,
812 schema: Option<&Schema>,
813 ) -> Result<Self, EntitiesError> {
814 let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
815 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
816 schema.as_ref(),
817 Extensions::all_available(),
818 cedar_policy_core::entities::TCComputation::ComputeNow,
819 );
820 eparser.from_json_value(json).map(Entities)
821 }
822
823 pub fn from_json_file(
847 json: impl std::io::Read,
848 schema: Option<&Schema>,
849 ) -> Result<Self, EntitiesError> {
850 let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
851 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
852 schema.as_ref(),
853 Extensions::all_available(),
854 cedar_policy_core::entities::TCComputation::ComputeNow,
855 );
856 eparser.from_json_file(json).map(Entities)
857 }
858
859 pub fn is_ancestor_of(&self, a: &EntityUid, b: &EntityUid) -> bool {
862 match self.0.entity(b.as_ref()) {
863 Dereference::Data(b) => b.is_descendant_of(a.as_ref()),
864 _ => a == b, }
866 }
867
868 pub fn ancestors<'a>(
871 &'a self,
872 euid: &EntityUid,
873 ) -> Option<impl Iterator<Item = &'a EntityUid>> {
874 let entity = match self.0.entity(euid.as_ref()) {
875 Dereference::Residual(_) | Dereference::NoSuchEntity => None,
876 Dereference::Data(e) => Some(e),
877 }?;
878 Some(entity.ancestors().map(EntityUid::ref_cast))
879 }
880
881 pub fn len(&self) -> usize {
883 self.0.len()
884 }
885
886 pub fn is_empty(&self) -> bool {
888 self.0.is_empty()
889 }
890
891 pub fn write_to_json(&self, f: impl std::io::Write) -> std::result::Result<(), EntitiesError> {
899 self.0.write_to_json(f)
900 }
901
902 pub fn to_json_value(&self) -> Result<serde_json::Value, EntitiesError> {
910 self.0.to_json_value()
911 }
912
913 #[doc = include_str!("../experimental_warning.md")]
914 pub fn to_dot_str(&self) -> String {
918 let mut dot_str = String::new();
919 #[expect(clippy::unwrap_used, reason = "writing to a String cannot fail")]
920 self.0.to_dot_str(&mut dot_str).unwrap();
921 dot_str
922 }
923}
924
925pub fn validate_scope_variables(
932 principal: &EntityUid,
933 action: &EntityUid,
934 resource: &EntityUid,
935 schema: &Schema,
936) -> std::result::Result<(), RequestValidationError> {
937 Ok(RequestSchema::validate_scope_variables(
938 &schema.0,
939 Some(&principal.0),
940 Some(&action.0),
941 Some(&resource.0),
942 )?)
943}
944
945pub mod entities {
947
948 #[derive(Debug)]
950 pub struct IntoIter {
951 pub(super) inner: <cedar_policy_core::entities::Entities as IntoIterator>::IntoIter,
952 }
953
954 impl Iterator for IntoIter {
955 type Item = super::Entity;
956
957 fn next(&mut self) -> Option<Self::Item> {
958 self.inner.next().map(super::Entity)
959 }
960 fn size_hint(&self) -> (usize, Option<usize>) {
961 self.inner.size_hint()
962 }
963 }
964}
965
966impl IntoIterator for Entities {
967 type Item = Entity;
968 type IntoIter = entities::IntoIter;
969
970 fn into_iter(self) -> Self::IntoIter {
971 Self::IntoIter {
972 inner: self.0.into_iter(),
973 }
974 }
975}
976
977#[repr(transparent)]
979#[derive(Debug, Clone, RefCast)]
980pub struct Authorizer(authorizer::Authorizer);
981
982#[doc(hidden)] impl AsRef<authorizer::Authorizer> for Authorizer {
984 fn as_ref(&self) -> &authorizer::Authorizer {
985 &self.0
986 }
987}
988
989impl Default for Authorizer {
990 fn default() -> Self {
991 Self::new()
992 }
993}
994
995impl Authorizer {
996 pub fn new() -> Self {
1053 Self(authorizer::Authorizer::new())
1054 }
1055
1056 pub fn is_authorized(&self, r: &Request, p: &PolicySet, e: &Entities) -> Response {
1110 self.0.is_authorized(r.0.clone(), &p.ast, &e.0).into()
1111 }
1112
1113 #[doc = include_str!("../experimental_warning.md")]
1118 #[cfg(feature = "partial-eval")]
1119 pub fn is_authorized_partial(
1120 &self,
1121 query: &Request,
1122 policy_set: &PolicySet,
1123 entities: &Entities,
1124 ) -> PartialResponse {
1125 let response = self
1126 .0
1127 .is_authorized_core(query.0.clone(), &policy_set.ast, &entities.0);
1128 PartialResponse(response)
1129 }
1130}
1131
1132#[derive(Debug, PartialEq, Eq, Clone)]
1134pub struct Response {
1135 pub(crate) decision: Decision,
1137 pub(crate) diagnostics: Diagnostics,
1139}
1140
1141#[doc = include_str!("../experimental_warning.md")]
1146#[cfg(feature = "partial-eval")]
1147#[repr(transparent)]
1148#[derive(Debug, Clone, RefCast)]
1149pub struct PartialResponse(cedar_policy_core::authorizer::PartialResponse);
1150
1151#[cfg(feature = "partial-eval")]
1152impl PartialResponse {
1153 pub fn decision(&self) -> Option<Decision> {
1156 self.0.decision()
1157 }
1158
1159 pub fn concretize(self) -> Response {
1162 self.0.concretize().into()
1163 }
1164
1165 pub fn definitely_satisfied(&self) -> impl Iterator<Item = Policy> + '_ {
1168 self.0.definitely_satisfied().map(Policy::from_ast)
1169 }
1170
1171 pub fn definitely_errored(&self) -> impl Iterator<Item = &PolicyId> {
1173 self.0.definitely_errored().map(PolicyId::ref_cast)
1174 }
1175
1176 pub fn may_be_determining(&self) -> impl Iterator<Item = Policy> + '_ {
1184 self.0.may_be_determining().map(Policy::from_ast)
1185 }
1186
1187 pub fn must_be_determining(&self) -> impl Iterator<Item = Policy> + '_ {
1195 self.0.must_be_determining().map(Policy::from_ast)
1196 }
1197
1198 pub fn nontrivial_residuals(&'_ self) -> impl Iterator<Item = Policy> + '_ {
1200 self.0.nontrivial_residuals().map(Policy::from_ast)
1201 }
1202
1203 pub fn all_residuals(&'_ self) -> impl Iterator<Item = Policy> + '_ {
1205 self.0.all_residuals().map(Policy::from_ast)
1206 }
1207
1208 pub fn unknown_entities(&self) -> HashSet<EntityUid> {
1210 let mut entity_uids = HashSet::new();
1211 for policy in self.0.all_residuals() {
1212 entity_uids.extend(policy.unknown_entities().into_iter().map(Into::into));
1213 }
1214 entity_uids
1215 }
1216
1217 pub fn get(&self, id: &PolicyId) -> Option<Policy> {
1219 self.0.get(id.as_ref()).map(Policy::from_ast)
1220 }
1221
1222 #[expect(
1224 clippy::needless_pass_by_value,
1225 reason = "don't want to change signature of deprecated public function"
1226 )]
1227 #[deprecated = "use reauthorize_with_bindings"]
1228 pub fn reauthorize(
1229 &self,
1230 mapping: HashMap<SmolStr, RestrictedExpression>,
1231 auth: &Authorizer,
1232 es: &Entities,
1233 ) -> Result<Self, ReauthorizationError> {
1234 self.reauthorize_with_bindings(mapping.iter().map(|(k, v)| (k.as_str(), v)), auth, es)
1235 }
1236
1237 pub fn reauthorize_with_bindings<'m>(
1240 &self,
1241 mapping: impl IntoIterator<Item = (&'m str, &'m RestrictedExpression)>,
1242 auth: &Authorizer,
1243 es: &Entities,
1244 ) -> Result<Self, ReauthorizationError> {
1245 let exts = Extensions::all_available();
1246 let evaluator = RestrictedEvaluator::new(exts);
1247 let mapping = mapping
1248 .into_iter()
1249 .map(|(name, expr)| {
1250 evaluator
1251 .interpret(BorrowedRestrictedExpr::new_unchecked(expr.0.as_ref()))
1252 .map(|v| (name.into(), v))
1253 })
1254 .collect::<Result<HashMap<_, _>, EvaluationError>>()?;
1255 let r = self.0.reauthorize(&mapping, &auth.0, &es.0)?;
1256 Ok(Self(r))
1257 }
1258}
1259
1260#[cfg(feature = "partial-eval")]
1261#[doc(hidden)]
1262impl From<cedar_policy_core::authorizer::PartialResponse> for PartialResponse {
1263 fn from(pr: cedar_policy_core::authorizer::PartialResponse) -> Self {
1264 Self(pr)
1265 }
1266}
1267
1268#[derive(Debug, PartialEq, Eq, Clone)]
1270pub struct Diagnostics {
1271 reason: HashSet<PolicyId>,
1274 errors: Vec<AuthorizationError>,
1277}
1278
1279#[doc(hidden)]
1280impl From<authorizer::Diagnostics> for Diagnostics {
1281 fn from(diagnostics: authorizer::Diagnostics) -> Self {
1282 Self {
1283 reason: diagnostics.reason.into_iter().map(PolicyId::new).collect(),
1284 errors: diagnostics.errors.into_iter().map(Into::into).collect(),
1285 }
1286 }
1287}
1288
1289impl Diagnostics {
1290 pub fn reason(&self) -> impl Iterator<Item = &PolicyId> {
1347 self.reason.iter()
1348 }
1349
1350 pub fn errors(&self) -> impl Iterator<Item = &AuthorizationError> + '_ {
1406 self.errors.iter()
1407 }
1408
1409 pub(crate) fn into_components(
1411 self,
1412 ) -> (
1413 impl Iterator<Item = PolicyId>,
1414 impl Iterator<Item = AuthorizationError>,
1415 ) {
1416 (self.reason.into_iter(), self.errors.into_iter())
1417 }
1418}
1419
1420impl Response {
1421 pub fn new(
1423 decision: Decision,
1424 reason: HashSet<PolicyId>,
1425 errors: Vec<AuthorizationError>,
1426 ) -> Self {
1427 Self {
1428 decision,
1429 diagnostics: Diagnostics { reason, errors },
1430 }
1431 }
1432
1433 pub fn decision(&self) -> Decision {
1435 self.decision
1436 }
1437
1438 pub fn diagnostics(&self) -> &Diagnostics {
1440 &self.diagnostics
1441 }
1442}
1443
1444#[doc(hidden)]
1445impl From<authorizer::Response> for Response {
1446 fn from(a: authorizer::Response) -> Self {
1447 Self {
1448 decision: a.decision,
1449 diagnostics: a.diagnostics.into(),
1450 }
1451 }
1452}
1453
1454#[derive(Default, Eq, PartialEq, Copy, Clone, Debug, Serialize, Deserialize)]
1456#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
1457#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1458#[serde(rename_all = "camelCase")]
1459#[non_exhaustive]
1460pub enum ValidationMode {
1461 #[default]
1464 Strict,
1465 #[doc = include_str!("../experimental_warning.md")]
1467 #[cfg(feature = "permissive-validate")]
1468 Permissive,
1469 #[doc = include_str!("../experimental_warning.md")]
1471 #[cfg(feature = "partial-validate")]
1472 Partial,
1473}
1474
1475#[doc(hidden)]
1476impl From<ValidationMode> for cedar_policy_core::validator::ValidationMode {
1477 fn from(mode: ValidationMode) -> Self {
1478 match mode {
1479 ValidationMode::Strict => Self::Strict,
1480 #[cfg(feature = "permissive-validate")]
1481 ValidationMode::Permissive => Self::Permissive,
1482 #[cfg(feature = "partial-validate")]
1483 ValidationMode::Partial => Self::Partial,
1484 }
1485 }
1486}
1487
1488#[repr(transparent)]
1490#[derive(Debug, Clone, RefCast)]
1491pub struct Validator(cedar_policy_core::validator::Validator);
1492
1493#[doc(hidden)] impl AsRef<cedar_policy_core::validator::Validator> for Validator {
1495 fn as_ref(&self) -> &cedar_policy_core::validator::Validator {
1496 &self.0
1497 }
1498}
1499
1500impl Validator {
1501 pub fn new(schema: Schema) -> Self {
1504 Self(cedar_policy_core::validator::Validator::new(schema.0))
1505 }
1506
1507 pub fn schema(&self) -> &Schema {
1509 RefCast::ref_cast(self.0.schema())
1510 }
1511
1512 pub fn validate(&self, pset: &PolicySet, mode: ValidationMode) -> ValidationResult {
1520 ValidationResult::from(self.0.validate(&pset.ast, mode.into()))
1521 }
1522
1523 pub fn validate_with_level(
1531 &self,
1532 pset: &PolicySet,
1533 mode: ValidationMode,
1534 max_deref_level: u32,
1535 ) -> ValidationResult {
1536 ValidationResult::from(
1537 self.0
1538 .validate_with_level(&pset.ast, mode.into(), max_deref_level),
1539 )
1540 }
1541}
1542
1543#[derive(Debug, Clone)]
1546pub struct SchemaFragment {
1547 value: cedar_policy_core::validator::ValidatorSchemaFragment<
1548 cedar_policy_core::validator::ConditionalName,
1549 cedar_policy_core::validator::ConditionalName,
1550 >,
1551 lossless:
1552 cedar_policy_core::validator::json_schema::Fragment<cedar_policy_core::validator::RawName>,
1553}
1554
1555#[doc(hidden)] impl
1557 AsRef<
1558 cedar_policy_core::validator::ValidatorSchemaFragment<
1559 cedar_policy_core::validator::ConditionalName,
1560 cedar_policy_core::validator::ConditionalName,
1561 >,
1562 > for SchemaFragment
1563{
1564 fn as_ref(
1565 &self,
1566 ) -> &cedar_policy_core::validator::ValidatorSchemaFragment<
1567 cedar_policy_core::validator::ConditionalName,
1568 cedar_policy_core::validator::ConditionalName,
1569 > {
1570 &self.value
1571 }
1572}
1573
1574#[doc(hidden)] impl
1576 TryFrom<
1577 cedar_policy_core::validator::json_schema::Fragment<cedar_policy_core::validator::RawName>,
1578 > for SchemaFragment
1579{
1580 type Error = SchemaError;
1581 fn try_from(
1582 json_frag: cedar_policy_core::validator::json_schema::Fragment<
1583 cedar_policy_core::validator::RawName,
1584 >,
1585 ) -> Result<Self, Self::Error> {
1586 Ok(Self {
1587 value: json_frag.clone().try_into()?,
1588 lossless: json_frag,
1589 })
1590 }
1591}
1592
1593fn get_annotation_by_key(
1594 annotations: &est::Annotations,
1595 annotation_key: impl AsRef<str>,
1596) -> Option<&str> {
1597 annotations
1598 .0
1599 .get(&annotation_key.as_ref().parse().ok()?)
1600 .map(|value| annotation_value_to_str_ref(value.as_ref()))
1601}
1602
1603fn annotation_value_to_str_ref(value: Option<&ast::Annotation>) -> &str {
1604 value.map_or("", |a| a.as_ref())
1605}
1606
1607fn annotations_to_pairs(annotations: &est::Annotations) -> impl Iterator<Item = (&str, &str)> {
1608 annotations
1609 .0
1610 .iter()
1611 .map(|(key, value)| (key.as_ref(), annotation_value_to_str_ref(value.as_ref())))
1612}
1613
1614impl SchemaFragment {
1615 pub fn namespace_annotations(
1621 &self,
1622 namespace: EntityNamespace,
1623 ) -> Option<impl Iterator<Item = (&str, &str)>> {
1624 self.lossless
1625 .0
1626 .get(&Some(namespace.0))
1627 .map(|ns_def| annotations_to_pairs(&ns_def.annotations))
1628 }
1629
1630 pub fn namespace_annotation(
1639 &self,
1640 namespace: EntityNamespace,
1641 annotation_key: impl AsRef<str>,
1642 ) -> Option<&str> {
1643 let ns = self.lossless.0.get(&Some(namespace.0))?;
1644 get_annotation_by_key(&ns.annotations, annotation_key)
1645 }
1646
1647 pub fn common_type_annotations(
1653 &self,
1654 namespace: Option<EntityNamespace>,
1655 ty: &str,
1656 ) -> Option<impl Iterator<Item = (&str, &str)>> {
1657 let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
1658 let ty = json_schema::CommonTypeId::new(ast::UnreservedId::from_normalized_str(ty).ok()?)
1659 .ok()?;
1660 ns_def
1661 .common_types
1662 .get(&ty)
1663 .map(|ty| annotations_to_pairs(&ty.annotations))
1664 }
1665
1666 pub fn common_type_annotation(
1675 &self,
1676 namespace: Option<EntityNamespace>,
1677 ty: &str,
1678 annotation_key: impl AsRef<str>,
1679 ) -> Option<&str> {
1680 let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
1681 let ty = json_schema::CommonTypeId::new(ast::UnreservedId::from_normalized_str(ty).ok()?)
1682 .ok()?;
1683 get_annotation_by_key(&ns_def.common_types.get(&ty)?.annotations, annotation_key)
1684 }
1685
1686 pub fn entity_type_annotations(
1692 &self,
1693 namespace: Option<EntityNamespace>,
1694 ty: &str,
1695 ) -> Option<impl Iterator<Item = (&str, &str)>> {
1696 let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
1697 let ty = ast::UnreservedId::from_normalized_str(ty).ok()?;
1698 ns_def
1699 .entity_types
1700 .get(&ty)
1701 .map(|ty| annotations_to_pairs(&ty.annotations))
1702 }
1703
1704 pub fn entity_type_annotation(
1713 &self,
1714 namespace: Option<EntityNamespace>,
1715 ty: &str,
1716 annotation_key: impl AsRef<str>,
1717 ) -> Option<&str> {
1718 let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
1719 let ty = ast::UnreservedId::from_normalized_str(ty).ok()?;
1720 get_annotation_by_key(&ns_def.entity_types.get(&ty)?.annotations, annotation_key)
1721 }
1722
1723 pub fn action_annotations(
1728 &self,
1729 namespace: Option<EntityNamespace>,
1730 id: &EntityId,
1731 ) -> Option<impl Iterator<Item = (&str, &str)>> {
1732 let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
1733 ns_def
1734 .actions
1735 .get(id.unescaped())
1736 .map(|a| annotations_to_pairs(&a.annotations))
1737 }
1738
1739 pub fn action_annotation(
1747 &self,
1748 namespace: Option<EntityNamespace>,
1749 id: &EntityId,
1750 annotation_key: impl AsRef<str>,
1751 ) -> Option<&str> {
1752 let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
1753 get_annotation_by_key(
1754 &ns_def.actions.get(id.unescaped())?.annotations,
1755 annotation_key,
1756 )
1757 }
1758
1759 pub fn namespaces(&self) -> impl Iterator<Item = Option<EntityNamespace>> + '_ {
1763 self.value.namespaces().filter_map(|ns| {
1764 match ns.map(|ns| ast::Name::try_from(ns.clone())) {
1765 Some(Ok(n)) => Some(Some(EntityNamespace(n))),
1766 None => Some(None), Some(Err(_)) => {
1768 None
1775 }
1776 }
1777 })
1778 }
1779
1780 pub fn from_json_str(src: &str) -> Result<Self, SchemaError> {
1783 let lossless = cedar_policy_core::validator::json_schema::Fragment::from_json_str(src)?;
1784 Ok(Self {
1785 value: lossless.clone().try_into()?,
1786 lossless,
1787 })
1788 }
1789
1790 pub fn from_json_value(json: serde_json::Value) -> Result<Self, SchemaError> {
1793 let lossless = cedar_policy_core::validator::json_schema::Fragment::from_json_value(json)?;
1794 Ok(Self {
1795 value: lossless.clone().try_into()?,
1796 lossless,
1797 })
1798 }
1799
1800 pub fn from_cedarschema_file(
1802 r: impl std::io::Read,
1803 ) -> Result<(Self, impl Iterator<Item = SchemaWarning>), CedarSchemaError> {
1804 let (lossless, warnings) =
1805 cedar_policy_core::validator::json_schema::Fragment::from_cedarschema_file(
1806 r,
1807 Extensions::all_available(),
1808 )?;
1809 Ok((
1810 Self {
1811 value: lossless.clone().try_into()?,
1812 lossless,
1813 },
1814 warnings,
1815 ))
1816 }
1817
1818 pub fn from_cedarschema_str(
1820 src: &str,
1821 ) -> Result<(Self, impl Iterator<Item = SchemaWarning>), CedarSchemaError> {
1822 let (lossless, warnings) =
1823 cedar_policy_core::validator::json_schema::Fragment::from_cedarschema_str(
1824 src,
1825 Extensions::all_available(),
1826 )?;
1827 Ok((
1828 Self {
1829 value: lossless.clone().try_into()?,
1830 lossless,
1831 },
1832 warnings,
1833 ))
1834 }
1835
1836 pub fn from_json_file(file: impl std::io::Read) -> Result<Self, SchemaError> {
1839 let lossless = cedar_policy_core::validator::json_schema::Fragment::from_json_file(file)?;
1840 Ok(Self {
1841 value: lossless.clone().try_into()?,
1842 lossless,
1843 })
1844 }
1845
1846 pub fn to_json_value(self) -> Result<serde_json::Value, SchemaError> {
1848 serde_json::to_value(self.lossless).map_err(|e| SchemaError::JsonSerialization(e.into()))
1849 }
1850
1851 pub fn to_json_string(&self) -> Result<String, SchemaError> {
1853 serde_json::to_string(&self.lossless).map_err(|e| SchemaError::JsonSerialization(e.into()))
1854 }
1855
1856 pub fn to_cedarschema(&self) -> Result<String, ToCedarSchemaError> {
1859 let str = self.lossless.to_cedarschema()?;
1860 Ok(str)
1861 }
1862}
1863
1864impl TryInto<Schema> for SchemaFragment {
1865 type Error = SchemaError;
1866
1867 fn try_into(self) -> Result<Schema, Self::Error> {
1871 Ok(Schema(
1872 cedar_policy_core::validator::ValidatorSchema::from_schema_fragments(
1873 [self.value],
1874 Extensions::all_available(),
1875 )?,
1876 ))
1877 }
1878}
1879
1880impl FromStr for SchemaFragment {
1881 type Err = CedarSchemaError;
1882 fn from_str(src: &str) -> Result<Self, Self::Err> {
1888 Self::from_cedarschema_str(src).map(|(frag, _)| frag)
1889 }
1890}
1891
1892#[repr(transparent)]
1894#[derive(Debug, Clone, RefCast)]
1895pub struct Schema(pub(crate) cedar_policy_core::validator::ValidatorSchema);
1896
1897#[doc(hidden)] impl AsRef<cedar_policy_core::validator::ValidatorSchema> for Schema {
1899 fn as_ref(&self) -> &cedar_policy_core::validator::ValidatorSchema {
1900 &self.0
1901 }
1902}
1903
1904#[doc(hidden)]
1905impl From<cedar_policy_core::validator::ValidatorSchema> for Schema {
1906 fn from(schema: cedar_policy_core::validator::ValidatorSchema) -> Self {
1907 Self(schema)
1908 }
1909}
1910
1911impl FromStr for Schema {
1912 type Err = CedarSchemaError;
1913
1914 fn from_str(schema_src: &str) -> Result<Self, Self::Err> {
1921 Self::from_cedarschema_str(schema_src).map(|(schema, _)| schema)
1922 }
1923}
1924
1925impl Schema {
1926 pub fn from_schema_fragments(
1931 fragments: impl IntoIterator<Item = SchemaFragment>,
1932 ) -> Result<Self, SchemaError> {
1933 Ok(Self(
1934 cedar_policy_core::validator::ValidatorSchema::from_schema_fragments(
1935 fragments.into_iter().map(|f| f.value),
1936 Extensions::all_available(),
1937 )?,
1938 ))
1939 }
1940
1941 pub fn from_json_value(json: serde_json::Value) -> Result<Self, SchemaError> {
1944 Ok(Self(
1945 cedar_policy_core::validator::ValidatorSchema::from_json_value(
1946 json,
1947 Extensions::all_available(),
1948 )?,
1949 ))
1950 }
1951
1952 pub fn from_json_str(json: &str) -> Result<Self, SchemaError> {
1955 Ok(Self(
1956 cedar_policy_core::validator::ValidatorSchema::from_json_str(
1957 json,
1958 Extensions::all_available(),
1959 )?,
1960 ))
1961 }
1962
1963 pub fn from_json_file(file: impl std::io::Read) -> Result<Self, SchemaError> {
1966 Ok(Self(
1967 cedar_policy_core::validator::ValidatorSchema::from_json_file(
1968 file,
1969 Extensions::all_available(),
1970 )?,
1971 ))
1972 }
1973
1974 pub fn from_cedarschema_file(
1976 file: impl std::io::Read,
1977 ) -> Result<(Self, impl Iterator<Item = SchemaWarning> + 'static), CedarSchemaError> {
1978 let (schema, warnings) =
1979 cedar_policy_core::validator::ValidatorSchema::from_cedarschema_file(
1980 file,
1981 Extensions::all_available(),
1982 )?;
1983 Ok((Self(schema), warnings))
1984 }
1985
1986 pub fn from_cedarschema_str(
1988 src: &str,
1989 ) -> Result<(Self, impl Iterator<Item = SchemaWarning>), CedarSchemaError> {
1990 let (schema, warnings) =
1991 cedar_policy_core::validator::ValidatorSchema::from_cedarschema_str(
1992 src,
1993 Extensions::all_available(),
1994 )?;
1995 Ok((Self(schema), warnings))
1996 }
1997
1998 pub fn action_entities(&self) -> Result<Entities, EntitiesError> {
2001 Ok(Entities(self.0.action_entities()?))
2002 }
2003
2004 pub fn principals(&self) -> impl Iterator<Item = &EntityTypeName> {
2029 self.0.principals().map(RefCast::ref_cast)
2030 }
2031
2032 pub fn resources(&self) -> impl Iterator<Item = &EntityTypeName> {
2056 self.0.resources().map(RefCast::ref_cast)
2057 }
2058
2059 pub fn principals_for_action(
2065 &self,
2066 action: &EntityUid,
2067 ) -> Option<impl Iterator<Item = &EntityTypeName>> {
2068 self.0
2069 .principals_for_action(&action.0)
2070 .map(|iter| iter.map(RefCast::ref_cast))
2071 }
2072
2073 pub fn resources_for_action(
2079 &self,
2080 action: &EntityUid,
2081 ) -> Option<impl Iterator<Item = &EntityTypeName>> {
2082 self.0
2083 .resources_for_action(&action.0)
2084 .map(|iter| iter.map(RefCast::ref_cast))
2085 }
2086
2087 pub fn request_envs(&self) -> impl Iterator<Item = RequestEnv> + '_ {
2090 self.0
2091 .unlinked_request_envs(cedar_policy_core::validator::ValidationMode::Strict)
2092 .map(Into::into)
2093 }
2094
2095 pub fn ancestors<'a>(
2101 &'a self,
2102 ty: &'a EntityTypeName,
2103 ) -> Option<impl Iterator<Item = &'a EntityTypeName> + 'a> {
2104 self.0
2105 .ancestors(&ty.0)
2106 .map(|iter| iter.map(RefCast::ref_cast))
2107 }
2108
2109 pub fn action_groups(&self) -> impl Iterator<Item = &EntityUid> {
2111 self.0.action_groups().map(RefCast::ref_cast)
2112 }
2113
2114 pub fn entity_types(&self) -> impl Iterator<Item = &EntityTypeName> {
2116 self.0
2117 .entity_types()
2118 .map(|ety| RefCast::ref_cast(ety.name()))
2119 }
2120
2121 pub fn actions(&self) -> impl Iterator<Item = &EntityUid> {
2123 self.0.actions().map(RefCast::ref_cast)
2124 }
2125
2126 pub fn actions_for_principal_and_resource<'a: 'b, 'b>(
2130 &'a self,
2131 principal_type: &'b EntityTypeName,
2132 resource_type: &'b EntityTypeName,
2133 ) -> impl Iterator<Item = &'a EntityUid> + 'b {
2134 self.0
2135 .actions_for_principal_and_resource(&principal_type.0, &resource_type.0)
2136 .map(RefCast::ref_cast)
2137 }
2138}
2139
2140pub fn schema_str_to_json_with_resolved_types(
2150 schema_str: &str,
2151) -> Result<(serde_json::Value, Vec<SchemaWarning>), CedarSchemaError> {
2152 let (json_schema_fragment, warnings) =
2154 json_schema::Fragment::from_cedarschema_str(schema_str, Extensions::all_available())
2155 .map_err(
2156 |e: cedar_policy_core::validator::CedarSchemaError| -> CedarSchemaError {
2157 e.into()
2158 },
2159 )?;
2160
2161 let warnings_as_schema_warnings: Vec<SchemaWarning> = warnings.collect();
2162
2163 let fully_resolved_fragment =
2165 match json_schema_fragment.to_internal_name_fragment_with_resolved_types() {
2166 Ok(fragment) => fragment,
2167 Err(e) => {
2168 return Err(e.into());
2170 }
2171 };
2172
2173 let json_value = serde_json::to_value(&fully_resolved_fragment).map_err(|e| {
2175 let schema_error = SchemaError::JsonSerialization(
2176 cedar_policy_core::validator::schema_errors::JsonSerializationError::from(e),
2177 );
2178 CedarSchemaError::Schema(schema_error)
2179 })?;
2180
2181 Ok((json_value, warnings_as_schema_warnings))
2182}
2183
2184#[cfg(test)]
2189mod test_schema_str_to_json_with_resolved_types {
2190 use super::*;
2191
2192 #[test]
2193 fn test_unresolved_type_error() {
2194 let schema_str = r#"entity User = { "name": MyName };"#;
2195
2196 let result = schema_str_to_json_with_resolved_types(schema_str);
2197
2198 match result {
2200 Ok(_) => panic!("Expected error but got success - MyName should not be resolved"),
2201 Err(CedarSchemaError::Schema(SchemaError::TypeNotDefined(type_not_defined_error))) => {
2202 let error_message = format!("{}", type_not_defined_error);
2204 assert!(
2205 error_message.contains("MyName"),
2206 "Expected error message to contain 'MyName', but got: {}",
2207 error_message
2208 );
2209
2210 assert!(
2212 error_message.contains("failed to resolve type"),
2213 "Expected error message to mention 'failed to resolve type', but got: {}",
2214 error_message
2215 );
2216 }
2217 Err(CedarSchemaError::Schema(other_schema_error)) => {
2218 panic!(
2219 "Expected TypeNotDefined error, but got different SchemaError: {:?}",
2220 other_schema_error
2221 );
2222 }
2223 Err(CedarSchemaError::Parse(parse_error)) => {
2224 panic!(
2225 "Expected TypeNotDefined error, but got parse error: {:?}",
2226 parse_error
2227 );
2228 }
2229 Err(CedarSchemaError::Io(io_error)) => {
2230 panic!(
2231 "Expected TypeNotDefined error, but got IO error: {:?}",
2232 io_error
2233 );
2234 }
2235 }
2236 }
2237
2238 #[test]
2239 fn test_successful_resolution() {
2240 let schema_str = r#"
2241 type MyName = String;
2242 entity User = { "name": MyName };
2243 "#;
2244
2245 let result = schema_str_to_json_with_resolved_types(schema_str);
2246
2247 match result {
2248 Ok((json_value, warnings)) => {
2249 assert!(json_value.is_object(), "Expected JSON object");
2251
2252 let json_str = serde_json::to_string(&json_value).unwrap();
2254 assert!(
2255 !json_str.contains("EntityOrCommon"),
2256 "JSON should not contain unresolved EntityOrCommon types: {}",
2257 json_str
2258 );
2259
2260 assert!(
2262 json_str.contains("MyName"),
2263 "JSON should contain resolved MyName type reference: {}",
2264 json_str
2265 );
2266
2267 assert_eq!(warnings.len(), 0, "Expected no warnings for valid schema");
2269 }
2270 Err(e) => panic!("Expected success but got error: {:?}", e),
2271 }
2272 }
2273}
2274#[derive(Debug, Clone)]
2277pub struct ValidationResult {
2278 validation_errors: Vec<ValidationError>,
2279 validation_warnings: Vec<ValidationWarning>,
2280}
2281
2282impl ValidationResult {
2283 pub fn validation_passed(&self) -> bool {
2287 self.validation_errors.is_empty()
2288 }
2289
2290 pub fn validation_passed_without_warnings(&self) -> bool {
2293 self.validation_errors.is_empty() && self.validation_warnings.is_empty()
2294 }
2295
2296 pub fn validation_errors(&self) -> impl Iterator<Item = &ValidationError> {
2298 self.validation_errors.iter()
2299 }
2300
2301 pub fn validation_warnings(&self) -> impl Iterator<Item = &ValidationWarning> {
2303 self.validation_warnings.iter()
2304 }
2305
2306 fn first_error_or_warning(&self) -> Option<&dyn Diagnostic> {
2307 self.validation_errors
2308 .first()
2309 .map(|e| e as &dyn Diagnostic)
2310 .or_else(|| {
2311 self.validation_warnings
2312 .first()
2313 .map(|w| w as &dyn Diagnostic)
2314 })
2315 }
2316
2317 pub(crate) fn into_errors_and_warnings(
2318 self,
2319 ) -> (
2320 impl Iterator<Item = ValidationError>,
2321 impl Iterator<Item = ValidationWarning>,
2322 ) {
2323 (
2324 self.validation_errors.into_iter(),
2325 self.validation_warnings.into_iter(),
2326 )
2327 }
2328}
2329
2330#[doc(hidden)]
2331impl From<cedar_policy_core::validator::ValidationResult> for ValidationResult {
2332 fn from(r: cedar_policy_core::validator::ValidationResult) -> Self {
2333 let (errors, warnings) = r.into_errors_and_warnings();
2334 Self {
2335 validation_errors: errors.map(ValidationError::from).collect(),
2336 validation_warnings: warnings.map(ValidationWarning::from).collect(),
2337 }
2338 }
2339}
2340
2341impl std::fmt::Display for ValidationResult {
2342 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2343 match self.first_error_or_warning() {
2344 Some(diagnostic) => write!(f, "{diagnostic}"),
2345 None => write!(f, "no errors or warnings"),
2346 }
2347 }
2348}
2349
2350impl std::error::Error for ValidationResult {
2351 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
2352 self.first_error_or_warning()
2353 .and_then(std::error::Error::source)
2354 }
2355
2356 fn description(&self) -> &str {
2357 #[expect(
2358 deprecated,
2359 reason = "description() is deprecated but we still want to forward it"
2360 )]
2361 self.first_error_or_warning()
2362 .map_or("no errors or warnings", std::error::Error::description)
2363 }
2364
2365 fn cause(&self) -> Option<&dyn std::error::Error> {
2366 #[expect(
2367 deprecated,
2368 reason = "cause() is deprecated but we still want to forward it"
2369 )]
2370 self.first_error_or_warning()
2371 .and_then(std::error::Error::cause)
2372 }
2373}
2374
2375impl Diagnostic for ValidationResult {
2379 fn related(&self) -> Option<Box<dyn Iterator<Item = &dyn Diagnostic> + '_>> {
2380 let mut related = self
2381 .validation_errors
2382 .iter()
2383 .map(|err| err as &dyn Diagnostic)
2384 .chain(
2385 self.validation_warnings
2386 .iter()
2387 .map(|warn| warn as &dyn Diagnostic),
2388 );
2389 related.next().map(move |first| match first.related() {
2390 Some(first_related) => Box::new(first_related.chain(related)),
2391 None => Box::new(related) as Box<dyn Iterator<Item = _>>,
2392 })
2393 }
2394
2395 fn severity(&self) -> Option<miette::Severity> {
2396 self.first_error_or_warning()
2397 .map_or(Some(miette::Severity::Advice), Diagnostic::severity)
2398 }
2399
2400 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
2401 self.first_error_or_warning().and_then(Diagnostic::labels)
2402 }
2403
2404 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
2405 self.first_error_or_warning()
2406 .and_then(Diagnostic::source_code)
2407 }
2408
2409 fn code(&self) -> Option<Box<dyn std::fmt::Display + '_>> {
2410 self.first_error_or_warning().and_then(Diagnostic::code)
2411 }
2412
2413 fn url(&self) -> Option<Box<dyn std::fmt::Display + '_>> {
2414 self.first_error_or_warning().and_then(Diagnostic::url)
2415 }
2416
2417 fn help(&self) -> Option<Box<dyn std::fmt::Display + '_>> {
2418 self.first_error_or_warning().and_then(Diagnostic::help)
2419 }
2420
2421 fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
2422 self.first_error_or_warning()
2423 .and_then(Diagnostic::diagnostic_source)
2424 }
2425}
2426
2427pub fn confusable_string_checker<'a>(
2433 templates: impl Iterator<Item = &'a Template> + 'a,
2434) -> impl Iterator<Item = ValidationWarning> + 'a {
2435 cedar_policy_core::validator::confusable_string_checks(templates.map(|t| &t.ast))
2436 .map(std::convert::Into::into)
2437}
2438
2439#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
2451pub struct EntityNamespace(pub(crate) ast::Name);
2452
2453#[doc(hidden)] impl AsRef<ast::Name> for EntityNamespace {
2455 fn as_ref(&self) -> &ast::Name {
2456 &self.0
2457 }
2458}
2459
2460impl FromStr for EntityNamespace {
2463 type Err = ParseErrors;
2464
2465 fn from_str(namespace_str: &str) -> Result<Self, Self::Err> {
2466 ast::Name::from_normalized_str(namespace_str)
2467 .map(EntityNamespace)
2468 .map_err(Into::into)
2469 }
2470}
2471
2472impl std::fmt::Display for EntityNamespace {
2473 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2474 write!(f, "{}", self.0)
2475 }
2476}
2477
2478#[derive(Debug, Clone, Default)]
2479pub(crate) struct StringifiedPolicySet {
2483 pub policies: Vec<String>,
2485 pub policy_templates: Vec<String>,
2487}
2488
2489#[derive(Debug, Clone, Default)]
2491pub struct PolicySet {
2492 pub(crate) ast: ast::PolicySet,
2495 policies: LinkedHashMap<PolicyId, Policy>,
2497 templates: LinkedHashMap<PolicyId, Template>,
2499}
2500
2501impl PartialEq for PolicySet {
2502 fn eq(&self, other: &Self) -> bool {
2503 self.ast.eq(&other.ast)
2505 }
2506}
2507impl Eq for PolicySet {}
2508
2509#[doc(hidden)] impl AsRef<ast::PolicySet> for PolicySet {
2511 fn as_ref(&self) -> &ast::PolicySet {
2512 &self.ast
2513 }
2514}
2515
2516#[doc(hidden)]
2517impl TryFrom<ast::PolicySet> for PolicySet {
2518 type Error = PolicySetError;
2519 fn try_from(pset: ast::PolicySet) -> Result<Self, Self::Error> {
2520 Self::from_ast(pset)
2521 }
2522}
2523
2524impl FromStr for PolicySet {
2525 type Err = ParseErrors;
2526
2527 fn from_str(policies: &str) -> Result<Self, Self::Err> {
2534 let (texts, pset) = parser::parse_policyset_and_also_return_policy_text(policies)?;
2535 #[expect(clippy::expect_used, reason = "By the invariant on `parse_policyset_and_also_return_policy_text(policies)`, every `PolicyId` in `pset.policies()` occurs as a key in `text`.")]
2536 let policies = pset.policies().map(|p|
2537 (
2538 PolicyId::new(p.id().clone()),
2539 Policy { lossless: LosslessPolicy::policy_or_template_text(*texts.get(p.id()).expect("internal invariant violation: policy id exists in asts but not texts")), ast: p.clone() }
2540 )
2541 ).collect();
2542 #[expect(clippy::expect_used, reason = "By the invariant on `parse_policyset_and_also_return_policy_text(policies)`, every `PolicyId` in `pset.templates()` also occurs as a key in `text`.")]
2543 let templates = pset.templates().map(|t|
2544 (
2545 PolicyId::new(t.id().clone()),
2546 Template { lossless: LosslessPolicy::policy_or_template_text(*texts.get(t.id()).expect("internal invariant violation: template id exists in asts but not ests")), ast: t.clone() }
2547 )
2548 ).collect();
2549 Ok(Self {
2550 ast: pset,
2551 policies,
2552 templates,
2553 })
2554 }
2555}
2556
2557impl PolicySet {
2558 fn from_est(est: &est::PolicySet) -> Result<Self, PolicySetError> {
2560 let ast: ast::PolicySet = est.clone().try_into()?;
2561 #[expect(
2562 clippy::expect_used,
2563 reason = "Since conversion from EST to AST succeeded, every `PolicyId` in `ast.policies()` occurs in `est`"
2564 )]
2565 let policies = ast
2566 .policies()
2567 .map(|p| {
2568 (
2569 PolicyId::new(p.id().clone()),
2570 Policy {
2571 lossless: LosslessPolicy::Est(est.get_policy(p.id()).expect(
2572 "internal invariant violation: policy id exists in asts but not ests",
2573 )),
2574 ast: p.clone(),
2575 },
2576 )
2577 })
2578 .collect();
2579 #[expect(
2580 clippy::expect_used,
2581 reason = "Since conversion from EST to AST succeeded, every `PolicyId` in `ast.templates()` occurs in `est`"
2582 )]
2583 let templates = ast
2584 .templates()
2585 .map(|t| {
2586 (
2587 PolicyId::new(t.id().clone()),
2588 Template {
2589 lossless: LosslessPolicy::Est(est.get_template(t.id()).expect(
2590 "internal invariant violation: template id exists in asts but not ests",
2591 )),
2592 ast: t.clone(),
2593 },
2594 )
2595 })
2596 .collect();
2597 Ok(Self {
2598 ast,
2599 policies,
2600 templates,
2601 })
2602 }
2603
2604 pub(crate) fn from_ast(ast: ast::PolicySet) -> Result<Self, PolicySetError> {
2606 Self::from_policies(ast.into_policies().map(Policy::from_ast))
2607 }
2608
2609 pub fn from_json_str(src: impl AsRef<str>) -> Result<Self, PolicySetError> {
2611 let est: est::PolicySet = serde_json::from_str(src.as_ref())
2612 .map_err(|e| policy_set_errors::JsonPolicySetError { inner: e })?;
2613 Self::from_est(&est)
2614 }
2615
2616 pub fn from_json_value(src: serde_json::Value) -> Result<Self, PolicySetError> {
2618 let est: est::PolicySet = serde_json::from_value(src)
2619 .map_err(|e| policy_set_errors::JsonPolicySetError { inner: e })?;
2620 Self::from_est(&est)
2621 }
2622
2623 pub fn from_json_file(r: impl std::io::Read) -> Result<Self, PolicySetError> {
2625 let est: est::PolicySet = serde_json::from_reader(r)
2626 .map_err(|e| policy_set_errors::JsonPolicySetError { inner: e })?;
2627 Self::from_est(&est)
2628 }
2629
2630 pub fn to_json(self) -> Result<serde_json::Value, PolicySetError> {
2632 let est = self.est()?;
2633 let value = serde_json::to_value(est)
2634 .map_err(|e| policy_set_errors::JsonPolicySetError { inner: e })?;
2635 Ok(value)
2636 }
2637
2638 fn est(self) -> Result<est::PolicySet, PolicyToJsonError> {
2640 let (static_policies, template_links): (Vec<_>, Vec<_>) =
2641 fold_partition(self.policies, is_static_or_link)?;
2642 let static_policies = static_policies.into_iter().collect::<LinkedHashMap<_, _>>();
2643 let templates = self
2644 .templates
2645 .into_iter()
2646 .map(|(id, template)| {
2647 template
2648 .lossless
2649 .est(|| template.ast.clone().into())
2650 .map(|est| (id.into(), est))
2651 })
2652 .collect::<Result<LinkedHashMap<_, _>, _>>()?;
2653 let est = est::PolicySet {
2654 templates,
2655 static_policies,
2656 template_links,
2657 };
2658
2659 Ok(est)
2660 }
2661
2662 pub fn to_cedar(&self) -> Option<String> {
2681 match self.stringify() {
2682 Some(StringifiedPolicySet {
2683 policies,
2684 policy_templates,
2685 }) => {
2686 let policies_as_vec = policies
2687 .into_iter()
2688 .chain(policy_templates)
2689 .collect::<Vec<_>>();
2690 Some(policies_as_vec.join("\n\n"))
2691 }
2692 None => None,
2693 }
2694 }
2695
2696 pub(crate) fn stringify(&self) -> Option<StringifiedPolicySet> {
2713 let policies = self
2714 .policies
2715 .values()
2716 .sorted_by_key(|p| AsRef::<str>::as_ref(p.id()))
2720 .map(Policy::to_cedar)
2721 .collect::<Option<Vec<_>>>()?;
2722 let policy_templates = self
2723 .templates
2724 .values()
2725 .sorted_by_key(|t| AsRef::<str>::as_ref(t.id()))
2726 .map(Template::to_cedar)
2727 .collect_vec();
2728
2729 Some(StringifiedPolicySet {
2730 policies,
2731 policy_templates,
2732 })
2733 }
2734
2735 pub fn new() -> Self {
2737 Self {
2738 ast: ast::PolicySet::new(),
2739 policies: LinkedHashMap::new(),
2740 templates: LinkedHashMap::new(),
2741 }
2742 }
2743
2744 pub fn from_policies(
2746 policies: impl IntoIterator<Item = Policy>,
2747 ) -> Result<Self, PolicySetError> {
2748 let mut set = Self::new();
2749 for policy in policies {
2750 set.add(policy)?;
2751 }
2752 Ok(set)
2753 }
2754
2755 pub fn merge(
2770 &mut self,
2771 other: &Self,
2772 rename_duplicates: bool,
2773 ) -> Result<HashMap<PolicyId, PolicyId>, PolicySetError> {
2774 match self.ast.merge_policyset(&other.ast, rename_duplicates) {
2775 Ok(renaming) => {
2776 let renaming: HashMap<PolicyId, PolicyId> = renaming
2777 .into_iter()
2778 .map(|(old_pid, new_pid)| (PolicyId::new(old_pid), PolicyId::new(new_pid)))
2779 .collect();
2780
2781 for (pid, op) in &other.policies {
2782 let pid = renaming.get(pid).unwrap_or(pid);
2783 if !self.policies.contains_key(pid) {
2784 #[expect(
2785 clippy::unwrap_used,
2786 reason = "`pid` is the new id of a policy from `other`, so it will be in `self` after merging"
2787 )]
2788 let new_p = Policy {
2789 ast: self.ast.get(pid.as_ref()).unwrap().clone(),
2792 lossless: op.lossless.clone(),
2793 };
2794 self.policies.insert(pid.clone(), new_p);
2795 }
2796 }
2797 for (pid, ot) in &other.templates {
2798 let pid = renaming.get(pid).unwrap_or(pid);
2799 if !self.templates.contains_key(pid) {
2800 #[expect(
2801 clippy::unwrap_used,
2802 reason = "`pid` is the new id of a template from `other`, so it will be in `self` after merging"
2803 )]
2804 let new_t = Template {
2805 ast: self.ast.get_template(pid.as_ref()).unwrap().clone(),
2806 lossless: ot.lossless.clone(),
2807 };
2808 self.templates.insert(pid.clone(), new_t);
2809 }
2810 }
2811
2812 Ok(renaming)
2813 }
2814 Err(ast::PolicySetError::Occupied { id }) => Err(PolicySetError::AlreadyDefined(
2815 policy_set_errors::AlreadyDefined {
2816 id: PolicyId::new(id),
2817 },
2818 )),
2819 }
2820 }
2821
2822 pub fn add(&mut self, policy: Policy) -> Result<(), PolicySetError> {
2826 if policy.is_static() {
2827 let id = PolicyId::new(policy.ast.id().clone());
2828 self.ast.add(policy.ast.clone())?;
2829 self.policies.insert(id, policy);
2830 Ok(())
2831 } else {
2832 Err(PolicySetError::ExpectedStatic(
2833 policy_set_errors::ExpectedStatic::new(),
2834 ))
2835 }
2836 }
2837
2838 pub fn remove_static(&mut self, policy_id: PolicyId) -> Result<Policy, PolicySetError> {
2842 let Some(policy) = self.policies.remove(&policy_id) else {
2843 return Err(PolicySetError::PolicyNonexistent(
2844 policy_set_errors::PolicyNonexistentError { policy_id },
2845 ));
2846 };
2847 if self
2848 .ast
2849 .remove_static(&ast::PolicyID::from_string(&policy_id))
2850 .is_ok()
2851 {
2852 Ok(policy)
2853 } else {
2854 self.policies.insert(policy_id.clone(), policy);
2856 Err(PolicySetError::PolicyNonexistent(
2857 policy_set_errors::PolicyNonexistentError { policy_id },
2858 ))
2859 }
2860 }
2861
2862 pub fn add_template(&mut self, template: Template) -> Result<(), PolicySetError> {
2864 let id = PolicyId::new(template.ast.id().clone());
2865 self.ast.add_template(template.ast.clone())?;
2866 self.templates.insert(id, template);
2867 Ok(())
2868 }
2869
2870 pub fn remove_template(&mut self, template_id: PolicyId) -> Result<Template, PolicySetError> {
2875 let Some(template) = self.templates.remove(&template_id) else {
2876 return Err(PolicySetError::TemplateNonexistent(
2877 policy_set_errors::TemplateNonexistentError { template_id },
2878 ));
2879 };
2880 #[expect(clippy::panic, reason = "We just found the policy in self.templates")]
2882 match self
2883 .ast
2884 .remove_template(&ast::PolicyID::from_string(&template_id))
2885 {
2886 Ok(_) => Ok(template),
2887 Err(ast::PolicySetTemplateRemovalError::RemoveTemplateWithLinksError(_)) => {
2888 self.templates.insert(template_id.clone(), template);
2889 Err(PolicySetError::RemoveTemplateWithActiveLinks(
2890 policy_set_errors::RemoveTemplateWithActiveLinksError { template_id },
2891 ))
2892 }
2893 Err(ast::PolicySetTemplateRemovalError::NotTemplateError(_)) => {
2894 self.templates.insert(template_id.clone(), template);
2895 Err(PolicySetError::RemoveTemplateNotTemplate(
2896 policy_set_errors::RemoveTemplateNotTemplateError { template_id },
2897 ))
2898 }
2899 Err(ast::PolicySetTemplateRemovalError::RemovePolicyNoTemplateError(_)) => {
2900 panic!("Found template policy in self.templates but not in self.ast");
2901 }
2902 }
2903 }
2904
2905 pub fn get_linked_policies(
2908 &self,
2909 template_id: PolicyId,
2910 ) -> Result<impl Iterator<Item = &PolicyId>, PolicySetError> {
2911 self.ast
2912 .get_linked_policies(&ast::PolicyID::from_string(&template_id))
2913 .map_or_else(
2914 |_| {
2915 Err(PolicySetError::TemplateNonexistent(
2916 policy_set_errors::TemplateNonexistentError { template_id },
2917 ))
2918 },
2919 |v| Ok(v.map(PolicyId::ref_cast)),
2920 )
2921 }
2922
2923 pub fn policies(&self) -> impl Iterator<Item = &Policy> {
2927 self.policies.values()
2928 }
2929
2930 pub fn templates(&self) -> impl Iterator<Item = &Template> {
2932 self.templates.values()
2933 }
2934
2935 pub fn template(&self, id: &PolicyId) -> Option<&Template> {
2937 self.templates.get(id)
2938 }
2939
2940 pub fn policy(&self, id: &PolicyId) -> Option<&Policy> {
2942 self.policies.get(id)
2943 }
2944
2945 pub fn annotation(&self, id: &PolicyId, key: impl AsRef<str>) -> Option<&str> {
2950 self.ast
2951 .get(id.as_ref())?
2952 .annotation(&key.as_ref().parse().ok()?)
2953 .map(AsRef::as_ref)
2954 }
2955
2956 pub fn template_annotation(&self, id: &PolicyId, key: impl AsRef<str>) -> Option<&str> {
2961 self.ast
2962 .get_template(id.as_ref())?
2963 .annotation(&key.as_ref().parse().ok()?)
2964 .map(AsRef::as_ref)
2965 }
2966
2967 pub fn is_empty(&self) -> bool {
2969 debug_assert_eq!(
2970 self.ast.is_empty(),
2971 self.policies.is_empty() && self.templates.is_empty()
2972 );
2973 self.ast.is_empty()
2974 }
2975
2976 pub fn num_of_policies(&self) -> usize {
2980 self.policies.len()
2981 }
2982
2983 pub fn num_of_templates(&self) -> usize {
2985 self.templates.len()
2986 }
2987
2988 pub fn link(
2997 &mut self,
2998 template_id: PolicyId,
2999 new_id: PolicyId,
3000 vals: HashMap<SlotId, EntityUid>,
3001 ) -> Result<(), PolicySetError> {
3002 let unwrapped_vals: HashMap<ast::SlotId, ast::EntityUID> = vals
3003 .into_iter()
3004 .map(|(key, value)| (key.into(), value.into()))
3005 .collect();
3006
3007 let Some(template) = self.templates.get(&template_id) else {
3012 return Err(if self.policies.contains_key(&template_id) {
3013 policy_set_errors::ExpectedTemplate::new().into()
3014 } else {
3015 policy_set_errors::LinkingError {
3016 inner: ast::LinkingError::NoSuchTemplate {
3017 id: template_id.into(),
3018 },
3019 }
3020 .into()
3021 });
3022 };
3023
3024 let linked_ast = self.ast.link(
3025 template_id.into(),
3026 new_id.clone().into(),
3027 unwrapped_vals.clone(),
3028 )?;
3029
3030 #[expect(
3031 clippy::expect_used,
3032 reason = "`lossless.link()` will not fail after `ast.link()` succeeds"
3033 )]
3034 let linked_lossless = template
3035 .lossless
3036 .clone()
3037 .link(unwrapped_vals.iter().map(|(k, v)| (*k, v)))
3038 .expect("ast.link() didn't fail above, so this shouldn't fail");
3043 self.policies.insert(
3044 new_id,
3045 Policy {
3046 ast: linked_ast.clone(),
3047 lossless: linked_lossless,
3048 },
3049 );
3050 Ok(())
3051 }
3052
3053 #[doc = include_str!("../experimental_warning.md")]
3055 #[cfg(feature = "partial-eval")]
3056 pub fn unknown_entities(&self) -> HashSet<EntityUid> {
3057 let mut entity_uids = HashSet::new();
3058 for policy in self.policies.values() {
3059 entity_uids.extend(policy.unknown_entities());
3060 }
3061 entity_uids
3062 }
3063
3064 pub fn unlink(&mut self, policy_id: PolicyId) -> Result<Policy, PolicySetError> {
3067 let Some(policy) = self.policies.remove(&policy_id) else {
3068 return Err(PolicySetError::LinkNonexistent(
3069 policy_set_errors::LinkNonexistentError { policy_id },
3070 ));
3071 };
3072 #[expect(clippy::panic, reason = "We just found the policy in self.policies")]
3074 match self.ast.unlink(&ast::PolicyID::from_string(&policy_id)) {
3075 Ok(_) => Ok(policy),
3076 Err(ast::PolicySetUnlinkError::NotLinkError(_)) => {
3077 self.policies.insert(policy_id.clone(), policy);
3079 Err(PolicySetError::UnlinkLinkNotLink(
3080 policy_set_errors::UnlinkLinkNotLinkError { policy_id },
3081 ))
3082 }
3083 Err(ast::PolicySetUnlinkError::UnlinkingError(_)) => {
3084 panic!("Found linked policy in self.policies but not in self.ast")
3085 }
3086 }
3087 }
3088}
3089
3090impl std::fmt::Display for PolicySet {
3091 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3092 let mut policies = self.policies().peekable();
3094 while let Some(policy) = policies.next() {
3095 policy.lossless.fmt(|| policy.ast.clone().into(), f)?;
3096 if policies.peek().is_some() {
3097 writeln!(f)?;
3098 }
3099 }
3100 Ok(())
3101 }
3102}
3103
3104fn is_static_or_link(
3107 (id, policy): (PolicyId, Policy),
3108) -> Result<Either<(ast::PolicyID, est::Policy), TemplateLink>, PolicyToJsonError> {
3109 match policy.template_id() {
3110 Some(template_id) => {
3111 let values = policy
3112 .ast
3113 .env()
3114 .iter()
3115 .map(|(id, euid)| (*id, euid.clone()))
3116 .collect();
3117 Ok(Either::Right(TemplateLink {
3118 new_id: id.into(),
3119 template_id: template_id.clone().into(),
3120 values,
3121 }))
3122 }
3123 None => policy
3124 .lossless
3125 .est(|| policy.ast.clone().into())
3126 .map(|est| Either::Left((id.into(), est))),
3127 }
3128}
3129
3130#[expect(
3133 clippy::redundant_pub_crate,
3134 reason = "can't be private because it's used in tests"
3135)]
3136pub(crate) fn fold_partition<T, A, B, E>(
3137 i: impl IntoIterator<Item = T>,
3138 f: impl Fn(T) -> Result<Either<A, B>, E>,
3139) -> Result<(Vec<A>, Vec<B>), E> {
3140 let mut lefts = vec![];
3141 let mut rights = vec![];
3142
3143 for item in i {
3144 match f(item)? {
3145 Either::Left(left) => lefts.push(left),
3146 Either::Right(right) => rights.push(right),
3147 }
3148 }
3149
3150 Ok((lefts, rights))
3151}
3152
3153#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
3157pub struct RequestEnv {
3158 pub(crate) principal: EntityTypeName,
3159 pub(crate) action: EntityUid,
3160 pub(crate) resource: EntityTypeName,
3161 pub(crate) principal_slot: Option<EntityTypeName>,
3162 pub(crate) resource_slot: Option<EntityTypeName>,
3163}
3164
3165impl RequestEnv {
3166 pub fn new(principal: EntityTypeName, action: EntityUid, resource: EntityTypeName) -> Self {
3168 Self {
3169 principal,
3170 action,
3171 resource,
3172 principal_slot: None,
3173 resource_slot: None,
3174 }
3175 }
3176
3177 pub fn new_request_env_with_slots(
3179 principal: EntityTypeName,
3180 action: EntityUid,
3181 resource: EntityTypeName,
3182 principal_slot: Option<EntityTypeName>,
3183 resource_slot: Option<EntityTypeName>,
3184 ) -> Self {
3185 Self {
3186 principal,
3187 action,
3188 resource,
3189 principal_slot,
3190 resource_slot,
3191 }
3192 }
3193
3194 pub fn principal(&self) -> &EntityTypeName {
3196 &self.principal
3197 }
3198
3199 pub fn action(&self) -> &EntityUid {
3201 &self.action
3202 }
3203
3204 pub fn resource(&self) -> &EntityTypeName {
3206 &self.resource
3207 }
3208
3209 pub fn principal_slot(&self) -> Option<&EntityTypeName> {
3211 self.principal_slot.as_ref()
3212 }
3213
3214 pub fn resource_slot(&self) -> Option<&EntityTypeName> {
3216 self.resource_slot.as_ref()
3217 }
3218}
3219
3220#[doc(hidden)]
3221impl From<cedar_policy_core::validator::types::RequestEnv<'_>> for RequestEnv {
3222 fn from(renv: cedar_policy_core::validator::types::RequestEnv<'_>) -> Self {
3223 match renv {
3224 cedar_policy_core::validator::types::RequestEnv::DeclaredAction {
3225 principal,
3226 action,
3227 resource,
3228 principal_slot,
3229 resource_slot,
3230 ..
3231 } => Self {
3232 principal: principal.clone().into(),
3233 action: action.clone().into(),
3234 resource: resource.clone().into(),
3235 principal_slot: principal_slot.map(EntityTypeName::from),
3236 resource_slot: resource_slot.map(EntityTypeName::from),
3237 },
3238 #[expect(
3239 clippy::unreachable,
3240 reason = "partial validation is not enabled and hence `RequestEnv::UndeclaredAction` should not show up"
3241 )]
3242 cedar_policy_core::validator::types::RequestEnv::UndeclaredAction => {
3243 unreachable!("used unsupported feature")
3244 }
3245 }
3246 }
3247}
3248
3249fn get_valid_request_envs(ast: &ast::Template, s: &Schema) -> impl Iterator<Item = RequestEnv> {
3254 let tc = Typechecker::new(
3255 &s.0,
3256 cedar_policy_core::validator::ValidationMode::default(),
3257 );
3258 tc.typecheck_by_request_env(ast)
3259 .into_iter()
3260 .filter_map(|(env, pc)| {
3261 if matches!(pc, PolicyCheck::Success(_)) {
3262 Some(env.into())
3263 } else {
3264 None
3265 }
3266 })
3267 .collect::<BTreeSet<_>>()
3268 .into_iter()
3269}
3270
3271#[derive(Debug, Clone)]
3277pub struct Template {
3278 pub(crate) ast: ast::Template,
3281
3282 pub(crate) lossless: LosslessPolicy,
3293}
3294
3295impl PartialEq for Template {
3296 fn eq(&self, other: &Self) -> bool {
3297 self.ast.eq(&other.ast)
3299 }
3300}
3301impl Eq for Template {}
3302
3303#[doc(hidden)] impl AsRef<ast::Template> for Template {
3305 fn as_ref(&self) -> &ast::Template {
3306 &self.ast
3307 }
3308}
3309
3310#[doc(hidden)]
3311impl From<ast::Template> for Template {
3312 fn from(template: ast::Template) -> Self {
3313 Self::from_ast(template)
3314 }
3315}
3316
3317impl Template {
3318 pub fn parse(id: Option<PolicyId>, src: impl AsRef<str>) -> Result<Self, ParseErrors> {
3324 let ast = parser::parse_template(id.map(Into::into), src.as_ref())?;
3325 Ok(Self {
3326 ast,
3327 lossless: LosslessPolicy::policy_or_template_text(Some(src.as_ref())),
3328 })
3329 }
3330
3331 pub fn id(&self) -> &PolicyId {
3333 PolicyId::ref_cast(self.ast.id())
3334 }
3335
3336 #[must_use]
3338 pub fn new_id(&self, id: PolicyId) -> Self {
3339 Self {
3340 ast: self.ast.new_id(id.into()),
3341 lossless: self.lossless.clone(), }
3343 }
3344
3345 pub fn effect(&self) -> Effect {
3347 self.ast.effect()
3348 }
3349
3350 pub fn has_non_scope_constraint(&self) -> bool {
3352 self.ast.non_scope_constraints().is_some()
3353 }
3354
3355 pub fn annotation(&self, key: impl AsRef<str>) -> Option<&str> {
3360 self.ast
3361 .annotation(&key.as_ref().parse().ok()?)
3362 .map(AsRef::as_ref)
3363 }
3364
3365 pub fn annotations(&self) -> impl Iterator<Item = (&str, &str)> {
3369 self.ast
3370 .annotations()
3371 .map(|(k, v)| (k.as_ref(), v.as_ref()))
3372 }
3373
3374 pub fn slots(&self) -> impl Iterator<Item = &SlotId> {
3376 self.ast.slots().map(|slot| SlotId::ref_cast(&slot.id))
3377 }
3378
3379 pub fn principal_constraint(&self) -> TemplatePrincipalConstraint {
3381 match self.ast.principal_constraint().as_inner() {
3382 ast::PrincipalOrResourceConstraint::Any => TemplatePrincipalConstraint::Any,
3383 ast::PrincipalOrResourceConstraint::In(eref) => {
3384 TemplatePrincipalConstraint::In(match eref {
3385 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3386 ast::EntityReference::Slot(_) => None,
3387 })
3388 }
3389 ast::PrincipalOrResourceConstraint::Eq(eref) => {
3390 TemplatePrincipalConstraint::Eq(match eref {
3391 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3392 ast::EntityReference::Slot(_) => None,
3393 })
3394 }
3395 ast::PrincipalOrResourceConstraint::Is(entity_type) => {
3396 TemplatePrincipalConstraint::Is(entity_type.as_ref().clone().into())
3397 }
3398 ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
3399 TemplatePrincipalConstraint::IsIn(
3400 entity_type.as_ref().clone().into(),
3401 match eref {
3402 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3403 ast::EntityReference::Slot(_) => None,
3404 },
3405 )
3406 }
3407 }
3408 }
3409
3410 pub fn action_constraint(&self) -> ActionConstraint {
3412 match self.ast.action_constraint() {
3414 ast::ActionConstraint::Any => ActionConstraint::Any,
3415 ast::ActionConstraint::In(ids) => {
3416 ActionConstraint::In(ids.iter().map(|id| id.as_ref().clone().into()).collect())
3417 }
3418 ast::ActionConstraint::Eq(id) => ActionConstraint::Eq(id.as_ref().clone().into()),
3419 #[cfg(feature = "tolerant-ast")]
3420 ast::ActionConstraint::ErrorConstraint => {
3421 ActionConstraint::Any
3426 }
3427 }
3428 }
3429
3430 pub fn resource_constraint(&self) -> TemplateResourceConstraint {
3432 match self.ast.resource_constraint().as_inner() {
3433 ast::PrincipalOrResourceConstraint::Any => TemplateResourceConstraint::Any,
3434 ast::PrincipalOrResourceConstraint::In(eref) => {
3435 TemplateResourceConstraint::In(match eref {
3436 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3437 ast::EntityReference::Slot(_) => None,
3438 })
3439 }
3440 ast::PrincipalOrResourceConstraint::Eq(eref) => {
3441 TemplateResourceConstraint::Eq(match eref {
3442 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3443 ast::EntityReference::Slot(_) => None,
3444 })
3445 }
3446 ast::PrincipalOrResourceConstraint::Is(entity_type) => {
3447 TemplateResourceConstraint::Is(entity_type.as_ref().clone().into())
3448 }
3449 ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
3450 TemplateResourceConstraint::IsIn(
3451 entity_type.as_ref().clone().into(),
3452 match eref {
3453 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3454 ast::EntityReference::Slot(_) => None,
3455 },
3456 )
3457 }
3458 }
3459 }
3460
3461 pub fn from_json(
3467 id: Option<PolicyId>,
3468 json: serde_json::Value,
3469 ) -> Result<Self, PolicyFromJsonError> {
3470 let est: est::Policy = serde_json::from_value(json)
3471 .map_err(|e| entities_json_errors::JsonDeserializationError::Serde(e.into()))
3472 .map_err(cedar_policy_core::est::FromJsonError::from)?;
3473 Self::from_est(id, est)
3474 }
3475
3476 fn from_est(id: Option<PolicyId>, est: est::Policy) -> Result<Self, PolicyFromJsonError> {
3477 Ok(Self {
3478 ast: est.clone().try_into_ast_template(id.map(PolicyId::into))?,
3479 lossless: LosslessPolicy::Est(est),
3480 })
3481 }
3482
3483 pub(crate) fn from_ast(ast: ast::Template) -> Self {
3484 Self {
3485 lossless: LosslessPolicy::Est(ast.clone().into()),
3486 ast,
3487 }
3488 }
3489
3490 pub fn to_json(&self) -> Result<serde_json::Value, PolicyToJsonError> {
3492 let est = self.lossless.est(|| self.ast.clone().into())?;
3493 serde_json::to_value(est).map_err(Into::into)
3494 }
3495
3496 pub fn to_cedar(&self) -> String {
3505 match &self.lossless {
3506 LosslessPolicy::Empty | LosslessPolicy::Est(_) => self.ast.to_string(),
3507 LosslessPolicy::Text { text, .. } => text.clone(),
3508 }
3509 }
3510
3511 pub fn get_valid_request_envs(&self, s: &Schema) -> impl Iterator<Item = RequestEnv> {
3516 get_valid_request_envs(&self.ast, s)
3517 }
3518}
3519
3520impl std::fmt::Display for Template {
3521 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3522 self.lossless.fmt(|| self.ast.clone().into(), f)
3524 }
3525}
3526
3527impl FromStr for Template {
3528 type Err = ParseErrors;
3529
3530 fn from_str(src: &str) -> Result<Self, Self::Err> {
3531 Self::parse(None, src)
3532 }
3533}
3534
3535#[derive(Debug, Clone, PartialEq, Eq)]
3537pub enum PrincipalConstraint {
3538 Any,
3540 In(EntityUid),
3542 Eq(EntityUid),
3544 Is(EntityTypeName),
3546 IsIn(EntityTypeName, EntityUid),
3548}
3549
3550#[derive(Debug, Clone, PartialEq, Eq)]
3552pub enum TemplatePrincipalConstraint {
3553 Any,
3555 In(Option<EntityUid>),
3558 Eq(Option<EntityUid>),
3561 Is(EntityTypeName),
3563 IsIn(EntityTypeName, Option<EntityUid>),
3566}
3567
3568impl TemplatePrincipalConstraint {
3569 pub fn has_slot(&self) -> bool {
3571 match self {
3572 Self::Any | Self::Is(_) => false,
3573 Self::In(o) | Self::Eq(o) | Self::IsIn(_, o) => o.is_none(),
3574 }
3575 }
3576}
3577
3578#[derive(Debug, Clone, PartialEq, Eq)]
3580pub enum ActionConstraint {
3581 Any,
3583 In(Vec<EntityUid>),
3585 Eq(EntityUid),
3587}
3588
3589#[derive(Debug, Clone, PartialEq, Eq)]
3591pub enum ResourceConstraint {
3592 Any,
3594 In(EntityUid),
3596 Eq(EntityUid),
3598 Is(EntityTypeName),
3600 IsIn(EntityTypeName, EntityUid),
3602}
3603
3604#[derive(Debug, Clone, PartialEq, Eq)]
3606pub enum TemplateResourceConstraint {
3607 Any,
3609 In(Option<EntityUid>),
3612 Eq(Option<EntityUid>),
3615 Is(EntityTypeName),
3617 IsIn(EntityTypeName, Option<EntityUid>),
3620}
3621
3622impl TemplateResourceConstraint {
3623 pub fn has_slot(&self) -> bool {
3625 match self {
3626 Self::Any | Self::Is(_) => false,
3627 Self::In(o) | Self::Eq(o) | Self::IsIn(_, o) => o.is_none(),
3628 }
3629 }
3630}
3631
3632#[derive(Debug, Clone)]
3634pub struct Policy {
3635 pub(crate) ast: ast::Policy,
3638 pub(crate) lossless: LosslessPolicy,
3646}
3647
3648impl PartialEq for Policy {
3649 fn eq(&self, other: &Self) -> bool {
3650 self.ast.eq(&other.ast)
3652 }
3653}
3654impl Eq for Policy {}
3655
3656#[doc(hidden)] impl AsRef<ast::Policy> for Policy {
3658 fn as_ref(&self) -> &ast::Policy {
3659 &self.ast
3660 }
3661}
3662
3663#[doc(hidden)]
3664impl From<ast::Policy> for Policy {
3665 fn from(policy: ast::Policy) -> Self {
3666 Self::from_ast(policy)
3667 }
3668}
3669
3670#[doc(hidden)]
3671impl From<ast::StaticPolicy> for Policy {
3672 fn from(policy: ast::StaticPolicy) -> Self {
3673 ast::Policy::from(policy).into()
3674 }
3675}
3676
3677impl Policy {
3678 pub fn template_id(&self) -> Option<&PolicyId> {
3681 if self.is_static() {
3682 None
3683 } else {
3684 Some(PolicyId::ref_cast(self.ast.template().id()))
3685 }
3686 }
3687
3688 pub fn template_links(&self) -> Option<HashMap<SlotId, EntityUid>> {
3691 if self.is_static() {
3692 None
3693 } else {
3694 let wrapped_vals: HashMap<SlotId, EntityUid> = self
3695 .ast
3696 .env()
3697 .iter()
3698 .map(|(key, value)| ((*key).into(), value.clone().into()))
3699 .collect();
3700 Some(wrapped_vals)
3701 }
3702 }
3703
3704 pub fn effect(&self) -> Effect {
3706 self.ast.effect()
3707 }
3708
3709 pub fn has_non_scope_constraint(&self) -> bool {
3711 self.ast.non_scope_constraints().is_some()
3712 }
3713
3714 pub fn annotation(&self, key: impl AsRef<str>) -> Option<&str> {
3719 self.ast
3720 .annotation(&key.as_ref().parse().ok()?)
3721 .map(AsRef::as_ref)
3722 }
3723
3724 pub fn annotations(&self) -> impl Iterator<Item = (&str, &str)> {
3728 self.ast
3729 .annotations()
3730 .map(|(k, v)| (k.as_ref(), v.as_ref()))
3731 }
3732
3733 pub fn id(&self) -> &PolicyId {
3735 PolicyId::ref_cast(self.ast.id())
3736 }
3737
3738 #[must_use]
3740 pub fn new_id(&self, id: PolicyId) -> Self {
3741 Self {
3742 ast: self.ast.new_id(id.into()),
3743 lossless: self.lossless.clone(), }
3745 }
3746
3747 pub fn is_static(&self) -> bool {
3749 self.ast.is_static()
3750 }
3751
3752 pub fn principal_constraint(&self) -> PrincipalConstraint {
3754 let slot_id = ast::SlotId::principal();
3755 match self.ast.template().principal_constraint().as_inner() {
3756 ast::PrincipalOrResourceConstraint::Any => PrincipalConstraint::Any,
3757 ast::PrincipalOrResourceConstraint::In(eref) => {
3758 PrincipalConstraint::In(self.convert_entity_reference(eref, slot_id).clone())
3759 }
3760 ast::PrincipalOrResourceConstraint::Eq(eref) => {
3761 PrincipalConstraint::Eq(self.convert_entity_reference(eref, slot_id).clone())
3762 }
3763 ast::PrincipalOrResourceConstraint::Is(entity_type) => {
3764 PrincipalConstraint::Is(entity_type.as_ref().clone().into())
3765 }
3766 ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
3767 PrincipalConstraint::IsIn(
3768 entity_type.as_ref().clone().into(),
3769 self.convert_entity_reference(eref, slot_id).clone(),
3770 )
3771 }
3772 }
3773 }
3774
3775 pub fn action_constraint(&self) -> ActionConstraint {
3777 match self.ast.template().action_constraint() {
3779 ast::ActionConstraint::Any => ActionConstraint::Any,
3780 ast::ActionConstraint::In(ids) => ActionConstraint::In(
3781 ids.iter()
3782 .map(|euid| EntityUid::ref_cast(euid.as_ref()))
3783 .cloned()
3784 .collect(),
3785 ),
3786 ast::ActionConstraint::Eq(id) => ActionConstraint::Eq(EntityUid::ref_cast(id).clone()),
3787 #[cfg(feature = "tolerant-ast")]
3788 ast::ActionConstraint::ErrorConstraint => {
3789 ActionConstraint::Any
3794 }
3795 }
3796 }
3797
3798 pub fn resource_constraint(&self) -> ResourceConstraint {
3800 let slot_id = ast::SlotId::resource();
3801 match self.ast.template().resource_constraint().as_inner() {
3802 ast::PrincipalOrResourceConstraint::Any => ResourceConstraint::Any,
3803 ast::PrincipalOrResourceConstraint::In(eref) => {
3804 ResourceConstraint::In(self.convert_entity_reference(eref, slot_id).clone())
3805 }
3806 ast::PrincipalOrResourceConstraint::Eq(eref) => {
3807 ResourceConstraint::Eq(self.convert_entity_reference(eref, slot_id).clone())
3808 }
3809 ast::PrincipalOrResourceConstraint::Is(entity_type) => {
3810 ResourceConstraint::Is(entity_type.as_ref().clone().into())
3811 }
3812 ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
3813 ResourceConstraint::IsIn(
3814 entity_type.as_ref().clone().into(),
3815 self.convert_entity_reference(eref, slot_id).clone(),
3816 )
3817 }
3818 }
3819 }
3820
3821 fn convert_entity_reference<'a>(
3828 &'a self,
3829 r: &'a ast::EntityReference,
3830 slot: ast::SlotId,
3831 ) -> &'a EntityUid {
3832 match r {
3833 ast::EntityReference::EUID(euid) => EntityUid::ref_cast(euid),
3834 #[expect(
3835 clippy::unwrap_used,
3836 reason = "This `unwrap` here is safe due the invariant (values total map) on policies"
3837 )]
3838 ast::EntityReference::Slot(_) => {
3839 EntityUid::ref_cast(self.ast.env().get(&slot).unwrap())
3840 }
3841 }
3842 }
3843
3844 pub fn parse(id: Option<PolicyId>, policy_src: impl AsRef<str>) -> Result<Self, ParseErrors> {
3853 let inline_ast = parser::parse_policy(id.map(Into::into), policy_src.as_ref())?;
3854 let (_, ast) = ast::Template::link_static_policy(inline_ast);
3855 Ok(Self {
3856 ast,
3857 lossless: LosslessPolicy::policy_or_template_text(Some(policy_src.as_ref())),
3858 })
3859 }
3860
3861 pub fn from_json(
3927 id: Option<PolicyId>,
3928 json: serde_json::Value,
3929 ) -> Result<Self, PolicyFromJsonError> {
3930 let est: est::Policy = serde_json::from_value(json)
3931 .map_err(|e| entities_json_errors::JsonDeserializationError::Serde(e.into()))
3932 .map_err(cedar_policy_core::est::FromJsonError::from)?;
3933 Self::from_est(id, est)
3934 }
3935
3936 pub fn get_valid_request_envs(&self, s: &Schema) -> impl Iterator<Item = RequestEnv> {
3941 get_valid_request_envs(self.ast.template(), s)
3942 }
3943
3944 pub fn entity_literals(&self) -> Vec<EntityUid> {
3946 self.ast
3947 .condition()
3948 .subexpressions()
3949 .filter_map(|e| match e.expr_kind() {
3950 cedar_policy_core::ast::ExprKind::Lit(
3951 cedar_policy_core::ast::Literal::EntityUID(euid),
3952 ) => Some(EntityUid((*euid).as_ref().clone())),
3953 _ => None,
3954 })
3955 .collect()
3956 }
3957
3958 pub fn sub_entity_literals(
3961 &self,
3962 mapping: BTreeMap<EntityUid, EntityUid>,
3963 ) -> Result<Self, PolicyFromJsonError> {
3964 #[expect(
3965 clippy::expect_used,
3966 reason = "This can't fail for a policy that was already constructed"
3967 )]
3968 let cloned_est = self
3969 .lossless
3970 .est(|| self.ast.clone().into())
3971 .expect("Internal error, failed to construct est.");
3972
3973 let mapping = mapping.into_iter().map(|(k, v)| (k.0, v.0)).collect();
3974
3975 #[expect(
3976 clippy::expect_used,
3977 reason = "This can't fail for a policy that was already constructed"
3978 )]
3979 let est = cloned_est
3980 .sub_entity_literals(&mapping)
3981 .expect("Internal error, failed to sub entity literals.");
3982
3983 let ast = est
3984 .clone()
3985 .try_into_ast_policy(Some(self.ast.id().clone()))?;
3986
3987 Ok(Self {
3988 ast,
3989 lossless: LosslessPolicy::Est(est),
3990 })
3991 }
3992
3993 fn from_est(id: Option<PolicyId>, est: est::Policy) -> Result<Self, PolicyFromJsonError> {
3994 Ok(Self {
3995 ast: est.clone().try_into_ast_policy(id.map(PolicyId::into))?,
3996 lossless: LosslessPolicy::Est(est),
3997 })
3998 }
3999
4000 pub fn to_json(&self) -> Result<serde_json::Value, PolicyToJsonError> {
4019 let est = self.lossless.est(|| self.ast.clone().into())?;
4020 serde_json::to_value(est).map_err(Into::into)
4021 }
4022
4023 pub fn to_cedar(&self) -> Option<String> {
4038 match &self.lossless {
4039 LosslessPolicy::Empty | LosslessPolicy::Est(_) => Some(self.ast.to_string()),
4040 LosslessPolicy::Text { text, slots } => {
4041 if slots.is_empty() {
4042 Some(text.clone())
4043 } else {
4044 None
4045 }
4046 }
4047 }
4048 }
4049
4050 #[doc = include_str!("../experimental_warning.md")]
4052 #[cfg(feature = "partial-eval")]
4053 pub fn unknown_entities(&self) -> HashSet<EntityUid> {
4054 self.ast
4055 .unknown_entities()
4056 .into_iter()
4057 .map(Into::into)
4058 .collect()
4059 }
4060
4061 pub(crate) fn from_ast(ast: ast::Policy) -> Self {
4067 let text = ast.to_string(); Self {
4069 ast,
4070 lossless: LosslessPolicy::policy_or_template_text(Some(text)),
4071 }
4072 }
4073}
4074
4075impl std::fmt::Display for Policy {
4076 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4077 self.lossless.fmt(|| self.ast.clone().into(), f)
4079 }
4080}
4081
4082impl FromStr for Policy {
4083 type Err = ParseErrors;
4084 fn from_str(policy: &str) -> Result<Self, Self::Err> {
4092 Self::parse(None, policy)
4093 }
4094}
4095
4096#[derive(Debug, Clone)]
4100pub(crate) enum LosslessPolicy {
4101 Empty,
4103 Est(est::Policy),
4105 Text {
4107 text: String,
4109 slots: HashMap<ast::SlotId, ast::EntityUID>,
4113 },
4114}
4115
4116impl LosslessPolicy {
4117 fn policy_or_template_text(text: Option<impl Into<String>>) -> Self {
4119 text.map_or(Self::Empty, |text| Self::Text {
4120 text: text.into(),
4121 slots: HashMap::new(),
4122 })
4123 }
4124
4125 fn est(
4127 &self,
4128 fallback_est: impl FnOnce() -> est::Policy,
4129 ) -> Result<est::Policy, PolicyToJsonError> {
4130 match self {
4131 Self::Empty => Ok(fallback_est()),
4133 Self::Est(est) => Ok(est.clone()),
4134 Self::Text { text, slots } => {
4135 let est =
4136 parser::parse_policy_or_template_to_est(text).map_err(ParseErrors::from)?;
4137 if slots.is_empty() {
4138 Ok(est)
4139 } else {
4140 let unwrapped_vals = slots.iter().map(|(k, v)| (*k, v.into())).collect();
4141 Ok(est.link(&unwrapped_vals)?)
4142 }
4143 }
4144 }
4145 }
4146
4147 fn link<'a>(
4148 self,
4149 vals: impl IntoIterator<Item = (ast::SlotId, &'a ast::EntityUID)>,
4150 ) -> Result<Self, est::LinkingError> {
4151 match self {
4152 Self::Empty => Ok(Self::Empty),
4153 Self::Est(est) => {
4154 let unwrapped_est_vals: HashMap<
4155 ast::SlotId,
4156 cedar_policy_core::entities::EntityUidJson,
4157 > = vals.into_iter().map(|(k, v)| (k, v.into())).collect();
4158 Ok(Self::Est(est.link(&unwrapped_est_vals)?))
4159 }
4160 Self::Text { text, slots } => {
4161 debug_assert!(
4162 slots.is_empty(),
4163 "shouldn't call link() on an already-linked policy"
4164 );
4165 let slots = vals.into_iter().map(|(k, v)| (k, v.clone())).collect();
4166 Ok(Self::Text { text, slots })
4167 }
4168 }
4169 }
4170
4171 fn fmt(
4172 &self,
4173 fallback_est: impl FnOnce() -> est::Policy,
4174 f: &mut std::fmt::Formatter<'_>,
4175 ) -> std::fmt::Result {
4176 match self {
4177 Self::Empty => match self.est(fallback_est) {
4178 Ok(est) => write!(f, "{est}"),
4179 Err(e) => write!(f, "<invalid policy: {e}>"),
4180 },
4181 Self::Est(est) => write!(f, "{est}"),
4182 Self::Text { text, slots } => {
4183 if slots.is_empty() {
4184 write!(f, "{text}")
4185 } else {
4186 match self.est(fallback_est) {
4192 Ok(est) => write!(f, "{est}"),
4193 Err(e) => write!(f, "<invalid linked policy: {e}>"),
4194 }
4195 }
4196 }
4197 }
4198 }
4199}
4200
4201#[repr(transparent)]
4203#[derive(Debug, Clone, RefCast)]
4204pub struct Expression(pub(crate) ast::Expr);
4205
4206#[doc(hidden)] impl AsRef<ast::Expr> for Expression {
4208 fn as_ref(&self) -> &ast::Expr {
4209 &self.0
4210 }
4211}
4212
4213#[doc(hidden)]
4214impl From<ast::Expr> for Expression {
4215 fn from(expr: ast::Expr) -> Self {
4216 Self(expr)
4217 }
4218}
4219
4220impl Expression {
4221 pub fn new_string(value: String) -> Self {
4223 Self(ast::Expr::val(value))
4224 }
4225
4226 pub fn new_bool(value: bool) -> Self {
4228 Self(ast::Expr::val(value))
4229 }
4230
4231 pub fn new_long(value: ast::Integer) -> Self {
4233 Self(ast::Expr::val(value))
4234 }
4235
4236 pub fn new_record(
4240 fields: impl IntoIterator<Item = (String, Self)>,
4241 ) -> Result<Self, ExpressionConstructionError> {
4242 Ok(Self(ast::Expr::record(
4243 fields.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
4244 )?))
4245 }
4246
4247 pub fn new_set(values: impl IntoIterator<Item = Self>) -> Self {
4249 Self(ast::Expr::set(values.into_iter().map(|v| v.0)))
4250 }
4251
4252 pub fn new_ip(src: impl AsRef<str>) -> Self {
4256 let src_expr = ast::Expr::val(src.as_ref());
4257 Self(ast::Expr::call_extension_fn(
4258 ip_extension_name(),
4259 vec![src_expr],
4260 ))
4261 }
4262
4263 pub fn new_decimal(src: impl AsRef<str>) -> Self {
4267 let src_expr = ast::Expr::val(src.as_ref());
4268 Self(ast::Expr::call_extension_fn(
4269 decimal_extension_name(),
4270 vec![src_expr],
4271 ))
4272 }
4273
4274 pub fn new_datetime(src: impl AsRef<str>) -> Self {
4278 let src_expr = ast::Expr::val(src.as_ref());
4279 Self(ast::Expr::call_extension_fn(
4280 datetime_extension_name(),
4281 vec![src_expr],
4282 ))
4283 }
4284
4285 pub fn new_duration(src: impl AsRef<str>) -> Self {
4289 let src_expr = ast::Expr::val(src.as_ref());
4290 Self(ast::Expr::call_extension_fn(
4291 duration_extension_name(),
4292 vec![src_expr],
4293 ))
4294 }
4295}
4296
4297#[cfg(test)]
4298impl Expression {
4299 pub(crate) fn into_inner(self) -> ast::Expr {
4302 self.0
4303 }
4304}
4305
4306impl FromStr for Expression {
4307 type Err = ParseErrors;
4308
4309 fn from_str(expression: &str) -> Result<Self, Self::Err> {
4311 ast::Expr::from_str(expression)
4312 .map(Expression)
4313 .map_err(Into::into)
4314 }
4315}
4316
4317impl std::fmt::Display for Expression {
4318 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4319 write!(f, "{}", &self.0)
4320 }
4321}
4322
4323#[repr(transparent)]
4339#[derive(Debug, Clone, RefCast, PartialEq, Eq)]
4340pub struct RestrictedExpression(pub(crate) ast::RestrictedExpr);
4341
4342#[doc(hidden)] impl AsRef<ast::RestrictedExpr> for RestrictedExpression {
4344 fn as_ref(&self) -> &ast::RestrictedExpr {
4345 &self.0
4346 }
4347}
4348
4349#[doc(hidden)]
4350impl From<ast::RestrictedExpr> for RestrictedExpression {
4351 fn from(expr: ast::RestrictedExpr) -> Self {
4352 Self(expr)
4353 }
4354}
4355
4356impl RestrictedExpression {
4357 pub fn new_string(value: String) -> Self {
4359 Self(ast::RestrictedExpr::val(value))
4360 }
4361
4362 pub fn new_bool(value: bool) -> Self {
4364 Self(ast::RestrictedExpr::val(value))
4365 }
4366
4367 pub fn new_long(value: ast::Integer) -> Self {
4369 Self(ast::RestrictedExpr::val(value))
4370 }
4371
4372 pub fn new_entity_uid(value: EntityUid) -> Self {
4374 Self(ast::RestrictedExpr::val(ast::EntityUID::from(value)))
4375 }
4376
4377 pub fn new_record(
4381 fields: impl IntoIterator<Item = (String, Self)>,
4382 ) -> Result<Self, ExpressionConstructionError> {
4383 Ok(Self(ast::RestrictedExpr::record(
4384 fields.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
4385 )?))
4386 }
4387
4388 pub fn new_set(values: impl IntoIterator<Item = Self>) -> Self {
4390 Self(ast::RestrictedExpr::set(values.into_iter().map(|v| v.0)))
4391 }
4392
4393 pub fn new_ip(src: impl AsRef<str>) -> Self {
4397 let src_expr = ast::RestrictedExpr::val(src.as_ref());
4398 Self(ast::RestrictedExpr::call_extension_fn(
4399 ip_extension_name(),
4400 [src_expr],
4401 ))
4402 }
4403
4404 pub fn new_decimal(src: impl AsRef<str>) -> Self {
4408 let src_expr = ast::RestrictedExpr::val(src.as_ref());
4409 Self(ast::RestrictedExpr::call_extension_fn(
4410 decimal_extension_name(),
4411 [src_expr],
4412 ))
4413 }
4414
4415 pub fn new_datetime(src: impl AsRef<str>) -> Self {
4419 let src_expr = ast::RestrictedExpr::val(src.as_ref());
4420 Self(ast::RestrictedExpr::call_extension_fn(
4421 datetime_extension_name(),
4422 [src_expr],
4423 ))
4424 }
4425
4426 pub fn new_duration(src: impl AsRef<str>) -> Self {
4430 let src_expr = ast::RestrictedExpr::val(src.as_ref());
4431 Self(ast::RestrictedExpr::call_extension_fn(
4432 duration_extension_name(),
4433 [src_expr],
4434 ))
4435 }
4436
4437 #[cfg(feature = "partial-eval")]
4439 pub fn new_unknown(name: impl AsRef<str>) -> Self {
4440 Self(ast::RestrictedExpr::unknown(ast::Unknown::new_untyped(
4441 name.as_ref(),
4442 )))
4443 }
4444}
4445
4446#[cfg(test)]
4447impl RestrictedExpression {
4448 pub(crate) fn into_inner(self) -> ast::RestrictedExpr {
4451 self.0
4452 }
4453}
4454
4455fn decimal_extension_name() -> ast::Name {
4456 #[expect(
4457 clippy::unwrap_used,
4458 reason = "This is a constant and is known to be safe, verified by a test"
4459 )]
4460 ast::Name::unqualified_name("decimal".parse().unwrap())
4461}
4462
4463fn ip_extension_name() -> ast::Name {
4464 #[expect(
4465 clippy::unwrap_used,
4466 reason = "This is a constant and is known to be safe, verified by a test"
4467 )]
4468 ast::Name::unqualified_name("ip".parse().unwrap())
4469}
4470
4471fn datetime_extension_name() -> ast::Name {
4472 #[expect(
4473 clippy::unwrap_used,
4474 reason = "This is a constant and is known to be safe, verified by a test"
4475 )]
4476 ast::Name::unqualified_name("datetime".parse().unwrap())
4477}
4478
4479fn duration_extension_name() -> ast::Name {
4480 #[expect(
4481 clippy::unwrap_used,
4482 reason = "This is a constant and is known to be safe, verified by a test"
4483 )]
4484 ast::Name::unqualified_name("duration".parse().unwrap())
4485}
4486
4487impl FromStr for RestrictedExpression {
4488 type Err = RestrictedExpressionParseError;
4489
4490 fn from_str(expression: &str) -> Result<Self, Self::Err> {
4492 ast::RestrictedExpr::from_str(expression)
4493 .map(RestrictedExpression)
4494 .map_err(Into::into)
4495 }
4496}
4497
4498#[doc = include_str!("../experimental_warning.md")]
4503#[cfg(feature = "partial-eval")]
4504#[derive(Debug, Clone)]
4505pub struct RequestBuilder<S> {
4506 principal: ast::EntityUIDEntry,
4507 action: ast::EntityUIDEntry,
4508 resource: ast::EntityUIDEntry,
4509 context: Option<ast::Context>,
4511 schema: S,
4512}
4513
4514#[doc = include_str!("../experimental_warning.md")]
4516#[cfg(feature = "partial-eval")]
4517#[derive(Debug, Clone, Copy)]
4518pub struct UnsetSchema;
4519
4520#[cfg(feature = "partial-eval")]
4521impl Default for RequestBuilder<UnsetSchema> {
4522 fn default() -> Self {
4523 Self {
4524 principal: ast::EntityUIDEntry::unknown(),
4525 action: ast::EntityUIDEntry::unknown(),
4526 resource: ast::EntityUIDEntry::unknown(),
4527 context: None,
4528 schema: UnsetSchema,
4529 }
4530 }
4531}
4532
4533#[cfg(feature = "partial-eval")]
4534impl<S> RequestBuilder<S> {
4535 #[must_use]
4540 pub fn principal(self, principal: EntityUid) -> Self {
4541 Self {
4542 principal: ast::EntityUIDEntry::known(principal.into(), None),
4543 ..self
4544 }
4545 }
4546
4547 #[must_use]
4551 pub fn unknown_principal_with_type(self, principal_type: EntityTypeName) -> Self {
4552 Self {
4553 principal: ast::EntityUIDEntry::unknown_with_type(principal_type.0, None),
4554 ..self
4555 }
4556 }
4557
4558 #[must_use]
4563 pub fn action(self, action: EntityUid) -> Self {
4564 Self {
4565 action: ast::EntityUIDEntry::known(action.into(), None),
4566 ..self
4567 }
4568 }
4569
4570 #[must_use]
4575 pub fn resource(self, resource: EntityUid) -> Self {
4576 Self {
4577 resource: ast::EntityUIDEntry::known(resource.into(), None),
4578 ..self
4579 }
4580 }
4581
4582 #[must_use]
4586 pub fn unknown_resource_with_type(self, resource_type: EntityTypeName) -> Self {
4587 Self {
4588 resource: ast::EntityUIDEntry::unknown_with_type(resource_type.0, None),
4589 ..self
4590 }
4591 }
4592
4593 #[must_use]
4595 pub fn context(self, context: Context) -> Self {
4596 Self {
4597 context: Some(context.0),
4598 ..self
4599 }
4600 }
4601}
4602
4603#[cfg(feature = "partial-eval")]
4604impl RequestBuilder<UnsetSchema> {
4605 #[must_use]
4607 pub fn schema(self, schema: &Schema) -> RequestBuilder<&Schema> {
4608 RequestBuilder {
4609 principal: self.principal,
4610 action: self.action,
4611 resource: self.resource,
4612 context: self.context,
4613 schema,
4614 }
4615 }
4616
4617 pub fn build(self) -> Request {
4619 Request(ast::Request::new_unchecked(
4620 self.principal,
4621 self.action,
4622 self.resource,
4623 self.context,
4624 ))
4625 }
4626}
4627
4628#[cfg(feature = "partial-eval")]
4629impl RequestBuilder<&Schema> {
4630 pub fn build(self) -> Result<Request, RequestValidationError> {
4632 Ok(Request(ast::Request::new_with_unknowns(
4633 self.principal,
4634 self.action,
4635 self.resource,
4636 self.context,
4637 Some(&self.schema.0),
4638 Extensions::all_available(),
4639 )?))
4640 }
4641}
4642
4643#[repr(transparent)]
4652#[derive(Debug, Clone, RefCast)]
4653pub struct Request(pub(crate) ast::Request);
4654
4655#[doc(hidden)] impl AsRef<ast::Request> for Request {
4657 fn as_ref(&self) -> &ast::Request {
4658 &self.0
4659 }
4660}
4661
4662#[doc(hidden)]
4663impl From<ast::Request> for Request {
4664 fn from(req: ast::Request) -> Self {
4665 Self(req)
4666 }
4667}
4668
4669impl PartialEq for Request {
4670 fn eq(&self, other: &Self) -> bool {
4671 self.principal() == other.principal()
4672 && self.action() == other.action()
4673 && self.resource() == other.resource()
4674 && self.context() == other.context()
4675 }
4676}
4677
4678impl Request {
4679 #[doc = include_str!("../experimental_warning.md")]
4681 #[cfg(feature = "partial-eval")]
4682 pub fn builder() -> RequestBuilder<UnsetSchema> {
4683 RequestBuilder::default()
4684 }
4685
4686 pub fn new(
4699 principal: EntityUid,
4700 action: EntityUid,
4701 resource: EntityUid,
4702 context: Context,
4703 schema: Option<&Schema>,
4704 ) -> Result<Self, RequestValidationError> {
4705 Ok(Self(ast::Request::new(
4706 (principal.into(), None),
4707 (action.into(), None),
4708 (resource.into(), None),
4709 context.0,
4710 schema.map(|schema| &schema.0),
4711 Extensions::all_available(),
4712 )?))
4713 }
4714
4715 pub fn context(&self) -> Option<&Context> {
4718 self.0.context().map(Context::ref_cast)
4719 }
4720
4721 pub fn principal(&self) -> Option<&EntityUid> {
4724 match self.0.principal() {
4725 ast::EntityUIDEntry::Known { euid, .. } => Some(EntityUid::ref_cast(euid.as_ref())),
4726 ast::EntityUIDEntry::Unknown { .. } => None,
4727 }
4728 }
4729
4730 pub fn action(&self) -> Option<&EntityUid> {
4733 match self.0.action() {
4734 ast::EntityUIDEntry::Known { euid, .. } => Some(EntityUid::ref_cast(euid.as_ref())),
4735 ast::EntityUIDEntry::Unknown { .. } => None,
4736 }
4737 }
4738
4739 pub fn resource(&self) -> Option<&EntityUid> {
4742 match self.0.resource() {
4743 ast::EntityUIDEntry::Known { euid, .. } => Some(EntityUid::ref_cast(euid.as_ref())),
4744 ast::EntityUIDEntry::Unknown { .. } => None,
4745 }
4746 }
4747}
4748
4749#[repr(transparent)]
4751#[derive(Debug, Clone, PartialEq, Eq, RefCast)]
4752pub struct Context(ast::Context);
4753
4754#[doc(hidden)] impl AsRef<ast::Context> for Context {
4756 fn as_ref(&self) -> &ast::Context {
4757 &self.0
4758 }
4759}
4760
4761impl Context {
4762 pub fn empty() -> Self {
4769 Self(ast::Context::empty())
4770 }
4771
4772 pub fn from_pairs(
4789 pairs: impl IntoIterator<Item = (String, RestrictedExpression)>,
4790 ) -> Result<Self, ContextCreationError> {
4791 Ok(Self(ast::Context::from_pairs(
4792 pairs.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
4793 Extensions::all_available(),
4794 )?))
4795 }
4796
4797 pub fn get(&self, key: &str) -> Option<EvalResult> {
4821 match &self.0 {
4822 ast::Context::Value(map) => map.get(key).map(|v| EvalResult::from(v.clone())),
4823 ast::Context::RestrictedResidual(_) => None,
4824 }
4825 }
4826
4827 pub fn from_json_str(
4857 json: &str,
4858 schema: Option<(&Schema, &EntityUid)>,
4859 ) -> Result<Self, ContextJsonError> {
4860 let schema = schema
4861 .map(|(s, uid)| Self::get_context_schema(s, uid))
4862 .transpose()?;
4863 let context = cedar_policy_core::entities::ContextJsonParser::new(
4864 schema.as_ref(),
4865 Extensions::all_available(),
4866 )
4867 .from_json_str(json)?;
4868 Ok(Self(context))
4869 }
4870
4871 pub fn from_json_value(
4921 json: serde_json::Value,
4922 schema: Option<(&Schema, &EntityUid)>,
4923 ) -> Result<Self, ContextJsonError> {
4924 let schema = schema
4925 .map(|(s, uid)| Self::get_context_schema(s, uid))
4926 .transpose()?;
4927 let context = cedar_policy_core::entities::ContextJsonParser::new(
4928 schema.as_ref(),
4929 Extensions::all_available(),
4930 )
4931 .from_json_value(json)?;
4932 Ok(Self(context))
4933 }
4934
4935 pub fn from_json_file(
4967 json: impl std::io::Read,
4968 schema: Option<(&Schema, &EntityUid)>,
4969 ) -> Result<Self, ContextJsonError> {
4970 let schema = schema
4971 .map(|(s, uid)| Self::get_context_schema(s, uid))
4972 .transpose()?;
4973 let context = cedar_policy_core::entities::ContextJsonParser::new(
4974 schema.as_ref(),
4975 Extensions::all_available(),
4976 )
4977 .from_json_file(json)?;
4978 Ok(Self(context))
4979 }
4980
4981 pub fn to_json_value(
4983 &self,
4984 ) -> Result<serde_json::Value, entities_json_errors::JsonSerializationError> {
4985 self.0.to_json_value()
4986 }
4987
4988 fn get_context_schema(
4990 schema: &Schema,
4991 action: &EntityUid,
4992 ) -> Result<impl ContextSchema, ContextJsonError> {
4993 cedar_policy_core::validator::context_schema_for_action(&schema.0, action.as_ref())
4994 .ok_or_else(|| ContextJsonError::missing_action(action.clone()))
4995 }
4996
4997 pub fn merge(
5001 self,
5002 other_context: impl IntoIterator<Item = (String, RestrictedExpression)>,
5003 ) -> Result<Self, ContextCreationError> {
5004 Self::from_pairs(self.into_iter().chain(other_context))
5005 }
5006
5007 pub fn validate(
5014 &self,
5015 schema: &crate::Schema,
5016 action: &EntityUid,
5017 ) -> std::result::Result<(), RequestValidationError> {
5018 Ok(RequestSchema::validate_context(
5020 &schema.0,
5021 &self.0,
5022 action.as_ref(),
5023 Extensions::all_available(),
5024 )?)
5025 }
5026}
5027
5028mod context {
5030 use super::{ast, RestrictedExpression};
5031
5032 #[derive(Debug)]
5034 pub struct IntoIter {
5035 pub(super) inner: <ast::Context as IntoIterator>::IntoIter,
5036 }
5037
5038 impl Iterator for IntoIter {
5039 type Item = (String, RestrictedExpression);
5040
5041 fn next(&mut self) -> Option<Self::Item> {
5042 self.inner
5043 .next()
5044 .map(|(k, v)| (k.to_string(), RestrictedExpression(v)))
5045 }
5046 }
5047}
5048
5049impl IntoIterator for Context {
5050 type Item = (String, RestrictedExpression);
5051
5052 type IntoIter = context::IntoIter;
5053
5054 fn into_iter(self) -> Self::IntoIter {
5055 Self::IntoIter {
5056 inner: self.0.into_iter(),
5057 }
5058 }
5059}
5060
5061#[doc(hidden)]
5062impl From<ast::Context> for Context {
5063 fn from(c: ast::Context) -> Self {
5064 Self(c)
5065 }
5066}
5067
5068impl std::fmt::Display for Request {
5069 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5070 write!(f, "{}", self.0)
5071 }
5072}
5073
5074impl std::fmt::Display for Context {
5075 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5076 write!(f, "{}", self.0)
5077 }
5078}
5079
5080#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
5082pub enum EvalResult {
5083 Bool(bool),
5085 Long(ast::Integer),
5087 String(String),
5089 EntityUid(EntityUid),
5091 Set(Set),
5093 Record(Record),
5095 ExtensionValue(String),
5097 }
5099
5100#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord)]
5102pub struct Set(BTreeSet<EvalResult>);
5103
5104impl Set {
5105 pub fn iter(&self) -> impl Iterator<Item = &EvalResult> {
5107 self.0.iter()
5108 }
5109
5110 pub fn contains(&self, elem: &EvalResult) -> bool {
5112 self.0.contains(elem)
5113 }
5114
5115 pub fn len(&self) -> usize {
5117 self.0.len()
5118 }
5119
5120 pub fn is_empty(&self) -> bool {
5122 self.0.is_empty()
5123 }
5124}
5125
5126#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord)]
5128pub struct Record(BTreeMap<String, EvalResult>);
5129
5130impl Record {
5131 pub fn iter(&self) -> impl Iterator<Item = (&String, &EvalResult)> {
5133 self.0.iter()
5134 }
5135
5136 pub fn contains_attribute(&self, key: impl AsRef<str>) -> bool {
5138 self.0.contains_key(key.as_ref())
5139 }
5140
5141 pub fn get(&self, key: impl AsRef<str>) -> Option<&EvalResult> {
5143 self.0.get(key.as_ref())
5144 }
5145
5146 pub fn len(&self) -> usize {
5148 self.0.len()
5149 }
5150
5151 pub fn is_empty(&self) -> bool {
5153 self.0.is_empty()
5154 }
5155}
5156
5157#[doc(hidden)]
5158impl From<ast::Value> for EvalResult {
5159 fn from(v: ast::Value) -> Self {
5160 match v.value {
5161 ast::ValueKind::Lit(ast::Literal::Bool(b)) => Self::Bool(b),
5162 ast::ValueKind::Lit(ast::Literal::Long(i)) => Self::Long(i),
5163 ast::ValueKind::Lit(ast::Literal::String(s)) => Self::String(s.to_string()),
5164 ast::ValueKind::Lit(ast::Literal::EntityUID(e)) => {
5165 Self::EntityUid(ast::EntityUID::clone(&e).into())
5166 }
5167 ast::ValueKind::Set(set) => Self::Set(Set(set
5168 .authoritative
5169 .iter()
5170 .map(|v| v.clone().into())
5171 .collect())),
5172 ast::ValueKind::Record(record) => Self::Record(Record(
5173 record
5174 .iter()
5175 .map(|(k, v)| (k.to_string(), v.clone().into()))
5176 .collect(),
5177 )),
5178 ast::ValueKind::ExtensionValue(ev) => {
5179 Self::ExtensionValue(RestrictedExpr::from(ev.as_ref().clone()).to_string())
5180 }
5181 }
5182 }
5183}
5184
5185#[doc(hidden)]
5186#[expect(
5187 clippy::fallible_impl_from,
5188 reason = "see the panic safety comments below"
5189)]
5190impl From<EvalResult> for Expression {
5191 fn from(res: EvalResult) -> Self {
5192 match res {
5193 EvalResult::Bool(b) => Self::new_bool(b),
5194 EvalResult::Long(l) => Self::new_long(l),
5195 EvalResult::String(s) => Self::new_string(s),
5196 EvalResult::EntityUid(eid) => {
5197 Self::from(ast::Expr::from(ast::Value::from(ast::EntityUID::from(eid))))
5198 }
5199 EvalResult::Set(set) => Self::new_set(set.iter().cloned().map(Self::from)),
5200 EvalResult::Record(r) =>
5201 {
5202 #[expect(
5203 clippy::unwrap_used,
5204 reason = "record originates from EvalResult so should not panic when reconstructing as an Expression"
5205 )]
5206 Self::new_record(r.iter().map(|(k, v)| (k.clone(), Self::from(v.clone())))).unwrap()
5207 }
5208 EvalResult::ExtensionValue(s) => {
5209 #[expect(
5210 clippy::unwrap_used,
5211 reason = "the string s is constructed using RestrictedExpr::to_string() so should not panic when being parsed back into a RestrictedExpr"
5212 )]
5213 let expr: ast::Expr = ast::RestrictedExpr::from_str(&s).unwrap().into();
5214 Self::from(expr)
5215 }
5216 }
5217 }
5218}
5219
5220impl std::fmt::Display for EvalResult {
5221 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5222 match self {
5223 Self::Bool(b) => write!(f, "{b}"),
5224 Self::Long(l) => write!(f, "{l}"),
5225 Self::String(s) => write!(f, "\"{}\"", s.escape_debug()),
5226 Self::EntityUid(uid) => write!(f, "{uid}"),
5227 Self::Set(s) => {
5228 write!(f, "[")?;
5229 for (i, ev) in s.iter().enumerate() {
5230 write!(f, "{ev}")?;
5231 if (i + 1) < s.len() {
5232 write!(f, ", ")?;
5233 }
5234 }
5235 write!(f, "]")?;
5236 Ok(())
5237 }
5238 Self::Record(r) => {
5239 write!(f, "{{")?;
5240 for (i, (k, v)) in r.iter().enumerate() {
5241 write!(f, "\"{}\": {v}", k.escape_debug())?;
5242 if (i + 1) < r.len() {
5243 write!(f, ", ")?;
5244 }
5245 }
5246 write!(f, "}}")?;
5247 Ok(())
5248 }
5249 Self::ExtensionValue(s) => write!(f, "{s}"),
5250 }
5251 }
5252}
5253
5254pub fn eval_expression(
5259 request: &Request,
5260 entities: &Entities,
5261 expr: &Expression,
5262) -> Result<EvalResult, EvaluationError> {
5263 let all_ext = Extensions::all_available();
5264 let eval = Evaluator::new(request.0.clone(), &entities.0, all_ext);
5265 Ok(EvalResult::from(
5266 eval.interpret(&expr.0, &ast::SlotEnv::new())?,
5268 ))
5269}
5270
5271#[cfg(feature = "tpe")]
5272pub use tpe::*;
5273
5274#[cfg(feature = "tpe")]
5275mod tpe {
5276 use std::collections::{BTreeMap, HashMap, HashSet};
5277 use std::sync::Arc;
5278
5279 use cedar_policy_core::ast::{self, Value};
5280 use cedar_policy_core::authorizer::Decision;
5281 use cedar_policy_core::batched_evaluator::is_authorized_batched;
5282 use cedar_policy_core::batched_evaluator::{
5283 err::BatchedEvalError, EntityLoader as EntityLoaderInternal,
5284 };
5285 use cedar_policy_core::evaluator::{EvaluationError, RestrictedEvaluator};
5286 use cedar_policy_core::extensions::Extensions;
5287 use cedar_policy_core::tpe;
5288 use itertools::Itertools;
5289 use ref_cast::RefCast;
5290 use smol_str::SmolStr;
5291
5292 use crate::{
5293 api, tpe_err, Authorizer, Context, Entities, EntityId, EntityTypeName, EntityUid,
5294 PartialEntityError, PartialRequestCreationError, PermissionQueryError, Policy, PolicySet,
5295 Request, RequestValidationError, RestrictedExpression, Schema,
5296 };
5297 use crate::{Entity, TpeReauthorizationError};
5298
5299 #[doc = include_str!("../experimental_warning.md")]
5302 #[repr(transparent)]
5303 #[derive(Debug, Clone, RefCast)]
5304 pub struct PartialEntityUid(pub(crate) tpe::request::PartialEntityUID);
5305
5306 #[doc(hidden)]
5307 impl AsRef<tpe::request::PartialEntityUID> for PartialEntityUid {
5308 fn as_ref(&self) -> &tpe::request::PartialEntityUID {
5309 &self.0
5310 }
5311 }
5312
5313 impl PartialEntityUid {
5314 pub fn new(ty: EntityTypeName, id: Option<EntityId>) -> Self {
5316 Self(tpe::request::PartialEntityUID {
5317 ty: ty.0,
5318 eid: id.map(|id| <EntityId as AsRef<ast::Eid>>::as_ref(&id).clone()),
5319 })
5320 }
5321
5322 pub fn from_concrete(euid: EntityUid) -> Self {
5324 let (ty, eid) = euid.0.components();
5325 Self(tpe::request::PartialEntityUID { ty, eid: Some(eid) })
5326 }
5327 }
5328
5329 #[doc = include_str!("../experimental_warning.md")]
5333 #[repr(transparent)]
5334 #[derive(Debug, Clone, RefCast)]
5335 pub struct PartialRequest(pub(crate) tpe::request::PartialRequest);
5336
5337 #[doc(hidden)]
5338 impl AsRef<tpe::request::PartialRequest> for PartialRequest {
5339 fn as_ref(&self) -> &tpe::request::PartialRequest {
5340 &self.0
5341 }
5342 }
5343
5344 impl PartialRequest {
5345 pub fn new(
5347 principal: PartialEntityUid,
5348 action: EntityUid,
5349 resource: PartialEntityUid,
5350 context: Option<Context>,
5351 schema: &Schema,
5352 ) -> Result<Self, PartialRequestCreationError> {
5353 let context = context
5354 .map(|c| match c.0 {
5355 ast::Context::RestrictedResidual(_) => {
5356 Err(PartialRequestCreationError::ContextContainsUnknowns)
5357 }
5358 ast::Context::Value(m) => Ok(m),
5359 })
5360 .transpose()?;
5361 tpe::request::PartialRequest::new(principal.0, action.0, resource.0, context, &schema.0)
5362 .map(Self)
5363 .map_err(|e| PartialRequestCreationError::Validation(e.into()))
5364 }
5365 }
5366
5367 #[doc = include_str!("../experimental_warning.md")]
5369 #[repr(transparent)]
5370 #[derive(Debug, Clone, RefCast)]
5371 pub struct ResourceQueryRequest(pub(crate) PartialRequest);
5372
5373 impl ResourceQueryRequest {
5374 pub fn new(
5376 principal: EntityUid,
5377 action: EntityUid,
5378 resource: EntityTypeName,
5379 context: Context,
5380 schema: &Schema,
5381 ) -> Result<Self, PartialRequestCreationError> {
5382 PartialRequest::new(
5383 PartialEntityUid(principal.0.into()),
5384 action,
5385 PartialEntityUid::new(resource, None),
5386 Some(context),
5387 schema,
5388 )
5389 .map(Self)
5390 }
5391
5392 pub fn to_request(
5394 &self,
5395 resource_id: EntityId,
5396 schema: Option<&Schema>,
5397 ) -> Result<Request, RequestValidationError> {
5398 #[expect(
5399 clippy::unwrap_used,
5400 reason = "various fields are validated through the constructor"
5401 )]
5402 Request::new(
5403 EntityUid(self.0 .0.get_principal().try_into().unwrap()),
5404 EntityUid(self.0 .0.get_action()),
5405 EntityUid::from_type_name_and_id(
5406 EntityTypeName(self.0 .0.get_resource_type()),
5407 resource_id,
5408 ),
5409 Context::from_pairs(
5410 self.0
5411 .0
5412 .get_context_attrs()
5413 .unwrap()
5414 .iter()
5415 .map(|(a, v)| (a.to_string(), RestrictedExpression(v.clone().into()))),
5416 )
5417 .unwrap(),
5418 schema,
5419 )
5420 }
5421 }
5422
5423 #[doc = include_str!("../experimental_warning.md")]
5425 #[repr(transparent)]
5426 #[derive(Debug, Clone, RefCast)]
5427 pub struct PrincipalQueryRequest(pub(crate) PartialRequest);
5428
5429 impl PrincipalQueryRequest {
5430 pub fn new(
5432 principal: EntityTypeName,
5433 action: EntityUid,
5434 resource: EntityUid,
5435 context: Context,
5436 schema: &Schema,
5437 ) -> Result<Self, PartialRequestCreationError> {
5438 PartialRequest::new(
5439 PartialEntityUid::new(principal, None),
5440 action,
5441 PartialEntityUid(resource.0.into()),
5442 Some(context),
5443 schema,
5444 )
5445 .map(Self)
5446 }
5447
5448 pub fn to_request(
5450 &self,
5451 principal_id: EntityId,
5452 schema: Option<&Schema>,
5453 ) -> Result<Request, RequestValidationError> {
5454 #[expect(
5455 clippy::unwrap_used,
5456 reason = "various fields are validated through the constructor"
5457 )]
5458 Request::new(
5459 EntityUid::from_type_name_and_id(
5460 EntityTypeName(self.0 .0.get_principal_type()),
5461 principal_id,
5462 ),
5463 EntityUid(self.0 .0.get_action()),
5464 EntityUid(self.0 .0.get_resource().try_into().unwrap()),
5465 Context::from_pairs(
5466 self.0
5467 .0
5468 .get_context_attrs()
5469 .unwrap()
5470 .iter()
5471 .map(|(a, v)| (a.to_string(), RestrictedExpression(v.clone().into()))),
5472 )
5473 .unwrap(),
5474 schema,
5475 )
5476 }
5477 }
5478
5479 #[doc = include_str!("../experimental_warning.md")]
5484 #[derive(Debug, Clone)]
5485 pub struct ActionQueryRequest {
5486 principal: PartialEntityUid,
5487 resource: PartialEntityUid,
5488 context: Option<Arc<BTreeMap<SmolStr, Value>>>,
5489 schema: Schema,
5490 }
5491
5492 impl ActionQueryRequest {
5493 pub fn new(
5495 principal: PartialEntityUid,
5496 resource: PartialEntityUid,
5497 context: Option<Context>,
5498 schema: Schema,
5499 ) -> Result<Self, PartialRequestCreationError> {
5500 let context = context
5501 .map(|c| match c.0 {
5502 ast::Context::RestrictedResidual(_) => {
5503 Err(PartialRequestCreationError::ContextContainsUnknowns)
5504 }
5505 ast::Context::Value(m) => Ok(m),
5506 })
5507 .transpose()?;
5508 Ok(Self {
5509 principal,
5510 resource,
5511 context,
5512 schema,
5513 })
5514 }
5515
5516 fn partial_request(
5517 &self,
5518 action: EntityUid,
5519 ) -> Result<PartialRequest, cedar_policy_core::validator::RequestValidationError> {
5520 tpe::request::PartialRequest::new(
5521 self.principal.0.clone(),
5522 action.0,
5523 self.resource.0.clone(),
5524 self.context.clone(),
5525 &self.schema.0,
5526 )
5527 .map(PartialRequest)
5528 }
5529 }
5530
5531 #[doc = include_str!("../experimental_warning.md")]
5533 #[repr(transparent)]
5534 #[derive(Debug, Clone, RefCast)]
5535 pub struct PartialEntity(pub(crate) tpe::entities::PartialEntity);
5536
5537 impl PartialEntity {
5538 pub fn new(
5540 uid: EntityUid,
5541 attrs: Option<BTreeMap<SmolStr, RestrictedExpression>>,
5542 ancestors: Option<HashSet<EntityUid>>,
5543 tags: Option<BTreeMap<SmolStr, RestrictedExpression>>,
5544 schema: &Schema,
5545 ) -> Result<Self, PartialEntityError> {
5546 Ok(Self(tpe::entities::PartialEntity::new(
5547 uid.0,
5548 attrs
5549 .map(|ps| {
5550 ps.into_iter()
5551 .map(|(k, v)| {
5552 Ok((
5553 k,
5554 RestrictedEvaluator::new(Extensions::all_available())
5555 .interpret(v.0.as_borrowed())?,
5556 ))
5557 })
5558 .collect::<Result<BTreeMap<_, _>, EvaluationError>>()
5559 })
5560 .transpose()?,
5561 ancestors.map(|s| s.into_iter().map(|e| e.0).collect()),
5562 tags.map(|ps| {
5563 ps.into_iter()
5564 .map(|(k, v)| {
5565 Ok((
5566 k,
5567 RestrictedEvaluator::new(Extensions::all_available())
5568 .interpret(v.0.as_borrowed())?,
5569 ))
5570 })
5571 .collect::<Result<BTreeMap<_, _>, EvaluationError>>()
5572 })
5573 .transpose()?,
5574 &schema.0,
5575 )?))
5576 }
5577 }
5578
5579 #[doc = include_str!("../experimental_warning.md")]
5581 #[repr(transparent)]
5582 #[derive(Debug, Clone, RefCast)]
5583 pub struct PartialEntities(pub(crate) tpe::entities::PartialEntities);
5584
5585 #[doc(hidden)]
5586 impl AsRef<tpe::entities::PartialEntities> for PartialEntities {
5587 fn as_ref(&self) -> &tpe::entities::PartialEntities {
5588 &self.0
5589 }
5590 }
5591
5592 impl PartialEntities {
5593 pub fn from_json_value(
5597 value: serde_json::Value,
5598 schema: &Schema,
5599 ) -> Result<Self, tpe_err::EntitiesError> {
5600 tpe::entities::PartialEntities::from_json_value(value, &schema.0).map(Self)
5601 }
5602
5603 pub fn from_concrete(
5605 entities: Entities,
5606 schema: &Schema,
5607 ) -> Result<Self, tpe_err::EntitiesError> {
5608 tpe::entities::PartialEntities::from_concrete(entities.0, &schema.0).map(Self)
5609 }
5610
5611 pub fn empty() -> Self {
5613 Self(tpe::entities::PartialEntities::new())
5614 }
5615
5616 pub fn from_partial_entities(
5618 entities: impl IntoIterator<Item = PartialEntity>,
5619 schema: &Schema,
5620 ) -> Result<Self, tpe_err::EntitiesError> {
5621 Ok(Self(tpe::entities::PartialEntities::from_entities(
5622 entities.into_iter().map(|entity| entity.0),
5623 &schema.0,
5624 )?))
5625 }
5626 }
5627
5628 #[doc = include_str!("../experimental_warning.md")]
5630 #[repr(transparent)]
5631 #[derive(Debug, Clone, RefCast)]
5632 pub struct TpeResponse<'a>(pub(crate) tpe::response::Response<'a>);
5633
5634 #[doc(hidden)]
5635 impl<'a> AsRef<tpe::response::Response<'a>> for TpeResponse<'a> {
5636 fn as_ref(&self) -> &tpe::response::Response<'a> {
5637 &self.0
5638 }
5639 }
5640
5641 impl TpeResponse<'_> {
5642 pub fn decision(&self) -> Option<Decision> {
5644 self.0.decision()
5645 }
5646
5647 pub fn reauthorize(
5649 &self,
5650 request: &Request,
5651 entities: &Entities,
5652 ) -> Result<api::Response, TpeReauthorizationError> {
5653 self.0
5654 .reauthorize(&request.0, &entities.0)
5655 .map(Into::into)
5656 .map_err(Into::into)
5657 }
5658
5659 pub fn residual_policies(&self) -> impl Iterator<Item = Policy> + '_ {
5666 self.0
5667 .residual_policies()
5668 .map(|p| Policy::from_ast(p.clone().into()))
5669 }
5670
5671 pub fn nontrivial_residual_policies(&'_ self) -> impl Iterator<Item = Policy> + '_ {
5678 self.0
5679 .residual_permits()
5680 .chain(self.0.residual_forbids())
5681 .map(|p| Policy::from_ast(p.clone().into()))
5682 }
5683 }
5684
5685 #[doc = include_str!("../experimental_warning.md")]
5692 pub trait EntityLoader {
5693 fn load_entities(
5697 &mut self,
5698 uids: &HashSet<EntityUid>,
5699 ) -> HashMap<EntityUid, Option<Entity>>;
5700 }
5701
5702 struct EntityLoaderWrapper<'a>(&'a mut dyn EntityLoader);
5704
5705 impl EntityLoaderInternal for EntityLoaderWrapper<'_> {
5706 fn load_entities(
5707 &mut self,
5708 uids: &HashSet<ast::EntityUID>,
5709 ) -> HashMap<ast::EntityUID, Option<ast::Entity>> {
5710 let ids = uids
5711 .iter()
5712 .map(|id| EntityUid::ref_cast(id).clone())
5713 .collect();
5714 self.0
5715 .load_entities(&ids)
5716 .into_iter()
5717 .map(|(uid, entity)| (uid.0, entity.map(|e| e.0)))
5718 .collect()
5719 }
5720 }
5721
5722 #[doc = include_str!("../experimental_warning.md")]
5724 #[derive(Debug)]
5725
5726 pub struct TestEntityLoader<'a> {
5727 entities: &'a Entities,
5728 }
5729
5730 impl<'a> TestEntityLoader<'a> {
5731 pub fn new(entities: &'a Entities) -> Self {
5733 Self { entities }
5734 }
5735 }
5736
5737 impl EntityLoader for TestEntityLoader<'_> {
5738 fn load_entities(
5739 &mut self,
5740 uids: &HashSet<EntityUid>,
5741 ) -> HashMap<EntityUid, Option<Entity>> {
5742 uids.iter()
5743 .map(|uid| {
5744 let entity = self.entities.get(uid).cloned();
5745 (uid.clone(), entity)
5746 })
5747 .collect()
5748 }
5749 }
5750
5751 impl PolicySet {
5752 #[doc = include_str!("../experimental_warning.md")]
5756 pub fn tpe<'a>(
5757 &self,
5758 request: &'a PartialRequest,
5759 entities: &'a PartialEntities,
5760 schema: &'a Schema,
5761 ) -> Result<TpeResponse<'a>, tpe_err::TpeError> {
5762 use cedar_policy_core::tpe::is_authorized;
5763 let ps = &self.ast;
5764 let res = is_authorized(ps, &request.0, &entities.0, &schema.0)?;
5765 Ok(TpeResponse(res))
5766 }
5767
5768 #[doc = include_str!("../experimental_warning.md")]
5777 pub fn is_authorized_batched(
5778 &self,
5779 query: &Request,
5780 schema: &Schema,
5781 loader: &mut dyn EntityLoader,
5782 max_iters: u32,
5783 ) -> Result<Decision, BatchedEvalError> {
5784 is_authorized_batched(
5785 &query.0,
5786 &self.ast,
5787 &schema.0,
5788 &mut EntityLoaderWrapper(loader),
5789 max_iters,
5790 )
5791 }
5792
5793 #[doc = include_str!("../experimental_warning.md")]
5795 pub fn query_resource(
5796 &self,
5797 request: &ResourceQueryRequest,
5798 entities: &Entities,
5799 schema: &Schema,
5800 ) -> Result<impl Iterator<Item = EntityUid>, PermissionQueryError> {
5801 let partial_entities = PartialEntities::from_concrete(entities.clone(), schema)?;
5802 let residuals = self.tpe(&request.0, &partial_entities, schema)?;
5803 #[expect(
5804 clippy::unwrap_used,
5805 reason = "policy set construction should succeed because there shouldn't be any policy id conflicts"
5806 )]
5807 let policies = &Self::from_policies(
5808 residuals
5809 .0
5810 .residual_policies()
5811 .map(|p| Policy::from_ast(p.clone().into())),
5812 )
5813 .unwrap();
5814 #[expect(
5815 clippy::unwrap_used,
5816 reason = "request construction should succeed because each entity passes validation"
5817 )]
5818 match residuals.decision() {
5819 Some(Decision::Allow) => Ok(entities
5820 .iter()
5821 .filter(|entity| {
5822 entity.0.uid().entity_type() == &request.0 .0.get_resource_type()
5823 })
5824 .map(super::Entity::uid)
5825 .collect_vec()
5826 .into_iter()),
5827 Some(Decision::Deny) => Ok(vec![].into_iter()),
5828 None => Ok(entities
5829 .iter()
5830 .filter(|entity| {
5831 entity.0.uid().entity_type() == &request.0 .0.get_resource_type()
5832 })
5833 .filter(|entity| {
5834 let authorizer = Authorizer::new();
5835 authorizer
5836 .is_authorized(
5837 &request.to_request(entity.uid().id().clone(), None).unwrap(),
5838 policies,
5839 entities,
5840 )
5841 .decision
5842 == Decision::Allow
5843 })
5844 .map(super::Entity::uid)
5845 .collect_vec()
5846 .into_iter()),
5847 }
5848 }
5849
5850 #[doc = include_str!("../experimental_warning.md")]
5852 pub fn query_principal(
5853 &self,
5854 request: &PrincipalQueryRequest,
5855 entities: &Entities,
5856 schema: &Schema,
5857 ) -> Result<impl Iterator<Item = EntityUid>, PermissionQueryError> {
5858 let partial_entities = PartialEntities::from_concrete(entities.clone(), schema)?;
5859 let residuals = self.tpe(&request.0, &partial_entities, schema)?;
5860 #[expect(
5861 clippy::unwrap_used,
5862 reason = "policy set construction should succeed because there shouldn't be any policy id conflicts"
5863 )]
5864 let policies = &Self::from_policies(
5865 residuals
5866 .0
5867 .residual_policies()
5868 .map(|p| Policy::from_ast(p.clone().into())),
5869 )
5870 .unwrap();
5871 #[expect(
5872 clippy::unwrap_used,
5873 reason = "request construction should succeed because each entity passes validation"
5874 )]
5875 match residuals.decision() {
5876 Some(Decision::Allow) => Ok(entities
5877 .iter()
5878 .filter(|entity| {
5879 entity.0.uid().entity_type() == &request.0 .0.get_principal_type()
5880 })
5881 .map(super::Entity::uid)
5882 .collect_vec()
5883 .into_iter()),
5884 Some(Decision::Deny) => Ok(vec![].into_iter()),
5885 None => Ok(entities
5886 .iter()
5887 .filter(|entity| {
5888 entity.0.uid().entity_type() == &request.0 .0.get_principal_type()
5889 })
5890 .filter(|entity| {
5891 let authorizer = Authorizer::new();
5892 authorizer
5893 .is_authorized(
5894 &request.to_request(entity.uid().id().clone(), None).unwrap(),
5895 policies,
5896 entities,
5897 )
5898 .decision
5899 == Decision::Allow
5900 })
5901 .map(super::Entity::uid)
5902 .collect_vec()
5903 .into_iter()),
5904 }
5905 }
5906
5907 #[doc = include_str!("../experimental_warning.md")]
5973 pub fn query_action<'a>(
5974 &self,
5975 request: &'a ActionQueryRequest,
5976 entities: &PartialEntities,
5977 ) -> Result<impl Iterator<Item = (&'a EntityUid, Option<Decision>)>, PermissionQueryError>
5978 {
5979 let mut authorized_actions = Vec::new();
5980 for action in request
5986 .schema
5987 .0
5988 .actions_for_principal_and_resource(&request.principal.0.ty, &request.resource.0.ty)
5989 {
5990 if let Ok(partial_request) = request.partial_request(action.clone().into()) {
5994 let decision = self
5995 .tpe(&partial_request, entities, &request.schema)?
5996 .decision();
5997 if decision != Some(Decision::Deny) {
5998 authorized_actions.push((RefCast::ref_cast(action), decision));
5999 }
6000 }
6001 }
6002 Ok(authorized_actions.into_iter())
6003 }
6004 }
6005}
6006
6007#[cfg(test)]
6009mod test_access {
6010 use cedar_policy_core::ast;
6011
6012 use super::*;
6013
6014 fn schema() -> Schema {
6015 let src = r#"
6016 type Task = {
6017 "id": Long,
6018 "name": String,
6019 "state": String,
6020};
6021
6022type T = String;
6023
6024type Tasks = Set<Task>;
6025entity List in [Application] = {
6026 "editors": Team,
6027 "name": String,
6028 "owner": User,
6029 "readers": Team,
6030 "tasks": Tasks,
6031};
6032entity Application;
6033entity User in [Team, Application] = {
6034 "joblevel": Long,
6035 "location": String,
6036};
6037
6038entity CoolList;
6039
6040entity Team in [Team, Application];
6041
6042action Read, Write, Create;
6043
6044action DeleteList, EditShare, UpdateList, CreateTask, UpdateTask, DeleteTask in Write appliesTo {
6045 principal: [User],
6046 resource : [List]
6047};
6048
6049action GetList in Read appliesTo {
6050 principal : [User],
6051 resource : [List, CoolList]
6052};
6053
6054action GetLists in Read appliesTo {
6055 principal : [User],
6056 resource : [Application]
6057};
6058
6059action CreateList in Create appliesTo {
6060 principal : [User],
6061 resource : [Application]
6062};
6063
6064 "#;
6065
6066 src.parse().unwrap()
6067 }
6068
6069 #[test]
6070 fn principals() {
6071 let schema = schema();
6072 let principals = schema.principals().collect::<HashSet<_>>();
6073 assert_eq!(principals.len(), 1);
6074 let user: EntityTypeName = "User".parse().unwrap();
6075 assert!(principals.contains(&user));
6076 let principals = schema.principals().collect::<Vec<_>>();
6077 assert!(principals.len() > 1);
6078 assert!(principals.iter().all(|ety| **ety == user));
6079 assert!(principals.iter().all(|ety| ety.0.loc().is_some()));
6080
6081 let et = ast::EntityType::EntityType(ast::Name::from_normalized_str("User").unwrap());
6082 let et = schema.0.get_entity_type(&et).unwrap();
6083 assert!(et.loc.is_some());
6084 }
6085
6086 #[cfg(feature = "extended-schema")]
6087 #[test]
6088 fn common_types_extended() {
6089 use cool_asserts::assert_matches;
6090
6091 use cedar_policy_core::validator::{types::Type, LocatedCommonType};
6092
6093 let schema = schema();
6094 assert_eq!(schema.0.common_types().collect::<HashSet<_>>().len(), 3);
6095 let task_type = LocatedCommonType {
6096 name: "Task".into(),
6097 name_loc: None,
6098 type_loc: None,
6099 };
6100 assert!(schema.0.common_types().contains(&task_type));
6101
6102 let tasks_type = LocatedCommonType {
6103 name: "Tasks".into(),
6104 name_loc: None,
6105 type_loc: None,
6106 };
6107 assert!(schema.0.common_types().contains(&tasks_type));
6108 assert!(schema.0.common_types().all(|ct| ct.name_loc.is_some()));
6109 assert!(schema.0.common_types().all(|ct| ct.type_loc.is_some()));
6110
6111 let tasks_type = LocatedCommonType {
6112 name: "T".into(),
6113 name_loc: None,
6114 type_loc: None,
6115 };
6116 assert!(schema.0.common_types().contains(&tasks_type));
6117
6118 let et = ast::EntityType::EntityType(ast::Name::from_normalized_str("List").unwrap());
6119 let et = schema.0.get_entity_type(&et).unwrap();
6120 let attrs = et.attributes();
6121
6122 let t = attrs.get_attr("tasks").unwrap();
6124 assert!(t.loc.is_some());
6125 assert_matches!(t.attr_type.as_ref(), cedar_policy_core::validator::types::Type::Set { ref element_type } => {
6126 let el = element_type.as_ref().unwrap();
6127 assert_matches!(el.as_ref(), Type::Record{ attrs, .. } => {
6128 assert!(attrs.get_attr("name").unwrap().loc.is_some());
6129 assert!(attrs.get_attr("id").unwrap().loc.is_some());
6130 assert!(attrs.get_attr("state").unwrap().loc.is_some());
6131 });
6132 });
6133 }
6134
6135 #[cfg(feature = "extended-schema")]
6136 #[test]
6137 fn namespace_extended() {
6138 let schema = schema();
6139 assert_eq!(schema.0.namespaces().collect::<HashSet<_>>().len(), 1);
6140 let default_namespace = schema.0.namespaces().last().unwrap();
6141 assert_eq!(default_namespace.name, SmolStr::from("__cedar"));
6142 assert!(default_namespace.name_loc.is_none());
6143 assert!(default_namespace.def_loc.is_none());
6144 }
6145
6146 #[test]
6147 fn empty_schema_principals_and_resources() {
6148 let empty: Schema = "".parse().unwrap();
6149 assert!(empty.principals().next().is_none());
6150 assert!(empty.resources().next().is_none());
6151 }
6152
6153 #[test]
6154 fn resources() {
6155 let schema = schema();
6156 let resources = schema.resources().cloned().collect::<HashSet<_>>();
6157 let expected: HashSet<EntityTypeName> = HashSet::from([
6158 "List".parse().unwrap(),
6159 "Application".parse().unwrap(),
6160 "CoolList".parse().unwrap(),
6161 ]);
6162 assert_eq!(resources, expected);
6163 assert!(resources.iter().all(|ety| ety.0.loc().is_some()));
6164 }
6165
6166 #[test]
6167 fn principals_for_action() {
6168 let schema = schema();
6169 let delete_list: EntityUid = r#"Action::"DeleteList""#.parse().unwrap();
6170 let delete_user: EntityUid = r#"Action::"DeleteUser""#.parse().unwrap();
6171 let got = schema
6172 .principals_for_action(&delete_list)
6173 .unwrap()
6174 .cloned()
6175 .collect::<Vec<_>>();
6176 assert_eq!(got, vec!["User".parse().unwrap()]);
6177 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
6178 assert!(schema.principals_for_action(&delete_user).is_none());
6179 }
6180
6181 #[test]
6182 fn resources_for_action() {
6183 let schema = schema();
6184 let delete_list: EntityUid = r#"Action::"DeleteList""#.parse().unwrap();
6185 let delete_user: EntityUid = r#"Action::"DeleteUser""#.parse().unwrap();
6186 let create_list: EntityUid = r#"Action::"CreateList""#.parse().unwrap();
6187 let get_list: EntityUid = r#"Action::"GetList""#.parse().unwrap();
6188 let got = schema
6189 .resources_for_action(&delete_list)
6190 .unwrap()
6191 .cloned()
6192 .collect::<Vec<_>>();
6193 assert_eq!(got, vec!["List".parse().unwrap()]);
6194 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
6195 let got = schema
6196 .resources_for_action(&create_list)
6197 .unwrap()
6198 .cloned()
6199 .collect::<Vec<_>>();
6200 assert_eq!(got, vec!["Application".parse().unwrap()]);
6201 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
6202 let got = schema
6203 .resources_for_action(&get_list)
6204 .unwrap()
6205 .cloned()
6206 .collect::<HashSet<_>>();
6207 assert_eq!(
6208 got,
6209 HashSet::from(["List".parse().unwrap(), "CoolList".parse().unwrap()])
6210 );
6211 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
6212 assert!(schema.principals_for_action(&delete_user).is_none());
6213 }
6214
6215 #[test]
6216 fn principal_parents() {
6217 let schema = schema();
6218 let user: EntityTypeName = "User".parse().unwrap();
6219 let parents = schema
6220 .ancestors(&user)
6221 .unwrap()
6222 .cloned()
6223 .collect::<HashSet<_>>();
6224 assert!(parents.iter().all(|ety| ety.0.loc().is_some()));
6225 let expected = HashSet::from(["Team".parse().unwrap(), "Application".parse().unwrap()]);
6226 assert_eq!(parents, expected);
6227 let parents = schema
6228 .ancestors(&"List".parse().unwrap())
6229 .unwrap()
6230 .cloned()
6231 .collect::<HashSet<_>>();
6232 assert!(parents.iter().all(|ety| ety.0.loc().is_some()));
6233 let expected = HashSet::from(["Application".parse().unwrap()]);
6234 assert_eq!(parents, expected);
6235 assert!(schema.ancestors(&"Foo".parse().unwrap()).is_none());
6236 let parents = schema
6237 .ancestors(&"CoolList".parse().unwrap())
6238 .unwrap()
6239 .cloned()
6240 .collect::<HashSet<_>>();
6241 assert!(parents.iter().all(|ety| ety.0.loc().is_some()));
6242 let expected = HashSet::from([]);
6243 assert_eq!(parents, expected);
6244 }
6245
6246 #[test]
6247 fn action_groups() {
6248 let schema = schema();
6249 let groups = schema.action_groups().cloned().collect::<HashSet<_>>();
6250 let expected = ["Read", "Write", "Create"]
6251 .into_iter()
6252 .map(|ty| format!("Action::\"{ty}\"").parse().unwrap())
6253 .collect::<HashSet<EntityUid>>();
6254 #[cfg(feature = "extended-schema")]
6255 assert!(groups.iter().all(|ety| ety.0.loc().is_some()));
6256 assert_eq!(groups, expected);
6257 }
6258
6259 #[test]
6260 fn actions() {
6261 let schema = schema();
6262 let actions = schema.actions().cloned().collect::<HashSet<_>>();
6263 let expected = [
6264 "Read",
6265 "Write",
6266 "Create",
6267 "DeleteList",
6268 "EditShare",
6269 "UpdateList",
6270 "CreateTask",
6271 "UpdateTask",
6272 "DeleteTask",
6273 "GetList",
6274 "GetLists",
6275 "CreateList",
6276 ]
6277 .into_iter()
6278 .map(|ty| format!("Action::\"{ty}\"").parse().unwrap())
6279 .collect::<HashSet<EntityUid>>();
6280 assert_eq!(actions, expected);
6281 #[cfg(feature = "extended-schema")]
6282 assert!(actions.iter().all(|ety| ety.0.loc().is_some()));
6283 }
6284
6285 #[test]
6286 fn actions_for_principal_and_resource() {
6287 let schema = schema();
6288 let pty: EntityTypeName = "User".parse().unwrap();
6289 let rty: EntityTypeName = "Application".parse().unwrap();
6290 let actions = schema
6291 .actions_for_principal_and_resource(&pty, &rty)
6292 .cloned()
6293 .collect::<HashSet<EntityUid>>();
6294 let expected = ["GetLists", "CreateList"]
6295 .into_iter()
6296 .map(|ty| format!("Action::\"{ty}\"").parse().unwrap())
6297 .collect::<HashSet<EntityUid>>();
6298 assert_eq!(actions, expected);
6299 }
6300
6301 #[test]
6302 fn entities() {
6303 let schema = schema();
6304 let entities = schema.entity_types().cloned().collect::<HashSet<_>>();
6305 let expected = ["List", "Application", "User", "CoolList", "Team"]
6306 .into_iter()
6307 .map(|ty| ty.parse().unwrap())
6308 .collect::<HashSet<EntityTypeName>>();
6309 assert_eq!(entities, expected);
6310 }
6311}
6312
6313#[cfg(test)]
6314mod test_access_namespace {
6315 use super::*;
6316
6317 fn schema() -> Schema {
6318 let src = r#"
6319 namespace Foo {
6320 type Task = {
6321 "id": Long,
6322 "name": String,
6323 "state": String,
6324};
6325
6326type Tasks = Set<Task>;
6327entity List in [Application] = {
6328 "editors": Team,
6329 "name": String,
6330 "owner": User,
6331 "readers": Team,
6332 "tasks": Tasks,
6333};
6334entity Application;
6335entity User in [Team, Application] = {
6336 "joblevel": Long,
6337 "location": String,
6338};
6339
6340entity CoolList;
6341
6342entity Team in [Team, Application];
6343
6344action Read, Write, Create;
6345
6346action DeleteList, EditShare, UpdateList, CreateTask, UpdateTask, DeleteTask in Write appliesTo {
6347 principal: [User],
6348 resource : [List]
6349};
6350
6351action GetList in Read appliesTo {
6352 principal : [User],
6353 resource : [List, CoolList]
6354};
6355
6356action GetLists in Read appliesTo {
6357 principal : [User],
6358 resource : [Application]
6359};
6360
6361action CreateList in Create appliesTo {
6362 principal : [User],
6363 resource : [Application]
6364};
6365 }
6366
6367 "#;
6368
6369 src.parse().unwrap()
6370 }
6371
6372 #[test]
6373 fn principals() {
6374 let schema = schema();
6375 let principals = schema.principals().collect::<HashSet<_>>();
6376 assert_eq!(principals.len(), 1);
6377 let user: EntityTypeName = "Foo::User".parse().unwrap();
6378 assert!(principals.contains(&user));
6379 let principals = schema.principals().collect::<Vec<_>>();
6380 assert!(principals.len() > 1);
6381 assert!(principals.iter().all(|ety| **ety == user));
6382 assert!(principals.iter().all(|ety| ety.0.loc().is_some()));
6383 }
6384
6385 #[test]
6386 fn empty_schema_principals_and_resources() {
6387 let empty: Schema = "".parse().unwrap();
6388 assert!(empty.principals().next().is_none());
6389 assert!(empty.resources().next().is_none());
6390 }
6391
6392 #[test]
6393 fn resources() {
6394 let schema = schema();
6395 let resources = schema.resources().cloned().collect::<HashSet<_>>();
6396 let expected: HashSet<EntityTypeName> = HashSet::from([
6397 "Foo::List".parse().unwrap(),
6398 "Foo::Application".parse().unwrap(),
6399 "Foo::CoolList".parse().unwrap(),
6400 ]);
6401 assert_eq!(resources, expected);
6402 assert!(resources.iter().all(|ety| ety.0.loc().is_some()));
6403 }
6404
6405 #[test]
6406 fn principals_for_action() {
6407 let schema = schema();
6408 let delete_list: EntityUid = r#"Foo::Action::"DeleteList""#.parse().unwrap();
6409 let delete_user: EntityUid = r#"Foo::Action::"DeleteUser""#.parse().unwrap();
6410 let got = schema
6411 .principals_for_action(&delete_list)
6412 .unwrap()
6413 .cloned()
6414 .collect::<Vec<_>>();
6415 assert_eq!(got, vec!["Foo::User".parse().unwrap()]);
6416 assert!(schema.principals_for_action(&delete_user).is_none());
6417 }
6418
6419 #[test]
6420 fn resources_for_action() {
6421 let schema = schema();
6422 let delete_list: EntityUid = r#"Foo::Action::"DeleteList""#.parse().unwrap();
6423 let delete_user: EntityUid = r#"Foo::Action::"DeleteUser""#.parse().unwrap();
6424 let create_list: EntityUid = r#"Foo::Action::"CreateList""#.parse().unwrap();
6425 let get_list: EntityUid = r#"Foo::Action::"GetList""#.parse().unwrap();
6426 let got = schema
6427 .resources_for_action(&delete_list)
6428 .unwrap()
6429 .cloned()
6430 .collect::<Vec<_>>();
6431 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
6432
6433 assert_eq!(got, vec!["Foo::List".parse().unwrap()]);
6434 let got = schema
6435 .resources_for_action(&create_list)
6436 .unwrap()
6437 .cloned()
6438 .collect::<Vec<_>>();
6439 assert_eq!(got, vec!["Foo::Application".parse().unwrap()]);
6440 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
6441
6442 let got = schema
6443 .resources_for_action(&get_list)
6444 .unwrap()
6445 .cloned()
6446 .collect::<HashSet<_>>();
6447 assert_eq!(
6448 got,
6449 HashSet::from([
6450 "Foo::List".parse().unwrap(),
6451 "Foo::CoolList".parse().unwrap()
6452 ])
6453 );
6454 assert!(schema.principals_for_action(&delete_user).is_none());
6455 }
6456
6457 #[test]
6458 fn principal_parents() {
6459 let schema = schema();
6460 let user: EntityTypeName = "Foo::User".parse().unwrap();
6461 let parents = schema
6462 .ancestors(&user)
6463 .unwrap()
6464 .cloned()
6465 .collect::<HashSet<_>>();
6466 let expected = HashSet::from([
6467 "Foo::Team".parse().unwrap(),
6468 "Foo::Application".parse().unwrap(),
6469 ]);
6470 assert_eq!(parents, expected);
6471 let parents = schema
6472 .ancestors(&"Foo::List".parse().unwrap())
6473 .unwrap()
6474 .cloned()
6475 .collect::<HashSet<_>>();
6476 let expected = HashSet::from(["Foo::Application".parse().unwrap()]);
6477 assert_eq!(parents, expected);
6478 assert!(schema.ancestors(&"Foo::Foo".parse().unwrap()).is_none());
6479 let parents = schema
6480 .ancestors(&"Foo::CoolList".parse().unwrap())
6481 .unwrap()
6482 .cloned()
6483 .collect::<HashSet<_>>();
6484 let expected = HashSet::from([]);
6485 assert_eq!(parents, expected);
6486 }
6487
6488 #[test]
6489 fn action_groups() {
6490 let schema = schema();
6491 let groups = schema.action_groups().cloned().collect::<HashSet<_>>();
6492 let expected = ["Read", "Write", "Create"]
6493 .into_iter()
6494 .map(|ty| format!("Foo::Action::\"{ty}\"").parse().unwrap())
6495 .collect::<HashSet<EntityUid>>();
6496 assert_eq!(groups, expected);
6497 }
6498
6499 #[test]
6500 fn actions() {
6501 let schema = schema();
6502 let actions = schema.actions().cloned().collect::<HashSet<_>>();
6503 let expected = [
6504 "Read",
6505 "Write",
6506 "Create",
6507 "DeleteList",
6508 "EditShare",
6509 "UpdateList",
6510 "CreateTask",
6511 "UpdateTask",
6512 "DeleteTask",
6513 "GetList",
6514 "GetLists",
6515 "CreateList",
6516 ]
6517 .into_iter()
6518 .map(|ty| format!("Foo::Action::\"{ty}\"").parse().unwrap())
6519 .collect::<HashSet<EntityUid>>();
6520 assert_eq!(actions, expected);
6521 }
6522
6523 #[test]
6524 fn entities() {
6525 let schema = schema();
6526 let entities = schema.entity_types().cloned().collect::<HashSet<_>>();
6527 let expected = [
6528 "Foo::List",
6529 "Foo::Application",
6530 "Foo::User",
6531 "Foo::CoolList",
6532 "Foo::Team",
6533 ]
6534 .into_iter()
6535 .map(|ty| ty.parse().unwrap())
6536 .collect::<HashSet<EntityTypeName>>();
6537 assert_eq!(entities, expected);
6538 }
6539
6540 #[test]
6541 fn test_request_context() {
6542 let context =
6544 Context::from_json_str(r#"{"testKey": "testValue", "numKey": 42}"#, None).unwrap();
6545
6546 let principal: EntityUid = "User::\"alice\"".parse().unwrap();
6548 let action: EntityUid = "Action::\"view\"".parse().unwrap();
6549 let resource: EntityUid = "Resource::\"doc123\"".parse().unwrap();
6550
6551 let request = Request::new(
6553 principal, action, resource, context, None, )
6555 .unwrap();
6556
6557 let retrieved_context = request.context().expect("Context should be present");
6559
6560 assert!(retrieved_context.get("testKey").is_some());
6562 assert!(retrieved_context.get("numKey").is_some());
6563 assert!(retrieved_context.get("nonexistent").is_none());
6564 }
6565
6566 #[cfg(feature = "extended-schema")]
6567 #[test]
6568 fn namespace_extended() {
6569 let schema = schema();
6570 assert_eq!(schema.0.namespaces().collect::<HashSet<_>>().len(), 2);
6571 let default_namespace = schema
6572 .0
6573 .namespaces()
6574 .filter(|n| n.name == *"__cedar")
6575 .last()
6576 .unwrap();
6577 assert!(default_namespace.name_loc.is_none());
6578 assert!(default_namespace.def_loc.is_none());
6579
6580 let default_namespace = schema
6581 .0
6582 .namespaces()
6583 .filter(|n| n.name == *"Foo")
6584 .last()
6585 .unwrap();
6586 assert!(default_namespace.name_loc.is_some());
6587 assert!(default_namespace.def_loc.is_some());
6588 }
6589}
6590
6591#[cfg(test)]
6592mod test_lossless_empty {
6593 use super::{LosslessPolicy, Policy, PolicyId, Template};
6594
6595 #[test]
6596 fn test_lossless_empty_policy() {
6597 const STATIC_POLICY_TEXT: &str = "permit(principal,action,resource);";
6598 let policy0 = Policy::parse(Some(PolicyId::new("policy0")), STATIC_POLICY_TEXT)
6599 .expect("Failed to parse");
6600 let lossy_policy0 = Policy {
6601 ast: policy0.ast.clone(),
6602 lossless: LosslessPolicy::policy_or_template_text(None::<&str>),
6603 };
6604 assert_eq!(
6606 lossy_policy0.to_cedar(),
6607 Some(String::from(
6608 "permit(\n principal,\n action,\n resource\n);"
6609 ))
6610 );
6611 let lossy_policy0_est = lossy_policy0
6613 .lossless
6614 .est(|| policy0.ast.clone().into())
6615 .unwrap();
6616 assert_eq!(lossy_policy0_est, policy0.ast.into());
6617 }
6618
6619 #[test]
6620 fn test_lossless_empty_template() {
6621 const TEMPLATE_TEXT: &str = "permit(principal == ?principal,action,resource);";
6622 let template0 = Template::parse(Some(PolicyId::new("template0")), TEMPLATE_TEXT)
6623 .expect("Failed to parse");
6624 let lossy_template0 = Template {
6625 ast: template0.ast.clone(),
6626 lossless: LosslessPolicy::policy_or_template_text(None::<&str>),
6627 };
6628 assert_eq!(
6630 lossy_template0.to_cedar(),
6631 String::from("permit(\n principal == ?principal,\n action,\n resource\n);")
6632 );
6633 let lossy_template0_est = lossy_template0
6635 .lossless
6636 .est(|| template0.ast.clone().into())
6637 .unwrap();
6638 assert_eq!(lossy_template0_est, template0.ast.into());
6639 }
6640}
6641
6642#[doc = include_str!("../experimental_warning.md")]
6649#[deprecated = "The `entity-manifest` experimental feature and all associated functions are deprecated. Migrate to `PolicySet::is_authorized_batch` for efficient authorization with on-demand entity loading."]
6650#[cfg(feature = "entity-manifest")]
6651pub fn compute_entity_manifest(
6652 validator: &Validator,
6653 pset: &PolicySet,
6654) -> Result<EntityManifest, EntityManifestError> {
6655 entity_manifest::compute_entity_manifest(&validator.0, &pset.ast).map_err(Into::into)
6656}