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, 5, 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 #[expect(clippy::unimplemented, reason = "experimental feature")]
3421 ast::ActionConstraint::ErrorConstraint => {
3422 unimplemented!("internal ErrorConstraint cannot be represented in the public API")
3423 }
3424 }
3425 }
3426
3427 pub fn resource_constraint(&self) -> TemplateResourceConstraint {
3429 match self.ast.resource_constraint().as_inner() {
3430 ast::PrincipalOrResourceConstraint::Any => TemplateResourceConstraint::Any,
3431 ast::PrincipalOrResourceConstraint::In(eref) => {
3432 TemplateResourceConstraint::In(match eref {
3433 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3434 ast::EntityReference::Slot(_) => None,
3435 })
3436 }
3437 ast::PrincipalOrResourceConstraint::Eq(eref) => {
3438 TemplateResourceConstraint::Eq(match eref {
3439 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3440 ast::EntityReference::Slot(_) => None,
3441 })
3442 }
3443 ast::PrincipalOrResourceConstraint::Is(entity_type) => {
3444 TemplateResourceConstraint::Is(entity_type.as_ref().clone().into())
3445 }
3446 ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
3447 TemplateResourceConstraint::IsIn(
3448 entity_type.as_ref().clone().into(),
3449 match eref {
3450 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3451 ast::EntityReference::Slot(_) => None,
3452 },
3453 )
3454 }
3455 }
3456 }
3457
3458 pub fn from_json(
3464 id: Option<PolicyId>,
3465 json: serde_json::Value,
3466 ) -> Result<Self, PolicyFromJsonError> {
3467 let est: est::Policy = serde_json::from_value(json)
3468 .map_err(|e| entities_json_errors::JsonDeserializationError::Serde(e.into()))
3469 .map_err(cedar_policy_core::est::FromJsonError::from)?;
3470 Self::from_est(id, est)
3471 }
3472
3473 fn from_est(id: Option<PolicyId>, est: est::Policy) -> Result<Self, PolicyFromJsonError> {
3474 Ok(Self {
3475 ast: est.clone().try_into_ast_template(id.map(PolicyId::into))?,
3476 lossless: LosslessPolicy::Est(est),
3477 })
3478 }
3479
3480 pub(crate) fn from_ast(ast: ast::Template) -> Self {
3481 Self {
3482 lossless: LosslessPolicy::Est(ast.clone().into()),
3483 ast,
3484 }
3485 }
3486
3487 pub fn to_json(&self) -> Result<serde_json::Value, PolicyToJsonError> {
3489 let est = self.lossless.est(|| self.ast.clone().into())?;
3490 serde_json::to_value(est).map_err(Into::into)
3491 }
3492
3493 pub fn to_cedar(&self) -> String {
3502 match &self.lossless {
3503 LosslessPolicy::Empty | LosslessPolicy::Est(_) => self.ast.to_string(),
3504 LosslessPolicy::Text { text, .. } => text.clone(),
3505 }
3506 }
3507
3508 pub fn get_valid_request_envs(&self, s: &Schema) -> impl Iterator<Item = RequestEnv> {
3513 get_valid_request_envs(&self.ast, s)
3514 }
3515}
3516
3517impl std::fmt::Display for Template {
3518 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3519 self.lossless.fmt(|| self.ast.clone().into(), f)
3521 }
3522}
3523
3524impl FromStr for Template {
3525 type Err = ParseErrors;
3526
3527 fn from_str(src: &str) -> Result<Self, Self::Err> {
3528 Self::parse(None, src)
3529 }
3530}
3531
3532#[derive(Debug, Clone, PartialEq, Eq)]
3534pub enum PrincipalConstraint {
3535 Any,
3537 In(EntityUid),
3539 Eq(EntityUid),
3541 Is(EntityTypeName),
3543 IsIn(EntityTypeName, EntityUid),
3545}
3546
3547#[derive(Debug, Clone, PartialEq, Eq)]
3549pub enum TemplatePrincipalConstraint {
3550 Any,
3552 In(Option<EntityUid>),
3555 Eq(Option<EntityUid>),
3558 Is(EntityTypeName),
3560 IsIn(EntityTypeName, Option<EntityUid>),
3563}
3564
3565impl TemplatePrincipalConstraint {
3566 pub fn has_slot(&self) -> bool {
3568 match self {
3569 Self::Any | Self::Is(_) => false,
3570 Self::In(o) | Self::Eq(o) | Self::IsIn(_, o) => o.is_none(),
3571 }
3572 }
3573}
3574
3575#[derive(Debug, Clone, PartialEq, Eq)]
3577pub enum ActionConstraint {
3578 Any,
3580 In(Vec<EntityUid>),
3582 Eq(EntityUid),
3584}
3585
3586#[derive(Debug, Clone, PartialEq, Eq)]
3588pub enum ResourceConstraint {
3589 Any,
3591 In(EntityUid),
3593 Eq(EntityUid),
3595 Is(EntityTypeName),
3597 IsIn(EntityTypeName, EntityUid),
3599}
3600
3601#[derive(Debug, Clone, PartialEq, Eq)]
3603pub enum TemplateResourceConstraint {
3604 Any,
3606 In(Option<EntityUid>),
3609 Eq(Option<EntityUid>),
3612 Is(EntityTypeName),
3614 IsIn(EntityTypeName, Option<EntityUid>),
3617}
3618
3619impl TemplateResourceConstraint {
3620 pub fn has_slot(&self) -> bool {
3622 match self {
3623 Self::Any | Self::Is(_) => false,
3624 Self::In(o) | Self::Eq(o) | Self::IsIn(_, o) => o.is_none(),
3625 }
3626 }
3627}
3628
3629#[derive(Debug, Clone)]
3631pub struct Policy {
3632 pub(crate) ast: ast::Policy,
3635 pub(crate) lossless: LosslessPolicy,
3643}
3644
3645impl PartialEq for Policy {
3646 fn eq(&self, other: &Self) -> bool {
3647 self.ast.eq(&other.ast)
3649 }
3650}
3651impl Eq for Policy {}
3652
3653#[doc(hidden)] impl AsRef<ast::Policy> for Policy {
3655 fn as_ref(&self) -> &ast::Policy {
3656 &self.ast
3657 }
3658}
3659
3660#[doc(hidden)]
3661impl From<ast::Policy> for Policy {
3662 fn from(policy: ast::Policy) -> Self {
3663 Self::from_ast(policy)
3664 }
3665}
3666
3667#[doc(hidden)]
3668impl From<ast::StaticPolicy> for Policy {
3669 fn from(policy: ast::StaticPolicy) -> Self {
3670 ast::Policy::from(policy).into()
3671 }
3672}
3673
3674impl Policy {
3675 pub fn template_id(&self) -> Option<&PolicyId> {
3678 if self.is_static() {
3679 None
3680 } else {
3681 Some(PolicyId::ref_cast(self.ast.template().id()))
3682 }
3683 }
3684
3685 pub fn template_links(&self) -> Option<HashMap<SlotId, EntityUid>> {
3688 if self.is_static() {
3689 None
3690 } else {
3691 let wrapped_vals: HashMap<SlotId, EntityUid> = self
3692 .ast
3693 .env()
3694 .iter()
3695 .map(|(key, value)| ((*key).into(), value.clone().into()))
3696 .collect();
3697 Some(wrapped_vals)
3698 }
3699 }
3700
3701 pub fn effect(&self) -> Effect {
3703 self.ast.effect()
3704 }
3705
3706 pub fn has_non_scope_constraint(&self) -> bool {
3708 self.ast.non_scope_constraints().is_some()
3709 }
3710
3711 pub fn annotation(&self, key: impl AsRef<str>) -> Option<&str> {
3716 self.ast
3717 .annotation(&key.as_ref().parse().ok()?)
3718 .map(AsRef::as_ref)
3719 }
3720
3721 pub fn annotations(&self) -> impl Iterator<Item = (&str, &str)> {
3725 self.ast
3726 .annotations()
3727 .map(|(k, v)| (k.as_ref(), v.as_ref()))
3728 }
3729
3730 pub fn id(&self) -> &PolicyId {
3732 PolicyId::ref_cast(self.ast.id())
3733 }
3734
3735 #[must_use]
3737 pub fn new_id(&self, id: PolicyId) -> Self {
3738 Self {
3739 ast: self.ast.new_id(id.into()),
3740 lossless: self.lossless.clone(), }
3742 }
3743
3744 pub fn is_static(&self) -> bool {
3746 self.ast.is_static()
3747 }
3748
3749 pub fn principal_constraint(&self) -> PrincipalConstraint {
3751 let slot_id = ast::SlotId::principal();
3752 match self.ast.template().principal_constraint().as_inner() {
3753 ast::PrincipalOrResourceConstraint::Any => PrincipalConstraint::Any,
3754 ast::PrincipalOrResourceConstraint::In(eref) => {
3755 PrincipalConstraint::In(self.convert_entity_reference(eref, slot_id).clone())
3756 }
3757 ast::PrincipalOrResourceConstraint::Eq(eref) => {
3758 PrincipalConstraint::Eq(self.convert_entity_reference(eref, slot_id).clone())
3759 }
3760 ast::PrincipalOrResourceConstraint::Is(entity_type) => {
3761 PrincipalConstraint::Is(entity_type.as_ref().clone().into())
3762 }
3763 ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
3764 PrincipalConstraint::IsIn(
3765 entity_type.as_ref().clone().into(),
3766 self.convert_entity_reference(eref, slot_id).clone(),
3767 )
3768 }
3769 }
3770 }
3771
3772 pub fn action_constraint(&self) -> ActionConstraint {
3774 match self.ast.template().action_constraint() {
3776 ast::ActionConstraint::Any => ActionConstraint::Any,
3777 ast::ActionConstraint::In(ids) => ActionConstraint::In(
3778 ids.iter()
3779 .map(|euid| EntityUid::ref_cast(euid.as_ref()))
3780 .cloned()
3781 .collect(),
3782 ),
3783 ast::ActionConstraint::Eq(id) => ActionConstraint::Eq(EntityUid::ref_cast(id).clone()),
3784 #[cfg(feature = "tolerant-ast")]
3785 #[expect(clippy::unimplemented, reason = "experimental feature")]
3786 ast::ActionConstraint::ErrorConstraint => {
3787 unimplemented!("internal ErrorConstraint cannot be represented in the public API")
3788 }
3789 }
3790 }
3791
3792 pub fn resource_constraint(&self) -> ResourceConstraint {
3794 let slot_id = ast::SlotId::resource();
3795 match self.ast.template().resource_constraint().as_inner() {
3796 ast::PrincipalOrResourceConstraint::Any => ResourceConstraint::Any,
3797 ast::PrincipalOrResourceConstraint::In(eref) => {
3798 ResourceConstraint::In(self.convert_entity_reference(eref, slot_id).clone())
3799 }
3800 ast::PrincipalOrResourceConstraint::Eq(eref) => {
3801 ResourceConstraint::Eq(self.convert_entity_reference(eref, slot_id).clone())
3802 }
3803 ast::PrincipalOrResourceConstraint::Is(entity_type) => {
3804 ResourceConstraint::Is(entity_type.as_ref().clone().into())
3805 }
3806 ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
3807 ResourceConstraint::IsIn(
3808 entity_type.as_ref().clone().into(),
3809 self.convert_entity_reference(eref, slot_id).clone(),
3810 )
3811 }
3812 }
3813 }
3814
3815 fn convert_entity_reference<'a>(
3822 &'a self,
3823 r: &'a ast::EntityReference,
3824 slot: ast::SlotId,
3825 ) -> &'a EntityUid {
3826 match r {
3827 ast::EntityReference::EUID(euid) => EntityUid::ref_cast(euid),
3828 #[expect(
3829 clippy::unwrap_used,
3830 reason = "This `unwrap` here is safe due the invariant (values total map) on policies"
3831 )]
3832 ast::EntityReference::Slot(_) => {
3833 EntityUid::ref_cast(self.ast.env().get(&slot).unwrap())
3834 }
3835 }
3836 }
3837
3838 pub fn parse(id: Option<PolicyId>, policy_src: impl AsRef<str>) -> Result<Self, ParseErrors> {
3847 let inline_ast = parser::parse_policy(id.map(Into::into), policy_src.as_ref())?;
3848 let (_, ast) = ast::Template::link_static_policy(inline_ast);
3849 Ok(Self {
3850 ast,
3851 lossless: LosslessPolicy::policy_or_template_text(Some(policy_src.as_ref())),
3852 })
3853 }
3854
3855 pub fn from_json(
3921 id: Option<PolicyId>,
3922 json: serde_json::Value,
3923 ) -> Result<Self, PolicyFromJsonError> {
3924 let est: est::Policy = serde_json::from_value(json)
3925 .map_err(|e| entities_json_errors::JsonDeserializationError::Serde(e.into()))
3926 .map_err(cedar_policy_core::est::FromJsonError::from)?;
3927 Self::from_est(id, est)
3928 }
3929
3930 pub fn get_valid_request_envs(&self, s: &Schema) -> impl Iterator<Item = RequestEnv> {
3935 get_valid_request_envs(self.ast.template(), s)
3936 }
3937
3938 pub fn entity_literals(&self) -> Vec<EntityUid> {
3940 self.ast
3941 .condition()
3942 .subexpressions()
3943 .filter_map(|e| match e.expr_kind() {
3944 cedar_policy_core::ast::ExprKind::Lit(
3945 cedar_policy_core::ast::Literal::EntityUID(euid),
3946 ) => Some(EntityUid((*euid).as_ref().clone())),
3947 _ => None,
3948 })
3949 .collect()
3950 }
3951
3952 pub fn sub_entity_literals(
3955 &self,
3956 mapping: BTreeMap<EntityUid, EntityUid>,
3957 ) -> Result<Self, PolicyFromJsonError> {
3958 #[expect(
3959 clippy::expect_used,
3960 reason = "This can't fail for a policy that was already constructed"
3961 )]
3962 let cloned_est = self
3963 .lossless
3964 .est(|| self.ast.clone().into())
3965 .expect("Internal error, failed to construct est.");
3966
3967 let mapping = mapping.into_iter().map(|(k, v)| (k.0, v.0)).collect();
3968
3969 #[expect(
3970 clippy::expect_used,
3971 reason = "This can't fail for a policy that was already constructed"
3972 )]
3973 let est = cloned_est
3974 .sub_entity_literals(&mapping)
3975 .expect("Internal error, failed to sub entity literals.");
3976
3977 let ast = est
3978 .clone()
3979 .try_into_ast_policy(Some(self.ast.id().clone()))?;
3980
3981 Ok(Self {
3982 ast,
3983 lossless: LosslessPolicy::Est(est),
3984 })
3985 }
3986
3987 fn from_est(id: Option<PolicyId>, est: est::Policy) -> Result<Self, PolicyFromJsonError> {
3988 Ok(Self {
3989 ast: est.clone().try_into_ast_policy(id.map(PolicyId::into))?,
3990 lossless: LosslessPolicy::Est(est),
3991 })
3992 }
3993
3994 pub fn to_json(&self) -> Result<serde_json::Value, PolicyToJsonError> {
4013 let est = self.lossless.est(|| self.ast.clone().into())?;
4014 serde_json::to_value(est).map_err(Into::into)
4015 }
4016
4017 pub fn to_cedar(&self) -> Option<String> {
4032 match &self.lossless {
4033 LosslessPolicy::Empty | LosslessPolicy::Est(_) => Some(self.ast.to_string()),
4034 LosslessPolicy::Text { text, slots } => {
4035 if slots.is_empty() {
4036 Some(text.clone())
4037 } else {
4038 None
4039 }
4040 }
4041 }
4042 }
4043
4044 #[doc = include_str!("../experimental_warning.md")]
4046 #[cfg(feature = "partial-eval")]
4047 pub fn unknown_entities(&self) -> HashSet<EntityUid> {
4048 self.ast
4049 .unknown_entities()
4050 .into_iter()
4051 .map(Into::into)
4052 .collect()
4053 }
4054
4055 pub(crate) fn from_ast(ast: ast::Policy) -> Self {
4061 let text = ast.to_string(); Self {
4063 ast,
4064 lossless: LosslessPolicy::policy_or_template_text(Some(text)),
4065 }
4066 }
4067}
4068
4069impl std::fmt::Display for Policy {
4070 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4071 self.lossless.fmt(|| self.ast.clone().into(), f)
4073 }
4074}
4075
4076impl FromStr for Policy {
4077 type Err = ParseErrors;
4078 fn from_str(policy: &str) -> Result<Self, Self::Err> {
4086 Self::parse(None, policy)
4087 }
4088}
4089
4090#[derive(Debug, Clone)]
4094pub(crate) enum LosslessPolicy {
4095 Empty,
4097 Est(est::Policy),
4099 Text {
4101 text: String,
4103 slots: HashMap<ast::SlotId, ast::EntityUID>,
4107 },
4108}
4109
4110impl LosslessPolicy {
4111 fn policy_or_template_text(text: Option<impl Into<String>>) -> Self {
4113 text.map_or(Self::Empty, |text| Self::Text {
4114 text: text.into(),
4115 slots: HashMap::new(),
4116 })
4117 }
4118
4119 fn est(
4121 &self,
4122 fallback_est: impl FnOnce() -> est::Policy,
4123 ) -> Result<est::Policy, PolicyToJsonError> {
4124 match self {
4125 Self::Empty => Ok(fallback_est()),
4127 Self::Est(est) => Ok(est.clone()),
4128 Self::Text { text, slots } => {
4129 let est =
4130 parser::parse_policy_or_template_to_est(text).map_err(ParseErrors::from)?;
4131 if slots.is_empty() {
4132 Ok(est)
4133 } else {
4134 let unwrapped_vals = slots.iter().map(|(k, v)| (*k, v.into())).collect();
4135 Ok(est.link(&unwrapped_vals)?)
4136 }
4137 }
4138 }
4139 }
4140
4141 fn link<'a>(
4142 self,
4143 vals: impl IntoIterator<Item = (ast::SlotId, &'a ast::EntityUID)>,
4144 ) -> Result<Self, est::LinkingError> {
4145 match self {
4146 Self::Empty => Ok(Self::Empty),
4147 Self::Est(est) => {
4148 let unwrapped_est_vals: HashMap<
4149 ast::SlotId,
4150 cedar_policy_core::entities::EntityUidJson,
4151 > = vals.into_iter().map(|(k, v)| (k, v.into())).collect();
4152 Ok(Self::Est(est.link(&unwrapped_est_vals)?))
4153 }
4154 Self::Text { text, slots } => {
4155 debug_assert!(
4156 slots.is_empty(),
4157 "shouldn't call link() on an already-linked policy"
4158 );
4159 let slots = vals.into_iter().map(|(k, v)| (k, v.clone())).collect();
4160 Ok(Self::Text { text, slots })
4161 }
4162 }
4163 }
4164
4165 fn fmt(
4166 &self,
4167 fallback_est: impl FnOnce() -> est::Policy,
4168 f: &mut std::fmt::Formatter<'_>,
4169 ) -> std::fmt::Result {
4170 match self {
4171 Self::Empty => match self.est(fallback_est) {
4172 Ok(est) => write!(f, "{est}"),
4173 Err(e) => write!(f, "<invalid policy: {e}>"),
4174 },
4175 Self::Est(est) => write!(f, "{est}"),
4176 Self::Text { text, slots } => {
4177 if slots.is_empty() {
4178 write!(f, "{text}")
4179 } else {
4180 match self.est(fallback_est) {
4186 Ok(est) => write!(f, "{est}"),
4187 Err(e) => write!(f, "<invalid linked policy: {e}>"),
4188 }
4189 }
4190 }
4191 }
4192 }
4193}
4194
4195#[repr(transparent)]
4197#[derive(Debug, Clone, RefCast)]
4198pub struct Expression(pub(crate) ast::Expr);
4199
4200#[doc(hidden)] impl AsRef<ast::Expr> for Expression {
4202 fn as_ref(&self) -> &ast::Expr {
4203 &self.0
4204 }
4205}
4206
4207#[doc(hidden)]
4208impl From<ast::Expr> for Expression {
4209 fn from(expr: ast::Expr) -> Self {
4210 Self(expr)
4211 }
4212}
4213
4214impl Expression {
4215 pub fn new_string(value: String) -> Self {
4217 Self(ast::Expr::val(value))
4218 }
4219
4220 pub fn new_bool(value: bool) -> Self {
4222 Self(ast::Expr::val(value))
4223 }
4224
4225 pub fn new_long(value: ast::Integer) -> Self {
4227 Self(ast::Expr::val(value))
4228 }
4229
4230 pub fn new_record(
4234 fields: impl IntoIterator<Item = (String, Self)>,
4235 ) -> Result<Self, ExpressionConstructionError> {
4236 Ok(Self(ast::Expr::record(
4237 fields.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
4238 )?))
4239 }
4240
4241 pub fn new_set(values: impl IntoIterator<Item = Self>) -> Self {
4243 Self(ast::Expr::set(values.into_iter().map(|v| v.0)))
4244 }
4245
4246 pub fn new_ip(src: impl AsRef<str>) -> Self {
4250 let src_expr = ast::Expr::val(src.as_ref());
4251 Self(ast::Expr::call_extension_fn(
4252 ip_extension_name(),
4253 vec![src_expr],
4254 ))
4255 }
4256
4257 pub fn new_decimal(src: impl AsRef<str>) -> Self {
4261 let src_expr = ast::Expr::val(src.as_ref());
4262 Self(ast::Expr::call_extension_fn(
4263 decimal_extension_name(),
4264 vec![src_expr],
4265 ))
4266 }
4267
4268 pub fn new_datetime(src: impl AsRef<str>) -> Self {
4272 let src_expr = ast::Expr::val(src.as_ref());
4273 Self(ast::Expr::call_extension_fn(
4274 datetime_extension_name(),
4275 vec![src_expr],
4276 ))
4277 }
4278
4279 pub fn new_duration(src: impl AsRef<str>) -> Self {
4283 let src_expr = ast::Expr::val(src.as_ref());
4284 Self(ast::Expr::call_extension_fn(
4285 duration_extension_name(),
4286 vec![src_expr],
4287 ))
4288 }
4289}
4290
4291#[cfg(test)]
4292impl Expression {
4293 pub(crate) fn into_inner(self) -> ast::Expr {
4296 self.0
4297 }
4298}
4299
4300impl FromStr for Expression {
4301 type Err = ParseErrors;
4302
4303 fn from_str(expression: &str) -> Result<Self, Self::Err> {
4305 ast::Expr::from_str(expression)
4306 .map(Expression)
4307 .map_err(Into::into)
4308 }
4309}
4310
4311impl std::fmt::Display for Expression {
4312 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4313 write!(f, "{}", &self.0)
4314 }
4315}
4316
4317#[repr(transparent)]
4333#[derive(Debug, Clone, RefCast, PartialEq, Eq)]
4334pub struct RestrictedExpression(pub(crate) ast::RestrictedExpr);
4335
4336#[doc(hidden)] impl AsRef<ast::RestrictedExpr> for RestrictedExpression {
4338 fn as_ref(&self) -> &ast::RestrictedExpr {
4339 &self.0
4340 }
4341}
4342
4343#[doc(hidden)]
4344impl From<ast::RestrictedExpr> for RestrictedExpression {
4345 fn from(expr: ast::RestrictedExpr) -> Self {
4346 Self(expr)
4347 }
4348}
4349
4350impl RestrictedExpression {
4351 pub fn new_string(value: String) -> Self {
4353 Self(ast::RestrictedExpr::val(value))
4354 }
4355
4356 pub fn new_bool(value: bool) -> Self {
4358 Self(ast::RestrictedExpr::val(value))
4359 }
4360
4361 pub fn new_long(value: ast::Integer) -> Self {
4363 Self(ast::RestrictedExpr::val(value))
4364 }
4365
4366 pub fn new_entity_uid(value: EntityUid) -> Self {
4368 Self(ast::RestrictedExpr::val(ast::EntityUID::from(value)))
4369 }
4370
4371 pub fn new_record(
4375 fields: impl IntoIterator<Item = (String, Self)>,
4376 ) -> Result<Self, ExpressionConstructionError> {
4377 Ok(Self(ast::RestrictedExpr::record(
4378 fields.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
4379 )?))
4380 }
4381
4382 pub fn new_set(values: impl IntoIterator<Item = Self>) -> Self {
4384 Self(ast::RestrictedExpr::set(values.into_iter().map(|v| v.0)))
4385 }
4386
4387 pub fn new_ip(src: impl AsRef<str>) -> Self {
4391 let src_expr = ast::RestrictedExpr::val(src.as_ref());
4392 Self(ast::RestrictedExpr::call_extension_fn(
4393 ip_extension_name(),
4394 [src_expr],
4395 ))
4396 }
4397
4398 pub fn new_decimal(src: impl AsRef<str>) -> Self {
4402 let src_expr = ast::RestrictedExpr::val(src.as_ref());
4403 Self(ast::RestrictedExpr::call_extension_fn(
4404 decimal_extension_name(),
4405 [src_expr],
4406 ))
4407 }
4408
4409 pub fn new_datetime(src: impl AsRef<str>) -> Self {
4413 let src_expr = ast::RestrictedExpr::val(src.as_ref());
4414 Self(ast::RestrictedExpr::call_extension_fn(
4415 datetime_extension_name(),
4416 [src_expr],
4417 ))
4418 }
4419
4420 pub fn new_duration(src: impl AsRef<str>) -> Self {
4424 let src_expr = ast::RestrictedExpr::val(src.as_ref());
4425 Self(ast::RestrictedExpr::call_extension_fn(
4426 duration_extension_name(),
4427 [src_expr],
4428 ))
4429 }
4430
4431 #[cfg(feature = "partial-eval")]
4433 pub fn new_unknown(name: impl AsRef<str>) -> Self {
4434 Self(ast::RestrictedExpr::unknown(ast::Unknown::new_untyped(
4435 name.as_ref(),
4436 )))
4437 }
4438}
4439
4440#[cfg(test)]
4441impl RestrictedExpression {
4442 pub(crate) fn into_inner(self) -> ast::RestrictedExpr {
4445 self.0
4446 }
4447}
4448
4449fn decimal_extension_name() -> ast::Name {
4450 #[expect(
4451 clippy::unwrap_used,
4452 reason = "This is a constant and is known to be safe, verified by a test"
4453 )]
4454 ast::Name::unqualified_name("decimal".parse().unwrap())
4455}
4456
4457fn ip_extension_name() -> ast::Name {
4458 #[expect(
4459 clippy::unwrap_used,
4460 reason = "This is a constant and is known to be safe, verified by a test"
4461 )]
4462 ast::Name::unqualified_name("ip".parse().unwrap())
4463}
4464
4465fn datetime_extension_name() -> ast::Name {
4466 #[expect(
4467 clippy::unwrap_used,
4468 reason = "This is a constant and is known to be safe, verified by a test"
4469 )]
4470 ast::Name::unqualified_name("datetime".parse().unwrap())
4471}
4472
4473fn duration_extension_name() -> ast::Name {
4474 #[expect(
4475 clippy::unwrap_used,
4476 reason = "This is a constant and is known to be safe, verified by a test"
4477 )]
4478 ast::Name::unqualified_name("duration".parse().unwrap())
4479}
4480
4481impl FromStr for RestrictedExpression {
4482 type Err = RestrictedExpressionParseError;
4483
4484 fn from_str(expression: &str) -> Result<Self, Self::Err> {
4486 ast::RestrictedExpr::from_str(expression)
4487 .map(RestrictedExpression)
4488 .map_err(Into::into)
4489 }
4490}
4491
4492#[doc = include_str!("../experimental_warning.md")]
4497#[cfg(feature = "partial-eval")]
4498#[derive(Debug, Clone)]
4499pub struct RequestBuilder<S> {
4500 principal: ast::EntityUIDEntry,
4501 action: ast::EntityUIDEntry,
4502 resource: ast::EntityUIDEntry,
4503 context: Option<ast::Context>,
4505 schema: S,
4506}
4507
4508#[doc = include_str!("../experimental_warning.md")]
4510#[cfg(feature = "partial-eval")]
4511#[derive(Debug, Clone, Copy)]
4512pub struct UnsetSchema;
4513
4514#[cfg(feature = "partial-eval")]
4515impl Default for RequestBuilder<UnsetSchema> {
4516 fn default() -> Self {
4517 Self {
4518 principal: ast::EntityUIDEntry::unknown(),
4519 action: ast::EntityUIDEntry::unknown(),
4520 resource: ast::EntityUIDEntry::unknown(),
4521 context: None,
4522 schema: UnsetSchema,
4523 }
4524 }
4525}
4526
4527#[cfg(feature = "partial-eval")]
4528impl<S> RequestBuilder<S> {
4529 #[must_use]
4534 pub fn principal(self, principal: EntityUid) -> Self {
4535 Self {
4536 principal: ast::EntityUIDEntry::known(principal.into(), None),
4537 ..self
4538 }
4539 }
4540
4541 #[must_use]
4545 pub fn unknown_principal_with_type(self, principal_type: EntityTypeName) -> Self {
4546 Self {
4547 principal: ast::EntityUIDEntry::unknown_with_type(principal_type.0, None),
4548 ..self
4549 }
4550 }
4551
4552 #[must_use]
4557 pub fn action(self, action: EntityUid) -> Self {
4558 Self {
4559 action: ast::EntityUIDEntry::known(action.into(), None),
4560 ..self
4561 }
4562 }
4563
4564 #[must_use]
4569 pub fn resource(self, resource: EntityUid) -> Self {
4570 Self {
4571 resource: ast::EntityUIDEntry::known(resource.into(), None),
4572 ..self
4573 }
4574 }
4575
4576 #[must_use]
4580 pub fn unknown_resource_with_type(self, resource_type: EntityTypeName) -> Self {
4581 Self {
4582 resource: ast::EntityUIDEntry::unknown_with_type(resource_type.0, None),
4583 ..self
4584 }
4585 }
4586
4587 #[must_use]
4589 pub fn context(self, context: Context) -> Self {
4590 Self {
4591 context: Some(context.0),
4592 ..self
4593 }
4594 }
4595}
4596
4597#[cfg(feature = "partial-eval")]
4598impl RequestBuilder<UnsetSchema> {
4599 #[must_use]
4601 pub fn schema(self, schema: &Schema) -> RequestBuilder<&Schema> {
4602 RequestBuilder {
4603 principal: self.principal,
4604 action: self.action,
4605 resource: self.resource,
4606 context: self.context,
4607 schema,
4608 }
4609 }
4610
4611 pub fn build(self) -> Request {
4613 Request(ast::Request::new_unchecked(
4614 self.principal,
4615 self.action,
4616 self.resource,
4617 self.context,
4618 ))
4619 }
4620}
4621
4622#[cfg(feature = "partial-eval")]
4623impl RequestBuilder<&Schema> {
4624 pub fn build(self) -> Result<Request, RequestValidationError> {
4626 Ok(Request(ast::Request::new_with_unknowns(
4627 self.principal,
4628 self.action,
4629 self.resource,
4630 self.context,
4631 Some(&self.schema.0),
4632 Extensions::all_available(),
4633 )?))
4634 }
4635}
4636
4637#[repr(transparent)]
4646#[derive(Debug, Clone, RefCast)]
4647pub struct Request(pub(crate) ast::Request);
4648
4649#[doc(hidden)] impl AsRef<ast::Request> for Request {
4651 fn as_ref(&self) -> &ast::Request {
4652 &self.0
4653 }
4654}
4655
4656#[doc(hidden)]
4657impl From<ast::Request> for Request {
4658 fn from(req: ast::Request) -> Self {
4659 Self(req)
4660 }
4661}
4662
4663impl PartialEq for Request {
4664 fn eq(&self, other: &Self) -> bool {
4665 self.principal() == other.principal()
4666 && self.action() == other.action()
4667 && self.resource() == other.resource()
4668 && self.context() == other.context()
4669 }
4670}
4671
4672impl Request {
4673 #[doc = include_str!("../experimental_warning.md")]
4675 #[cfg(feature = "partial-eval")]
4676 pub fn builder() -> RequestBuilder<UnsetSchema> {
4677 RequestBuilder::default()
4678 }
4679
4680 pub fn new(
4693 principal: EntityUid,
4694 action: EntityUid,
4695 resource: EntityUid,
4696 context: Context,
4697 schema: Option<&Schema>,
4698 ) -> Result<Self, RequestValidationError> {
4699 Ok(Self(ast::Request::new(
4700 (principal.into(), None),
4701 (action.into(), None),
4702 (resource.into(), None),
4703 context.0,
4704 schema.map(|schema| &schema.0),
4705 Extensions::all_available(),
4706 )?))
4707 }
4708
4709 pub fn context(&self) -> Option<&Context> {
4712 self.0.context().map(Context::ref_cast)
4713 }
4714
4715 pub fn principal(&self) -> Option<&EntityUid> {
4718 match self.0.principal() {
4719 ast::EntityUIDEntry::Known { euid, .. } => Some(EntityUid::ref_cast(euid.as_ref())),
4720 ast::EntityUIDEntry::Unknown { .. } => None,
4721 }
4722 }
4723
4724 pub fn action(&self) -> Option<&EntityUid> {
4727 match self.0.action() {
4728 ast::EntityUIDEntry::Known { euid, .. } => Some(EntityUid::ref_cast(euid.as_ref())),
4729 ast::EntityUIDEntry::Unknown { .. } => None,
4730 }
4731 }
4732
4733 pub fn resource(&self) -> Option<&EntityUid> {
4736 match self.0.resource() {
4737 ast::EntityUIDEntry::Known { euid, .. } => Some(EntityUid::ref_cast(euid.as_ref())),
4738 ast::EntityUIDEntry::Unknown { .. } => None,
4739 }
4740 }
4741}
4742
4743#[repr(transparent)]
4745#[derive(Debug, Clone, PartialEq, Eq, RefCast)]
4746pub struct Context(ast::Context);
4747
4748#[doc(hidden)] impl AsRef<ast::Context> for Context {
4750 fn as_ref(&self) -> &ast::Context {
4751 &self.0
4752 }
4753}
4754
4755impl Context {
4756 pub fn empty() -> Self {
4763 Self(ast::Context::empty())
4764 }
4765
4766 pub fn from_pairs(
4783 pairs: impl IntoIterator<Item = (String, RestrictedExpression)>,
4784 ) -> Result<Self, ContextCreationError> {
4785 Ok(Self(ast::Context::from_pairs(
4786 pairs.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
4787 Extensions::all_available(),
4788 )?))
4789 }
4790
4791 pub fn get(&self, key: &str) -> Option<EvalResult> {
4815 match &self.0 {
4816 ast::Context::Value(map) => map.get(key).map(|v| EvalResult::from(v.clone())),
4817 ast::Context::RestrictedResidual(_) => None,
4818 }
4819 }
4820
4821 pub fn from_json_str(
4851 json: &str,
4852 schema: Option<(&Schema, &EntityUid)>,
4853 ) -> Result<Self, ContextJsonError> {
4854 let schema = schema
4855 .map(|(s, uid)| Self::get_context_schema(s, uid))
4856 .transpose()?;
4857 let context = cedar_policy_core::entities::ContextJsonParser::new(
4858 schema.as_ref(),
4859 Extensions::all_available(),
4860 )
4861 .from_json_str(json)?;
4862 Ok(Self(context))
4863 }
4864
4865 pub fn from_json_value(
4915 json: serde_json::Value,
4916 schema: Option<(&Schema, &EntityUid)>,
4917 ) -> Result<Self, ContextJsonError> {
4918 let schema = schema
4919 .map(|(s, uid)| Self::get_context_schema(s, uid))
4920 .transpose()?;
4921 let context = cedar_policy_core::entities::ContextJsonParser::new(
4922 schema.as_ref(),
4923 Extensions::all_available(),
4924 )
4925 .from_json_value(json)?;
4926 Ok(Self(context))
4927 }
4928
4929 pub fn from_json_file(
4961 json: impl std::io::Read,
4962 schema: Option<(&Schema, &EntityUid)>,
4963 ) -> Result<Self, ContextJsonError> {
4964 let schema = schema
4965 .map(|(s, uid)| Self::get_context_schema(s, uid))
4966 .transpose()?;
4967 let context = cedar_policy_core::entities::ContextJsonParser::new(
4968 schema.as_ref(),
4969 Extensions::all_available(),
4970 )
4971 .from_json_file(json)?;
4972 Ok(Self(context))
4973 }
4974
4975 pub fn to_json_value(
4977 &self,
4978 ) -> Result<serde_json::Value, entities_json_errors::JsonSerializationError> {
4979 self.0.to_json_value()
4980 }
4981
4982 fn get_context_schema(
4984 schema: &Schema,
4985 action: &EntityUid,
4986 ) -> Result<impl ContextSchema, ContextJsonError> {
4987 cedar_policy_core::validator::context_schema_for_action(&schema.0, action.as_ref())
4988 .ok_or_else(|| ContextJsonError::missing_action(action.clone()))
4989 }
4990
4991 pub fn merge(
4995 self,
4996 other_context: impl IntoIterator<Item = (String, RestrictedExpression)>,
4997 ) -> Result<Self, ContextCreationError> {
4998 Self::from_pairs(self.into_iter().chain(other_context))
4999 }
5000
5001 pub fn validate(
5008 &self,
5009 schema: &crate::Schema,
5010 action: &EntityUid,
5011 ) -> std::result::Result<(), RequestValidationError> {
5012 Ok(RequestSchema::validate_context(
5014 &schema.0,
5015 &self.0,
5016 action.as_ref(),
5017 Extensions::all_available(),
5018 )?)
5019 }
5020}
5021
5022mod context {
5024 use super::{ast, RestrictedExpression};
5025
5026 #[derive(Debug)]
5028 pub struct IntoIter {
5029 pub(super) inner: <ast::Context as IntoIterator>::IntoIter,
5030 }
5031
5032 impl Iterator for IntoIter {
5033 type Item = (String, RestrictedExpression);
5034
5035 fn next(&mut self) -> Option<Self::Item> {
5036 self.inner
5037 .next()
5038 .map(|(k, v)| (k.to_string(), RestrictedExpression(v)))
5039 }
5040 }
5041}
5042
5043impl IntoIterator for Context {
5044 type Item = (String, RestrictedExpression);
5045
5046 type IntoIter = context::IntoIter;
5047
5048 fn into_iter(self) -> Self::IntoIter {
5049 Self::IntoIter {
5050 inner: self.0.into_iter(),
5051 }
5052 }
5053}
5054
5055#[doc(hidden)]
5056impl From<ast::Context> for Context {
5057 fn from(c: ast::Context) -> Self {
5058 Self(c)
5059 }
5060}
5061
5062impl std::fmt::Display for Request {
5063 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5064 write!(f, "{}", self.0)
5065 }
5066}
5067
5068impl std::fmt::Display for Context {
5069 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5070 write!(f, "{}", self.0)
5071 }
5072}
5073
5074#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
5076pub enum EvalResult {
5077 Bool(bool),
5079 Long(ast::Integer),
5081 String(String),
5083 EntityUid(EntityUid),
5085 Set(Set),
5087 Record(Record),
5089 ExtensionValue(String),
5091 }
5093
5094#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord)]
5096pub struct Set(BTreeSet<EvalResult>);
5097
5098impl Set {
5099 pub fn iter(&self) -> impl Iterator<Item = &EvalResult> {
5101 self.0.iter()
5102 }
5103
5104 pub fn contains(&self, elem: &EvalResult) -> bool {
5106 self.0.contains(elem)
5107 }
5108
5109 pub fn len(&self) -> usize {
5111 self.0.len()
5112 }
5113
5114 pub fn is_empty(&self) -> bool {
5116 self.0.is_empty()
5117 }
5118}
5119
5120#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord)]
5122pub struct Record(BTreeMap<String, EvalResult>);
5123
5124impl Record {
5125 pub fn iter(&self) -> impl Iterator<Item = (&String, &EvalResult)> {
5127 self.0.iter()
5128 }
5129
5130 pub fn contains_attribute(&self, key: impl AsRef<str>) -> bool {
5132 self.0.contains_key(key.as_ref())
5133 }
5134
5135 pub fn get(&self, key: impl AsRef<str>) -> Option<&EvalResult> {
5137 self.0.get(key.as_ref())
5138 }
5139
5140 pub fn len(&self) -> usize {
5142 self.0.len()
5143 }
5144
5145 pub fn is_empty(&self) -> bool {
5147 self.0.is_empty()
5148 }
5149}
5150
5151#[doc(hidden)]
5152impl From<ast::Value> for EvalResult {
5153 fn from(v: ast::Value) -> Self {
5154 match v.value {
5155 ast::ValueKind::Lit(ast::Literal::Bool(b)) => Self::Bool(b),
5156 ast::ValueKind::Lit(ast::Literal::Long(i)) => Self::Long(i),
5157 ast::ValueKind::Lit(ast::Literal::String(s)) => Self::String(s.to_string()),
5158 ast::ValueKind::Lit(ast::Literal::EntityUID(e)) => {
5159 Self::EntityUid(ast::EntityUID::clone(&e).into())
5160 }
5161 ast::ValueKind::Set(set) => Self::Set(Set(set
5162 .authoritative
5163 .iter()
5164 .map(|v| v.clone().into())
5165 .collect())),
5166 ast::ValueKind::Record(record) => Self::Record(Record(
5167 record
5168 .iter()
5169 .map(|(k, v)| (k.to_string(), v.clone().into()))
5170 .collect(),
5171 )),
5172 ast::ValueKind::ExtensionValue(ev) => {
5173 Self::ExtensionValue(RestrictedExpr::from(ev.as_ref().clone()).to_string())
5174 }
5175 }
5176 }
5177}
5178
5179#[doc(hidden)]
5180#[expect(
5181 clippy::fallible_impl_from,
5182 reason = "see the panic safety comments below"
5183)]
5184impl From<EvalResult> for Expression {
5185 fn from(res: EvalResult) -> Self {
5186 match res {
5187 EvalResult::Bool(b) => Self::new_bool(b),
5188 EvalResult::Long(l) => Self::new_long(l),
5189 EvalResult::String(s) => Self::new_string(s),
5190 EvalResult::EntityUid(eid) => {
5191 Self::from(ast::Expr::from(ast::Value::from(ast::EntityUID::from(eid))))
5192 }
5193 EvalResult::Set(set) => Self::new_set(set.iter().cloned().map(Self::from)),
5194 EvalResult::Record(r) =>
5195 {
5196 #[expect(
5197 clippy::unwrap_used,
5198 reason = "record originates from EvalResult so should not panic when reconstructing as an Expression"
5199 )]
5200 Self::new_record(r.iter().map(|(k, v)| (k.clone(), Self::from(v.clone())))).unwrap()
5201 }
5202 EvalResult::ExtensionValue(s) => {
5203 #[expect(
5204 clippy::unwrap_used,
5205 reason = "the string s is constructed using RestrictedExpr::to_string() so should not panic when being parsed back into a RestrictedExpr"
5206 )]
5207 let expr: ast::Expr = ast::RestrictedExpr::from_str(&s).unwrap().into();
5208 Self::from(expr)
5209 }
5210 }
5211 }
5212}
5213
5214impl std::fmt::Display for EvalResult {
5215 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5216 match self {
5217 Self::Bool(b) => write!(f, "{b}"),
5218 Self::Long(l) => write!(f, "{l}"),
5219 Self::String(s) => write!(f, "\"{}\"", s.escape_debug()),
5220 Self::EntityUid(uid) => write!(f, "{uid}"),
5221 Self::Set(s) => {
5222 write!(f, "[")?;
5223 for (i, ev) in s.iter().enumerate() {
5224 write!(f, "{ev}")?;
5225 if (i + 1) < s.len() {
5226 write!(f, ", ")?;
5227 }
5228 }
5229 write!(f, "]")?;
5230 Ok(())
5231 }
5232 Self::Record(r) => {
5233 write!(f, "{{")?;
5234 for (i, (k, v)) in r.iter().enumerate() {
5235 write!(f, "\"{}\": {v}", k.escape_debug())?;
5236 if (i + 1) < r.len() {
5237 write!(f, ", ")?;
5238 }
5239 }
5240 write!(f, "}}")?;
5241 Ok(())
5242 }
5243 Self::ExtensionValue(s) => write!(f, "{s}"),
5244 }
5245 }
5246}
5247
5248pub fn eval_expression(
5253 request: &Request,
5254 entities: &Entities,
5255 expr: &Expression,
5256) -> Result<EvalResult, EvaluationError> {
5257 let all_ext = Extensions::all_available();
5258 let eval = Evaluator::new(request.0.clone(), &entities.0, all_ext);
5259 Ok(EvalResult::from(
5260 eval.interpret(&expr.0, &ast::SlotEnv::new())?,
5262 ))
5263}
5264
5265#[cfg(feature = "tpe")]
5266pub use tpe::*;
5267
5268#[cfg(feature = "tpe")]
5269mod tpe {
5270 use std::collections::{BTreeMap, HashMap, HashSet};
5271 use std::sync::Arc;
5272
5273 use cedar_policy_core::ast::{self, Value};
5274 use cedar_policy_core::authorizer::Decision;
5275 use cedar_policy_core::batched_evaluator::is_authorized_batched;
5276 use cedar_policy_core::batched_evaluator::{
5277 err::BatchedEvalError, EntityLoader as EntityLoaderInternal,
5278 };
5279 use cedar_policy_core::evaluator::{EvaluationError, RestrictedEvaluator};
5280 use cedar_policy_core::extensions::Extensions;
5281 use cedar_policy_core::tpe;
5282 use itertools::Itertools;
5283 use ref_cast::RefCast;
5284 use smol_str::SmolStr;
5285
5286 use crate::{
5287 api, tpe_err, Authorizer, Context, Entities, EntityId, EntityTypeName, EntityUid,
5288 PartialEntityError, PartialRequestCreationError, PermissionQueryError, Policy, PolicySet,
5289 Request, RequestValidationError, RestrictedExpression, Schema,
5290 };
5291 use crate::{Entity, TpeReauthorizationError};
5292
5293 #[doc = include_str!("../experimental_warning.md")]
5296 #[repr(transparent)]
5297 #[derive(Debug, Clone, RefCast)]
5298 pub struct PartialEntityUid(pub(crate) tpe::request::PartialEntityUID);
5299
5300 #[doc(hidden)]
5301 impl AsRef<tpe::request::PartialEntityUID> for PartialEntityUid {
5302 fn as_ref(&self) -> &tpe::request::PartialEntityUID {
5303 &self.0
5304 }
5305 }
5306
5307 impl PartialEntityUid {
5308 pub fn new(ty: EntityTypeName, id: Option<EntityId>) -> Self {
5310 Self(tpe::request::PartialEntityUID {
5311 ty: ty.0,
5312 eid: id.map(|id| <EntityId as AsRef<ast::Eid>>::as_ref(&id).clone()),
5313 })
5314 }
5315
5316 pub fn from_concrete(euid: EntityUid) -> Self {
5318 let (ty, eid) = euid.0.components();
5319 Self(tpe::request::PartialEntityUID { ty, eid: Some(eid) })
5320 }
5321 }
5322
5323 #[doc = include_str!("../experimental_warning.md")]
5327 #[repr(transparent)]
5328 #[derive(Debug, Clone, RefCast)]
5329 pub struct PartialRequest(pub(crate) tpe::request::PartialRequest);
5330
5331 #[doc(hidden)]
5332 impl AsRef<tpe::request::PartialRequest> for PartialRequest {
5333 fn as_ref(&self) -> &tpe::request::PartialRequest {
5334 &self.0
5335 }
5336 }
5337
5338 impl PartialRequest {
5339 pub fn new(
5341 principal: PartialEntityUid,
5342 action: EntityUid,
5343 resource: PartialEntityUid,
5344 context: Option<Context>,
5345 schema: &Schema,
5346 ) -> Result<Self, PartialRequestCreationError> {
5347 let context = context
5348 .map(|c| match c.0 {
5349 ast::Context::RestrictedResidual(_) => {
5350 Err(PartialRequestCreationError::ContextContainsUnknowns)
5351 }
5352 ast::Context::Value(m) => Ok(m),
5353 })
5354 .transpose()?;
5355 tpe::request::PartialRequest::new(principal.0, action.0, resource.0, context, &schema.0)
5356 .map(Self)
5357 .map_err(|e| PartialRequestCreationError::Validation(e.into()))
5358 }
5359 }
5360
5361 #[doc = include_str!("../experimental_warning.md")]
5363 #[repr(transparent)]
5364 #[derive(Debug, Clone, RefCast)]
5365 pub struct ResourceQueryRequest(pub(crate) PartialRequest);
5366
5367 impl ResourceQueryRequest {
5368 pub fn new(
5370 principal: EntityUid,
5371 action: EntityUid,
5372 resource: EntityTypeName,
5373 context: Context,
5374 schema: &Schema,
5375 ) -> Result<Self, PartialRequestCreationError> {
5376 PartialRequest::new(
5377 PartialEntityUid(principal.0.into()),
5378 action,
5379 PartialEntityUid::new(resource, None),
5380 Some(context),
5381 schema,
5382 )
5383 .map(Self)
5384 }
5385
5386 pub fn to_request(
5388 &self,
5389 resource_id: EntityId,
5390 schema: Option<&Schema>,
5391 ) -> Result<Request, RequestValidationError> {
5392 #[expect(
5393 clippy::unwrap_used,
5394 reason = "various fields are validated through the constructor"
5395 )]
5396 Request::new(
5397 EntityUid(self.0 .0.get_principal().try_into().unwrap()),
5398 EntityUid(self.0 .0.get_action()),
5399 EntityUid::from_type_name_and_id(
5400 EntityTypeName(self.0 .0.get_resource_type()),
5401 resource_id,
5402 ),
5403 Context::from_pairs(
5404 self.0
5405 .0
5406 .get_context_attrs()
5407 .unwrap()
5408 .iter()
5409 .map(|(a, v)| (a.to_string(), RestrictedExpression(v.clone().into()))),
5410 )
5411 .unwrap(),
5412 schema,
5413 )
5414 }
5415 }
5416
5417 #[doc = include_str!("../experimental_warning.md")]
5419 #[repr(transparent)]
5420 #[derive(Debug, Clone, RefCast)]
5421 pub struct PrincipalQueryRequest(pub(crate) PartialRequest);
5422
5423 impl PrincipalQueryRequest {
5424 pub fn new(
5426 principal: EntityTypeName,
5427 action: EntityUid,
5428 resource: EntityUid,
5429 context: Context,
5430 schema: &Schema,
5431 ) -> Result<Self, PartialRequestCreationError> {
5432 PartialRequest::new(
5433 PartialEntityUid::new(principal, None),
5434 action,
5435 PartialEntityUid(resource.0.into()),
5436 Some(context),
5437 schema,
5438 )
5439 .map(Self)
5440 }
5441
5442 pub fn to_request(
5444 &self,
5445 principal_id: EntityId,
5446 schema: Option<&Schema>,
5447 ) -> Result<Request, RequestValidationError> {
5448 #[expect(
5449 clippy::unwrap_used,
5450 reason = "various fields are validated through the constructor"
5451 )]
5452 Request::new(
5453 EntityUid::from_type_name_and_id(
5454 EntityTypeName(self.0 .0.get_principal_type()),
5455 principal_id,
5456 ),
5457 EntityUid(self.0 .0.get_action()),
5458 EntityUid(self.0 .0.get_resource().try_into().unwrap()),
5459 Context::from_pairs(
5460 self.0
5461 .0
5462 .get_context_attrs()
5463 .unwrap()
5464 .iter()
5465 .map(|(a, v)| (a.to_string(), RestrictedExpression(v.clone().into()))),
5466 )
5467 .unwrap(),
5468 schema,
5469 )
5470 }
5471 }
5472
5473 #[doc = include_str!("../experimental_warning.md")]
5478 #[derive(Debug, Clone)]
5479 pub struct ActionQueryRequest {
5480 principal: PartialEntityUid,
5481 resource: PartialEntityUid,
5482 context: Option<Arc<BTreeMap<SmolStr, Value>>>,
5483 schema: Schema,
5484 }
5485
5486 impl ActionQueryRequest {
5487 pub fn new(
5489 principal: PartialEntityUid,
5490 resource: PartialEntityUid,
5491 context: Option<Context>,
5492 schema: Schema,
5493 ) -> Result<Self, PartialRequestCreationError> {
5494 let context = context
5495 .map(|c| match c.0 {
5496 ast::Context::RestrictedResidual(_) => {
5497 Err(PartialRequestCreationError::ContextContainsUnknowns)
5498 }
5499 ast::Context::Value(m) => Ok(m),
5500 })
5501 .transpose()?;
5502 Ok(Self {
5503 principal,
5504 resource,
5505 context,
5506 schema,
5507 })
5508 }
5509
5510 fn partial_request(
5511 &self,
5512 action: EntityUid,
5513 ) -> Result<PartialRequest, cedar_policy_core::validator::RequestValidationError> {
5514 tpe::request::PartialRequest::new(
5515 self.principal.0.clone(),
5516 action.0,
5517 self.resource.0.clone(),
5518 self.context.clone(),
5519 &self.schema.0,
5520 )
5521 .map(PartialRequest)
5522 }
5523 }
5524
5525 #[doc = include_str!("../experimental_warning.md")]
5527 #[repr(transparent)]
5528 #[derive(Debug, Clone, RefCast)]
5529 pub struct PartialEntity(pub(crate) tpe::entities::PartialEntity);
5530
5531 impl PartialEntity {
5532 pub fn new(
5534 uid: EntityUid,
5535 attrs: Option<BTreeMap<SmolStr, RestrictedExpression>>,
5536 ancestors: Option<HashSet<EntityUid>>,
5537 tags: Option<BTreeMap<SmolStr, RestrictedExpression>>,
5538 schema: &Schema,
5539 ) -> Result<Self, PartialEntityError> {
5540 Ok(Self(tpe::entities::PartialEntity::new(
5541 uid.0,
5542 attrs
5543 .map(|ps| {
5544 ps.into_iter()
5545 .map(|(k, v)| {
5546 Ok((
5547 k,
5548 RestrictedEvaluator::new(Extensions::all_available())
5549 .interpret(v.0.as_borrowed())?,
5550 ))
5551 })
5552 .collect::<Result<BTreeMap<_, _>, EvaluationError>>()
5553 })
5554 .transpose()?,
5555 ancestors.map(|s| s.into_iter().map(|e| e.0).collect()),
5556 tags.map(|ps| {
5557 ps.into_iter()
5558 .map(|(k, v)| {
5559 Ok((
5560 k,
5561 RestrictedEvaluator::new(Extensions::all_available())
5562 .interpret(v.0.as_borrowed())?,
5563 ))
5564 })
5565 .collect::<Result<BTreeMap<_, _>, EvaluationError>>()
5566 })
5567 .transpose()?,
5568 &schema.0,
5569 )?))
5570 }
5571 }
5572
5573 #[doc = include_str!("../experimental_warning.md")]
5575 #[repr(transparent)]
5576 #[derive(Debug, Clone, RefCast)]
5577 pub struct PartialEntities(pub(crate) tpe::entities::PartialEntities);
5578
5579 #[doc(hidden)]
5580 impl AsRef<tpe::entities::PartialEntities> for PartialEntities {
5581 fn as_ref(&self) -> &tpe::entities::PartialEntities {
5582 &self.0
5583 }
5584 }
5585
5586 impl PartialEntities {
5587 pub fn from_json_value(
5591 value: serde_json::Value,
5592 schema: &Schema,
5593 ) -> Result<Self, tpe_err::EntitiesError> {
5594 tpe::entities::PartialEntities::from_json_value(value, &schema.0).map(Self)
5595 }
5596
5597 pub fn from_concrete(
5599 entities: Entities,
5600 schema: &Schema,
5601 ) -> Result<Self, tpe_err::EntitiesError> {
5602 tpe::entities::PartialEntities::from_concrete(entities.0, &schema.0).map(Self)
5603 }
5604
5605 pub fn empty() -> Self {
5607 Self(tpe::entities::PartialEntities::new())
5608 }
5609
5610 pub fn from_partial_entities(
5612 entities: impl IntoIterator<Item = PartialEntity>,
5613 schema: &Schema,
5614 ) -> Result<Self, tpe_err::EntitiesError> {
5615 Ok(Self(tpe::entities::PartialEntities::from_entities(
5616 entities.into_iter().map(|entity| entity.0),
5617 &schema.0,
5618 )?))
5619 }
5620 }
5621
5622 #[doc = include_str!("../experimental_warning.md")]
5624 #[repr(transparent)]
5625 #[derive(Debug, Clone, RefCast)]
5626 pub struct TpeResponse<'a>(pub(crate) tpe::response::Response<'a>);
5627
5628 #[doc(hidden)]
5629 impl<'a> AsRef<tpe::response::Response<'a>> for TpeResponse<'a> {
5630 fn as_ref(&self) -> &tpe::response::Response<'a> {
5631 &self.0
5632 }
5633 }
5634
5635 impl TpeResponse<'_> {
5636 pub fn decision(&self) -> Option<Decision> {
5638 self.0.decision()
5639 }
5640
5641 pub fn reauthorize(
5643 &self,
5644 request: &Request,
5645 entities: &Entities,
5646 ) -> Result<api::Response, TpeReauthorizationError> {
5647 self.0
5648 .reauthorize(&request.0, &entities.0)
5649 .map(Into::into)
5650 .map_err(Into::into)
5651 }
5652
5653 pub fn residual_policies(&self) -> impl Iterator<Item = Policy> + '_ {
5660 self.0
5661 .residual_policies()
5662 .map(|p| Policy::from_ast(p.clone().into()))
5663 }
5664
5665 pub fn nontrivial_residual_policies(&'_ self) -> impl Iterator<Item = Policy> + '_ {
5672 self.0
5673 .residual_permits()
5674 .chain(self.0.residual_forbids())
5675 .map(|p| Policy::from_ast(p.clone().into()))
5676 }
5677 }
5678
5679 #[doc = include_str!("../experimental_warning.md")]
5686 pub trait EntityLoader {
5687 fn load_entities(
5691 &mut self,
5692 uids: &HashSet<EntityUid>,
5693 ) -> HashMap<EntityUid, Option<Entity>>;
5694 }
5695
5696 struct EntityLoaderWrapper<'a>(&'a mut dyn EntityLoader);
5698
5699 impl EntityLoaderInternal for EntityLoaderWrapper<'_> {
5700 fn load_entities(
5701 &mut self,
5702 uids: &HashSet<ast::EntityUID>,
5703 ) -> HashMap<ast::EntityUID, Option<ast::Entity>> {
5704 let ids = uids
5705 .iter()
5706 .map(|id| EntityUid::ref_cast(id).clone())
5707 .collect();
5708 self.0
5709 .load_entities(&ids)
5710 .into_iter()
5711 .map(|(uid, entity)| (uid.0, entity.map(|e| e.0)))
5712 .collect()
5713 }
5714 }
5715
5716 #[doc = include_str!("../experimental_warning.md")]
5718 #[derive(Debug)]
5719
5720 pub struct TestEntityLoader<'a> {
5721 entities: &'a Entities,
5722 }
5723
5724 impl<'a> TestEntityLoader<'a> {
5725 pub fn new(entities: &'a Entities) -> Self {
5727 Self { entities }
5728 }
5729 }
5730
5731 impl EntityLoader for TestEntityLoader<'_> {
5732 fn load_entities(
5733 &mut self,
5734 uids: &HashSet<EntityUid>,
5735 ) -> HashMap<EntityUid, Option<Entity>> {
5736 uids.iter()
5737 .map(|uid| {
5738 let entity = self.entities.get(uid).cloned();
5739 (uid.clone(), entity)
5740 })
5741 .collect()
5742 }
5743 }
5744
5745 impl PolicySet {
5746 #[doc = include_str!("../experimental_warning.md")]
5750 pub fn tpe<'a>(
5751 &self,
5752 request: &'a PartialRequest,
5753 entities: &'a PartialEntities,
5754 schema: &'a Schema,
5755 ) -> Result<TpeResponse<'a>, tpe_err::TpeError> {
5756 use cedar_policy_core::tpe::is_authorized;
5757 let ps = &self.ast;
5758 let res = is_authorized(ps, &request.0, &entities.0, &schema.0)?;
5759 Ok(TpeResponse(res))
5760 }
5761
5762 #[doc = include_str!("../experimental_warning.md")]
5771 pub fn is_authorized_batched(
5772 &self,
5773 query: &Request,
5774 schema: &Schema,
5775 loader: &mut dyn EntityLoader,
5776 max_iters: u32,
5777 ) -> Result<Decision, BatchedEvalError> {
5778 is_authorized_batched(
5779 &query.0,
5780 &self.ast,
5781 &schema.0,
5782 &mut EntityLoaderWrapper(loader),
5783 max_iters,
5784 )
5785 }
5786
5787 #[doc = include_str!("../experimental_warning.md")]
5789 pub fn query_resource(
5790 &self,
5791 request: &ResourceQueryRequest,
5792 entities: &Entities,
5793 schema: &Schema,
5794 ) -> Result<impl Iterator<Item = EntityUid>, PermissionQueryError> {
5795 let partial_entities = PartialEntities::from_concrete(entities.clone(), schema)?;
5796 let residuals = self.tpe(&request.0, &partial_entities, schema)?;
5797 #[expect(
5798 clippy::unwrap_used,
5799 reason = "policy set construction should succeed because there shouldn't be any policy id conflicts"
5800 )]
5801 let policies = &Self::from_policies(
5802 residuals
5803 .0
5804 .residual_policies()
5805 .map(|p| Policy::from_ast(p.clone().into())),
5806 )
5807 .unwrap();
5808 #[expect(
5809 clippy::unwrap_used,
5810 reason = "request construction should succeed because each entity passes validation"
5811 )]
5812 match residuals.decision() {
5813 Some(Decision::Allow) => Ok(entities
5814 .iter()
5815 .filter(|entity| {
5816 entity.0.uid().entity_type() == &request.0 .0.get_resource_type()
5817 })
5818 .map(super::Entity::uid)
5819 .collect_vec()
5820 .into_iter()),
5821 Some(Decision::Deny) => Ok(vec![].into_iter()),
5822 None => Ok(entities
5823 .iter()
5824 .filter(|entity| {
5825 entity.0.uid().entity_type() == &request.0 .0.get_resource_type()
5826 })
5827 .filter(|entity| {
5828 let authorizer = Authorizer::new();
5829 authorizer
5830 .is_authorized(
5831 &request.to_request(entity.uid().id().clone(), None).unwrap(),
5832 policies,
5833 entities,
5834 )
5835 .decision
5836 == Decision::Allow
5837 })
5838 .map(super::Entity::uid)
5839 .collect_vec()
5840 .into_iter()),
5841 }
5842 }
5843
5844 #[doc = include_str!("../experimental_warning.md")]
5846 pub fn query_principal(
5847 &self,
5848 request: &PrincipalQueryRequest,
5849 entities: &Entities,
5850 schema: &Schema,
5851 ) -> Result<impl Iterator<Item = EntityUid>, PermissionQueryError> {
5852 let partial_entities = PartialEntities::from_concrete(entities.clone(), schema)?;
5853 let residuals = self.tpe(&request.0, &partial_entities, schema)?;
5854 #[expect(
5855 clippy::unwrap_used,
5856 reason = "policy set construction should succeed because there shouldn't be any policy id conflicts"
5857 )]
5858 let policies = &Self::from_policies(
5859 residuals
5860 .0
5861 .residual_policies()
5862 .map(|p| Policy::from_ast(p.clone().into())),
5863 )
5864 .unwrap();
5865 #[expect(
5866 clippy::unwrap_used,
5867 reason = "request construction should succeed because each entity passes validation"
5868 )]
5869 match residuals.decision() {
5870 Some(Decision::Allow) => Ok(entities
5871 .iter()
5872 .filter(|entity| {
5873 entity.0.uid().entity_type() == &request.0 .0.get_principal_type()
5874 })
5875 .map(super::Entity::uid)
5876 .collect_vec()
5877 .into_iter()),
5878 Some(Decision::Deny) => Ok(vec![].into_iter()),
5879 None => Ok(entities
5880 .iter()
5881 .filter(|entity| {
5882 entity.0.uid().entity_type() == &request.0 .0.get_principal_type()
5883 })
5884 .filter(|entity| {
5885 let authorizer = Authorizer::new();
5886 authorizer
5887 .is_authorized(
5888 &request.to_request(entity.uid().id().clone(), None).unwrap(),
5889 policies,
5890 entities,
5891 )
5892 .decision
5893 == Decision::Allow
5894 })
5895 .map(super::Entity::uid)
5896 .collect_vec()
5897 .into_iter()),
5898 }
5899 }
5900
5901 #[doc = include_str!("../experimental_warning.md")]
5967 pub fn query_action<'a>(
5968 &self,
5969 request: &'a ActionQueryRequest,
5970 entities: &PartialEntities,
5971 ) -> Result<impl Iterator<Item = (&'a EntityUid, Option<Decision>)>, PermissionQueryError>
5972 {
5973 let mut authorized_actions = Vec::new();
5974 for action in request
5980 .schema
5981 .0
5982 .actions_for_principal_and_resource(&request.principal.0.ty, &request.resource.0.ty)
5983 {
5984 if let Ok(partial_request) = request.partial_request(action.clone().into()) {
5988 let decision = self
5989 .tpe(&partial_request, entities, &request.schema)?
5990 .decision();
5991 if decision != Some(Decision::Deny) {
5992 authorized_actions.push((RefCast::ref_cast(action), decision));
5993 }
5994 }
5995 }
5996 Ok(authorized_actions.into_iter())
5997 }
5998 }
5999}
6000
6001#[cfg(test)]
6003mod test_access {
6004 use cedar_policy_core::ast;
6005
6006 use super::*;
6007
6008 fn schema() -> Schema {
6009 let src = r#"
6010 type Task = {
6011 "id": Long,
6012 "name": String,
6013 "state": String,
6014};
6015
6016type T = String;
6017
6018type Tasks = Set<Task>;
6019entity List in [Application] = {
6020 "editors": Team,
6021 "name": String,
6022 "owner": User,
6023 "readers": Team,
6024 "tasks": Tasks,
6025};
6026entity Application;
6027entity User in [Team, Application] = {
6028 "joblevel": Long,
6029 "location": String,
6030};
6031
6032entity CoolList;
6033
6034entity Team in [Team, Application];
6035
6036action Read, Write, Create;
6037
6038action DeleteList, EditShare, UpdateList, CreateTask, UpdateTask, DeleteTask in Write appliesTo {
6039 principal: [User],
6040 resource : [List]
6041};
6042
6043action GetList in Read appliesTo {
6044 principal : [User],
6045 resource : [List, CoolList]
6046};
6047
6048action GetLists in Read appliesTo {
6049 principal : [User],
6050 resource : [Application]
6051};
6052
6053action CreateList in Create appliesTo {
6054 principal : [User],
6055 resource : [Application]
6056};
6057
6058 "#;
6059
6060 src.parse().unwrap()
6061 }
6062
6063 #[test]
6064 fn principals() {
6065 let schema = schema();
6066 let principals = schema.principals().collect::<HashSet<_>>();
6067 assert_eq!(principals.len(), 1);
6068 let user: EntityTypeName = "User".parse().unwrap();
6069 assert!(principals.contains(&user));
6070 let principals = schema.principals().collect::<Vec<_>>();
6071 assert!(principals.len() > 1);
6072 assert!(principals.iter().all(|ety| **ety == user));
6073 assert!(principals.iter().all(|ety| ety.0.loc().is_some()));
6074
6075 let et = ast::EntityType::EntityType(ast::Name::from_normalized_str("User").unwrap());
6076 let et = schema.0.get_entity_type(&et).unwrap();
6077 assert!(et.loc.is_some());
6078 }
6079
6080 #[cfg(feature = "extended-schema")]
6081 #[test]
6082 fn common_types_extended() {
6083 use cool_asserts::assert_matches;
6084
6085 use cedar_policy_core::validator::{types::Type, LocatedCommonType};
6086
6087 let schema = schema();
6088 assert_eq!(schema.0.common_types().collect::<HashSet<_>>().len(), 3);
6089 let task_type = LocatedCommonType {
6090 name: "Task".into(),
6091 name_loc: None,
6092 type_loc: None,
6093 };
6094 assert!(schema.0.common_types().contains(&task_type));
6095
6096 let tasks_type = LocatedCommonType {
6097 name: "Tasks".into(),
6098 name_loc: None,
6099 type_loc: None,
6100 };
6101 assert!(schema.0.common_types().contains(&tasks_type));
6102 assert!(schema.0.common_types().all(|ct| ct.name_loc.is_some()));
6103 assert!(schema.0.common_types().all(|ct| ct.type_loc.is_some()));
6104
6105 let tasks_type = LocatedCommonType {
6106 name: "T".into(),
6107 name_loc: None,
6108 type_loc: None,
6109 };
6110 assert!(schema.0.common_types().contains(&tasks_type));
6111
6112 let et = ast::EntityType::EntityType(ast::Name::from_normalized_str("List").unwrap());
6113 let et = schema.0.get_entity_type(&et).unwrap();
6114 let attrs = et.attributes();
6115
6116 let t = attrs.get_attr("tasks").unwrap();
6118 assert!(t.loc.is_some());
6119 assert_matches!(t.attr_type.as_ref(), cedar_policy_core::validator::types::Type::Set { ref element_type } => {
6120 let el = element_type.as_ref().unwrap();
6121 assert_matches!(el.as_ref(), Type::Record{ attrs, .. } => {
6122 assert!(attrs.get_attr("name").unwrap().loc.is_some());
6123 assert!(attrs.get_attr("id").unwrap().loc.is_some());
6124 assert!(attrs.get_attr("state").unwrap().loc.is_some());
6125 });
6126 });
6127 }
6128
6129 #[cfg(feature = "extended-schema")]
6130 #[test]
6131 fn namespace_extended() {
6132 let schema = schema();
6133 assert_eq!(schema.0.namespaces().collect::<HashSet<_>>().len(), 1);
6134 let default_namespace = schema.0.namespaces().last().unwrap();
6135 assert_eq!(default_namespace.name, SmolStr::from("__cedar"));
6136 assert!(default_namespace.name_loc.is_none());
6137 assert!(default_namespace.def_loc.is_none());
6138 }
6139
6140 #[test]
6141 fn empty_schema_principals_and_resources() {
6142 let empty: Schema = "".parse().unwrap();
6143 assert!(empty.principals().next().is_none());
6144 assert!(empty.resources().next().is_none());
6145 }
6146
6147 #[test]
6148 fn resources() {
6149 let schema = schema();
6150 let resources = schema.resources().cloned().collect::<HashSet<_>>();
6151 let expected: HashSet<EntityTypeName> = HashSet::from([
6152 "List".parse().unwrap(),
6153 "Application".parse().unwrap(),
6154 "CoolList".parse().unwrap(),
6155 ]);
6156 assert_eq!(resources, expected);
6157 assert!(resources.iter().all(|ety| ety.0.loc().is_some()));
6158 }
6159
6160 #[test]
6161 fn principals_for_action() {
6162 let schema = schema();
6163 let delete_list: EntityUid = r#"Action::"DeleteList""#.parse().unwrap();
6164 let delete_user: EntityUid = r#"Action::"DeleteUser""#.parse().unwrap();
6165 let got = schema
6166 .principals_for_action(&delete_list)
6167 .unwrap()
6168 .cloned()
6169 .collect::<Vec<_>>();
6170 assert_eq!(got, vec!["User".parse().unwrap()]);
6171 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
6172 assert!(schema.principals_for_action(&delete_user).is_none());
6173 }
6174
6175 #[test]
6176 fn resources_for_action() {
6177 let schema = schema();
6178 let delete_list: EntityUid = r#"Action::"DeleteList""#.parse().unwrap();
6179 let delete_user: EntityUid = r#"Action::"DeleteUser""#.parse().unwrap();
6180 let create_list: EntityUid = r#"Action::"CreateList""#.parse().unwrap();
6181 let get_list: EntityUid = r#"Action::"GetList""#.parse().unwrap();
6182 let got = schema
6183 .resources_for_action(&delete_list)
6184 .unwrap()
6185 .cloned()
6186 .collect::<Vec<_>>();
6187 assert_eq!(got, vec!["List".parse().unwrap()]);
6188 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
6189 let got = schema
6190 .resources_for_action(&create_list)
6191 .unwrap()
6192 .cloned()
6193 .collect::<Vec<_>>();
6194 assert_eq!(got, vec!["Application".parse().unwrap()]);
6195 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
6196 let got = schema
6197 .resources_for_action(&get_list)
6198 .unwrap()
6199 .cloned()
6200 .collect::<HashSet<_>>();
6201 assert_eq!(
6202 got,
6203 HashSet::from(["List".parse().unwrap(), "CoolList".parse().unwrap()])
6204 );
6205 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
6206 assert!(schema.principals_for_action(&delete_user).is_none());
6207 }
6208
6209 #[test]
6210 fn principal_parents() {
6211 let schema = schema();
6212 let user: EntityTypeName = "User".parse().unwrap();
6213 let parents = schema
6214 .ancestors(&user)
6215 .unwrap()
6216 .cloned()
6217 .collect::<HashSet<_>>();
6218 assert!(parents.iter().all(|ety| ety.0.loc().is_some()));
6219 let expected = HashSet::from(["Team".parse().unwrap(), "Application".parse().unwrap()]);
6220 assert_eq!(parents, expected);
6221 let parents = schema
6222 .ancestors(&"List".parse().unwrap())
6223 .unwrap()
6224 .cloned()
6225 .collect::<HashSet<_>>();
6226 assert!(parents.iter().all(|ety| ety.0.loc().is_some()));
6227 let expected = HashSet::from(["Application".parse().unwrap()]);
6228 assert_eq!(parents, expected);
6229 assert!(schema.ancestors(&"Foo".parse().unwrap()).is_none());
6230 let parents = schema
6231 .ancestors(&"CoolList".parse().unwrap())
6232 .unwrap()
6233 .cloned()
6234 .collect::<HashSet<_>>();
6235 assert!(parents.iter().all(|ety| ety.0.loc().is_some()));
6236 let expected = HashSet::from([]);
6237 assert_eq!(parents, expected);
6238 }
6239
6240 #[test]
6241 fn action_groups() {
6242 let schema = schema();
6243 let groups = schema.action_groups().cloned().collect::<HashSet<_>>();
6244 let expected = ["Read", "Write", "Create"]
6245 .into_iter()
6246 .map(|ty| format!("Action::\"{ty}\"").parse().unwrap())
6247 .collect::<HashSet<EntityUid>>();
6248 #[cfg(feature = "extended-schema")]
6249 assert!(groups.iter().all(|ety| ety.0.loc().is_some()));
6250 assert_eq!(groups, expected);
6251 }
6252
6253 #[test]
6254 fn actions() {
6255 let schema = schema();
6256 let actions = schema.actions().cloned().collect::<HashSet<_>>();
6257 let expected = [
6258 "Read",
6259 "Write",
6260 "Create",
6261 "DeleteList",
6262 "EditShare",
6263 "UpdateList",
6264 "CreateTask",
6265 "UpdateTask",
6266 "DeleteTask",
6267 "GetList",
6268 "GetLists",
6269 "CreateList",
6270 ]
6271 .into_iter()
6272 .map(|ty| format!("Action::\"{ty}\"").parse().unwrap())
6273 .collect::<HashSet<EntityUid>>();
6274 assert_eq!(actions, expected);
6275 #[cfg(feature = "extended-schema")]
6276 assert!(actions.iter().all(|ety| ety.0.loc().is_some()));
6277 }
6278
6279 #[test]
6280 fn actions_for_principal_and_resource() {
6281 let schema = schema();
6282 let pty: EntityTypeName = "User".parse().unwrap();
6283 let rty: EntityTypeName = "Application".parse().unwrap();
6284 let actions = schema
6285 .actions_for_principal_and_resource(&pty, &rty)
6286 .cloned()
6287 .collect::<HashSet<EntityUid>>();
6288 let expected = ["GetLists", "CreateList"]
6289 .into_iter()
6290 .map(|ty| format!("Action::\"{ty}\"").parse().unwrap())
6291 .collect::<HashSet<EntityUid>>();
6292 assert_eq!(actions, expected);
6293 }
6294
6295 #[test]
6296 fn entities() {
6297 let schema = schema();
6298 let entities = schema.entity_types().cloned().collect::<HashSet<_>>();
6299 let expected = ["List", "Application", "User", "CoolList", "Team"]
6300 .into_iter()
6301 .map(|ty| ty.parse().unwrap())
6302 .collect::<HashSet<EntityTypeName>>();
6303 assert_eq!(entities, expected);
6304 }
6305}
6306
6307#[cfg(test)]
6308mod test_access_namespace {
6309 use super::*;
6310
6311 fn schema() -> Schema {
6312 let src = r#"
6313 namespace Foo {
6314 type Task = {
6315 "id": Long,
6316 "name": String,
6317 "state": String,
6318};
6319
6320type Tasks = Set<Task>;
6321entity List in [Application] = {
6322 "editors": Team,
6323 "name": String,
6324 "owner": User,
6325 "readers": Team,
6326 "tasks": Tasks,
6327};
6328entity Application;
6329entity User in [Team, Application] = {
6330 "joblevel": Long,
6331 "location": String,
6332};
6333
6334entity CoolList;
6335
6336entity Team in [Team, Application];
6337
6338action Read, Write, Create;
6339
6340action DeleteList, EditShare, UpdateList, CreateTask, UpdateTask, DeleteTask in Write appliesTo {
6341 principal: [User],
6342 resource : [List]
6343};
6344
6345action GetList in Read appliesTo {
6346 principal : [User],
6347 resource : [List, CoolList]
6348};
6349
6350action GetLists in Read appliesTo {
6351 principal : [User],
6352 resource : [Application]
6353};
6354
6355action CreateList in Create appliesTo {
6356 principal : [User],
6357 resource : [Application]
6358};
6359 }
6360
6361 "#;
6362
6363 src.parse().unwrap()
6364 }
6365
6366 #[test]
6367 fn principals() {
6368 let schema = schema();
6369 let principals = schema.principals().collect::<HashSet<_>>();
6370 assert_eq!(principals.len(), 1);
6371 let user: EntityTypeName = "Foo::User".parse().unwrap();
6372 assert!(principals.contains(&user));
6373 let principals = schema.principals().collect::<Vec<_>>();
6374 assert!(principals.len() > 1);
6375 assert!(principals.iter().all(|ety| **ety == user));
6376 assert!(principals.iter().all(|ety| ety.0.loc().is_some()));
6377 }
6378
6379 #[test]
6380 fn empty_schema_principals_and_resources() {
6381 let empty: Schema = "".parse().unwrap();
6382 assert!(empty.principals().next().is_none());
6383 assert!(empty.resources().next().is_none());
6384 }
6385
6386 #[test]
6387 fn resources() {
6388 let schema = schema();
6389 let resources = schema.resources().cloned().collect::<HashSet<_>>();
6390 let expected: HashSet<EntityTypeName> = HashSet::from([
6391 "Foo::List".parse().unwrap(),
6392 "Foo::Application".parse().unwrap(),
6393 "Foo::CoolList".parse().unwrap(),
6394 ]);
6395 assert_eq!(resources, expected);
6396 assert!(resources.iter().all(|ety| ety.0.loc().is_some()));
6397 }
6398
6399 #[test]
6400 fn principals_for_action() {
6401 let schema = schema();
6402 let delete_list: EntityUid = r#"Foo::Action::"DeleteList""#.parse().unwrap();
6403 let delete_user: EntityUid = r#"Foo::Action::"DeleteUser""#.parse().unwrap();
6404 let got = schema
6405 .principals_for_action(&delete_list)
6406 .unwrap()
6407 .cloned()
6408 .collect::<Vec<_>>();
6409 assert_eq!(got, vec!["Foo::User".parse().unwrap()]);
6410 assert!(schema.principals_for_action(&delete_user).is_none());
6411 }
6412
6413 #[test]
6414 fn resources_for_action() {
6415 let schema = schema();
6416 let delete_list: EntityUid = r#"Foo::Action::"DeleteList""#.parse().unwrap();
6417 let delete_user: EntityUid = r#"Foo::Action::"DeleteUser""#.parse().unwrap();
6418 let create_list: EntityUid = r#"Foo::Action::"CreateList""#.parse().unwrap();
6419 let get_list: EntityUid = r#"Foo::Action::"GetList""#.parse().unwrap();
6420 let got = schema
6421 .resources_for_action(&delete_list)
6422 .unwrap()
6423 .cloned()
6424 .collect::<Vec<_>>();
6425 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
6426
6427 assert_eq!(got, vec!["Foo::List".parse().unwrap()]);
6428 let got = schema
6429 .resources_for_action(&create_list)
6430 .unwrap()
6431 .cloned()
6432 .collect::<Vec<_>>();
6433 assert_eq!(got, vec!["Foo::Application".parse().unwrap()]);
6434 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
6435
6436 let got = schema
6437 .resources_for_action(&get_list)
6438 .unwrap()
6439 .cloned()
6440 .collect::<HashSet<_>>();
6441 assert_eq!(
6442 got,
6443 HashSet::from([
6444 "Foo::List".parse().unwrap(),
6445 "Foo::CoolList".parse().unwrap()
6446 ])
6447 );
6448 assert!(schema.principals_for_action(&delete_user).is_none());
6449 }
6450
6451 #[test]
6452 fn principal_parents() {
6453 let schema = schema();
6454 let user: EntityTypeName = "Foo::User".parse().unwrap();
6455 let parents = schema
6456 .ancestors(&user)
6457 .unwrap()
6458 .cloned()
6459 .collect::<HashSet<_>>();
6460 let expected = HashSet::from([
6461 "Foo::Team".parse().unwrap(),
6462 "Foo::Application".parse().unwrap(),
6463 ]);
6464 assert_eq!(parents, expected);
6465 let parents = schema
6466 .ancestors(&"Foo::List".parse().unwrap())
6467 .unwrap()
6468 .cloned()
6469 .collect::<HashSet<_>>();
6470 let expected = HashSet::from(["Foo::Application".parse().unwrap()]);
6471 assert_eq!(parents, expected);
6472 assert!(schema.ancestors(&"Foo::Foo".parse().unwrap()).is_none());
6473 let parents = schema
6474 .ancestors(&"Foo::CoolList".parse().unwrap())
6475 .unwrap()
6476 .cloned()
6477 .collect::<HashSet<_>>();
6478 let expected = HashSet::from([]);
6479 assert_eq!(parents, expected);
6480 }
6481
6482 #[test]
6483 fn action_groups() {
6484 let schema = schema();
6485 let groups = schema.action_groups().cloned().collect::<HashSet<_>>();
6486 let expected = ["Read", "Write", "Create"]
6487 .into_iter()
6488 .map(|ty| format!("Foo::Action::\"{ty}\"").parse().unwrap())
6489 .collect::<HashSet<EntityUid>>();
6490 assert_eq!(groups, expected);
6491 }
6492
6493 #[test]
6494 fn actions() {
6495 let schema = schema();
6496 let actions = schema.actions().cloned().collect::<HashSet<_>>();
6497 let expected = [
6498 "Read",
6499 "Write",
6500 "Create",
6501 "DeleteList",
6502 "EditShare",
6503 "UpdateList",
6504 "CreateTask",
6505 "UpdateTask",
6506 "DeleteTask",
6507 "GetList",
6508 "GetLists",
6509 "CreateList",
6510 ]
6511 .into_iter()
6512 .map(|ty| format!("Foo::Action::\"{ty}\"").parse().unwrap())
6513 .collect::<HashSet<EntityUid>>();
6514 assert_eq!(actions, expected);
6515 }
6516
6517 #[test]
6518 fn entities() {
6519 let schema = schema();
6520 let entities = schema.entity_types().cloned().collect::<HashSet<_>>();
6521 let expected = [
6522 "Foo::List",
6523 "Foo::Application",
6524 "Foo::User",
6525 "Foo::CoolList",
6526 "Foo::Team",
6527 ]
6528 .into_iter()
6529 .map(|ty| ty.parse().unwrap())
6530 .collect::<HashSet<EntityTypeName>>();
6531 assert_eq!(entities, expected);
6532 }
6533
6534 #[test]
6535 fn test_request_context() {
6536 let context =
6538 Context::from_json_str(r#"{"testKey": "testValue", "numKey": 42}"#, None).unwrap();
6539
6540 let principal: EntityUid = "User::\"alice\"".parse().unwrap();
6542 let action: EntityUid = "Action::\"view\"".parse().unwrap();
6543 let resource: EntityUid = "Resource::\"doc123\"".parse().unwrap();
6544
6545 let request = Request::new(
6547 principal, action, resource, context, None, )
6549 .unwrap();
6550
6551 let retrieved_context = request.context().expect("Context should be present");
6553
6554 assert!(retrieved_context.get("testKey").is_some());
6556 assert!(retrieved_context.get("numKey").is_some());
6557 assert!(retrieved_context.get("nonexistent").is_none());
6558 }
6559
6560 #[cfg(feature = "extended-schema")]
6561 #[test]
6562 fn namespace_extended() {
6563 let schema = schema();
6564 assert_eq!(schema.0.namespaces().collect::<HashSet<_>>().len(), 2);
6565 let default_namespace = schema
6566 .0
6567 .namespaces()
6568 .filter(|n| n.name == *"__cedar")
6569 .last()
6570 .unwrap();
6571 assert!(default_namespace.name_loc.is_none());
6572 assert!(default_namespace.def_loc.is_none());
6573
6574 let default_namespace = schema
6575 .0
6576 .namespaces()
6577 .filter(|n| n.name == *"Foo")
6578 .last()
6579 .unwrap();
6580 assert!(default_namespace.name_loc.is_some());
6581 assert!(default_namespace.def_loc.is_some());
6582 }
6583}
6584
6585#[cfg(test)]
6586mod test_lossless_empty {
6587 use super::{LosslessPolicy, Policy, PolicyId, Template};
6588
6589 #[test]
6590 fn test_lossless_empty_policy() {
6591 const STATIC_POLICY_TEXT: &str = "permit(principal,action,resource);";
6592 let policy0 = Policy::parse(Some(PolicyId::new("policy0")), STATIC_POLICY_TEXT)
6593 .expect("Failed to parse");
6594 let lossy_policy0 = Policy {
6595 ast: policy0.ast.clone(),
6596 lossless: LosslessPolicy::policy_or_template_text(None::<&str>),
6597 };
6598 assert_eq!(
6600 lossy_policy0.to_cedar(),
6601 Some(String::from(
6602 "permit(\n principal,\n action,\n resource\n);"
6603 ))
6604 );
6605 let lossy_policy0_est = lossy_policy0
6607 .lossless
6608 .est(|| policy0.ast.clone().into())
6609 .unwrap();
6610 assert_eq!(lossy_policy0_est, policy0.ast.into());
6611 }
6612
6613 #[test]
6614 fn test_lossless_empty_template() {
6615 const TEMPLATE_TEXT: &str = "permit(principal == ?principal,action,resource);";
6616 let template0 = Template::parse(Some(PolicyId::new("template0")), TEMPLATE_TEXT)
6617 .expect("Failed to parse");
6618 let lossy_template0 = Template {
6619 ast: template0.ast.clone(),
6620 lossless: LosslessPolicy::policy_or_template_text(None::<&str>),
6621 };
6622 assert_eq!(
6624 lossy_template0.to_cedar(),
6625 String::from("permit(\n principal == ?principal,\n action,\n resource\n);")
6626 );
6627 let lossy_template0_est = lossy_template0
6629 .lossless
6630 .est(|| template0.ast.clone().into())
6631 .unwrap();
6632 assert_eq!(lossy_template0_est, template0.ast.into());
6633 }
6634}
6635
6636#[doc = include_str!("../experimental_warning.md")]
6643#[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."]
6644#[cfg(feature = "entity-manifest")]
6645pub fn compute_entity_manifest(
6646 validator: &Validator,
6647 pset: &PolicySet,
6648) -> Result<EntityManifest, EntityManifestError> {
6649 entity_manifest::compute_entity_manifest(&validator.0, &pset.ast).map_err(Into::into)
6650}