cedar_policy/api.rs
1/*
2 * Copyright Cedar Contributors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//! This module contains the public library api
18#![allow(
19 clippy::missing_panics_doc,
20 clippy::missing_errors_doc,
21 clippy::similar_names,
22 clippy::result_large_err, // see #878
23)]
24
25mod id;
26#[cfg(feature = "entity-manifest")]
27use cedar_policy_validator::entity_manifest;
28// TODO (#1157) implement wrappers for these structs before they become public
29#[cfg(feature = "entity-manifest")]
30pub use cedar_policy_validator::entity_manifest::{
31 AccessTrie, EntityManifest, EntityRoot, Fields, RootAccessTrie,
32};
33use cedar_policy_validator::json_schema;
34use cedar_policy_validator::typecheck::{PolicyCheck, Typechecker};
35pub use id::*;
36
37mod err;
38pub use err::*;
39
40pub use ast::Effect;
41pub use authorizer::Decision;
42#[cfg(feature = "partial-eval")]
43use cedar_policy_core::ast::BorrowedRestrictedExpr;
44use cedar_policy_core::ast::{self, RestrictedExpr};
45use cedar_policy_core::authorizer;
46use cedar_policy_core::entities::{ContextSchema, Dereference};
47use cedar_policy_core::est::{self, TemplateLink};
48use cedar_policy_core::evaluator::Evaluator;
49#[cfg(feature = "partial-eval")]
50use cedar_policy_core::evaluator::RestrictedEvaluator;
51use cedar_policy_core::extensions::Extensions;
52use cedar_policy_core::parser;
53use cedar_policy_core::FromNormalizedStr;
54use itertools::{Either, Itertools};
55use miette::Diagnostic;
56use ref_cast::RefCast;
57use serde::{Deserialize, Serialize};
58use smol_str::SmolStr;
59use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
60use std::io::Read;
61use std::str::FromStr;
62use std::sync::Arc;
63
64// PANIC SAFETY: `CARGO_PKG_VERSION` should return a valid SemVer version string
65#[allow(clippy::unwrap_used)]
66pub(crate) mod version {
67 use lazy_static::lazy_static;
68 use semver::Version;
69
70 lazy_static! {
71 // Cedar Rust SDK Semantic Versioning version
72 static ref SDK_VERSION: Version = env!("CARGO_PKG_VERSION").parse().unwrap();
73 // Cedar language version
74 // The patch version field may be unnecessary
75 static ref LANG_VERSION: Version = Version::new(4, 3, 0);
76 }
77 /// Get the Cedar SDK Semantic Versioning version
78 #[allow(clippy::module_name_repetitions)]
79 pub fn get_sdk_version() -> Version {
80 SDK_VERSION.clone()
81 }
82 /// Get the Cedar language version
83 #[allow(clippy::module_name_repetitions)]
84 pub fn get_lang_version() -> Version {
85 LANG_VERSION.clone()
86 }
87}
88
89/// Entity datatype
90#[repr(transparent)]
91#[derive(Debug, Clone, PartialEq, Eq, RefCast, Hash)]
92pub struct Entity(pub(crate) ast::Entity);
93
94#[doc(hidden)] // because this converts to a private/internal type
95impl AsRef<ast::Entity> for Entity {
96 fn as_ref(&self) -> &ast::Entity {
97 &self.0
98 }
99}
100
101impl Entity {
102 /// Create a new `Entity` with this Uid, attributes, and parents (and no tags).
103 ///
104 /// Attribute values are specified here as "restricted expressions".
105 /// See docs on `RestrictedExpression`
106 /// ```
107 /// # use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid, RestrictedExpression};
108 /// # use std::collections::{HashMap, HashSet};
109 /// # use std::str::FromStr;
110 /// let eid = EntityId::from_str("alice").unwrap();
111 /// let type_name = EntityTypeName::from_str("User").unwrap();
112 /// let euid = EntityUid::from_type_name_and_id(type_name, eid);
113 /// let attrs = HashMap::from([
114 /// ("age".to_string(), RestrictedExpression::from_str("21").unwrap()),
115 /// ("department".to_string(), RestrictedExpression::from_str("\"CS\"").unwrap()),
116 /// ]);
117 /// let parent_eid = EntityId::from_str("admin").unwrap();
118 /// let parent_type_name = EntityTypeName::from_str("Group").unwrap();
119 /// let parent_euid = EntityUid::from_type_name_and_id(parent_type_name, parent_eid);
120 /// let parents = HashSet::from([parent_euid]);
121 /// let entity = Entity::new(euid, attrs, parents);
122 ///```
123 pub fn new(
124 uid: EntityUid,
125 attrs: HashMap<String, RestrictedExpression>,
126 parents: HashSet<EntityUid>,
127 ) -> Result<Self, EntityAttrEvaluationError> {
128 Self::new_with_tags(uid, attrs, parents, [])
129 }
130
131 /// Create a new `Entity` with no attributes or tags.
132 ///
133 /// Unlike [`Entity::new()`], this constructor cannot error.
134 /// (The only source of errors in `Entity::new()` are attributes.)
135 pub fn new_no_attrs(uid: EntityUid, parents: HashSet<EntityUid>) -> Self {
136 // note that we take a "parents" parameter here; we will compute TC when
137 // the `Entities` object is created
138 Self(ast::Entity::new_with_attr_partial_value(
139 uid.into(),
140 [],
141 HashSet::new(),
142 parents.into_iter().map(EntityUid::into).collect(),
143 [],
144 ))
145 }
146
147 /// Create a new `Entity` with this Uid, attributes, parents, and tags.
148 ///
149 /// Attribute and tag values are specified here as "restricted expressions".
150 /// See docs on [`RestrictedExpression`].
151 pub fn new_with_tags(
152 uid: EntityUid,
153 attrs: impl IntoIterator<Item = (String, RestrictedExpression)>,
154 parents: impl IntoIterator<Item = EntityUid>,
155 tags: impl IntoIterator<Item = (String, RestrictedExpression)>,
156 ) -> Result<Self, EntityAttrEvaluationError> {
157 // note that we take a "parents" parameter here, not "ancestors"; we
158 // will compute TC when the `Entities` object is created
159 Ok(Self(ast::Entity::new(
160 uid.into(),
161 attrs.into_iter().map(|(k, v)| (k.into(), v.0)),
162 HashSet::new(),
163 parents.into_iter().map(EntityUid::into).collect(),
164 tags.into_iter().map(|(k, v)| (k.into(), v.0)),
165 Extensions::all_available(),
166 )?))
167 }
168
169 /// Create a new `Entity` with this Uid, no attributes, and no parents.
170 /// ```
171 /// # use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid};
172 /// # use std::str::FromStr;
173 /// let eid = EntityId::from_str("alice").unwrap();
174 /// let type_name = EntityTypeName::from_str("User").unwrap();
175 /// let euid = EntityUid::from_type_name_and_id(type_name, eid);
176 /// let alice = Entity::with_uid(euid);
177 /// # cool_asserts::assert_matches!(alice.attr("age"), None);
178 /// ```
179 pub fn with_uid(uid: EntityUid) -> Self {
180 Self(ast::Entity::with_uid(uid.into()))
181 }
182
183 /// Get the Uid of this entity
184 /// ```
185 /// # use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid};
186 /// # use std::str::FromStr;
187 /// # let eid = EntityId::from_str("alice").unwrap();
188 /// let type_name = EntityTypeName::from_str("User").unwrap();
189 /// let euid = EntityUid::from_type_name_and_id(type_name, eid);
190 /// let alice = Entity::with_uid(euid.clone());
191 /// assert_eq!(alice.uid(), euid);
192 /// ```
193 pub fn uid(&self) -> EntityUid {
194 self.0.uid().clone().into()
195 }
196
197 /// Get the value for the given attribute, or `None` if not present.
198 ///
199 /// This can also return Some(Err) if the attribute is not a value (i.e., is
200 /// unknown due to partial evaluation).
201 /// ```
202 /// # use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid, EvalResult, RestrictedExpression};
203 /// # use std::collections::{HashMap, HashSet};
204 /// # use std::str::FromStr;
205 /// let eid = EntityId::from_str("alice").unwrap();
206 /// let type_name = EntityTypeName::from_str("User").unwrap();
207 /// let euid = EntityUid::from_type_name_and_id(type_name, eid);
208 /// let attrs = HashMap::from([
209 /// ("age".to_string(), RestrictedExpression::from_str("21").unwrap()),
210 /// ("department".to_string(), RestrictedExpression::from_str("\"CS\"").unwrap()),
211 /// ]);
212 /// let entity = Entity::new(euid, attrs, HashSet::new()).unwrap();
213 /// assert_eq!(entity.attr("age").unwrap().unwrap(), EvalResult::Long(21));
214 /// assert_eq!(entity.attr("department").unwrap().unwrap(), EvalResult::String("CS".to_string()));
215 /// assert!(entity.attr("foo").is_none());
216 /// ```
217 pub fn attr(&self, attr: &str) -> Option<Result<EvalResult, PartialValueToValueError>> {
218 match ast::Value::try_from(self.0.get(attr)?.clone()) {
219 Ok(v) => Some(Ok(EvalResult::from(v))),
220 Err(e) => Some(Err(e)),
221 }
222 }
223
224 /// Get the value for the given tag, or `None` if not present.
225 ///
226 /// This can also return Some(Err) if the tag is not a value (i.e., is
227 /// unknown due to partial evaluation).
228 pub fn tag(&self, tag: &str) -> Option<Result<EvalResult, PartialValueToValueError>> {
229 match ast::Value::try_from(self.0.get_tag(tag)?.clone()) {
230 Ok(v) => Some(Ok(EvalResult::from(v))),
231 Err(e) => Some(Err(e)),
232 }
233 }
234
235 /// Consume the entity and return the entity's owned Uid, attributes and parents.
236 pub fn into_inner(
237 self,
238 ) -> (
239 EntityUid,
240 HashMap<String, RestrictedExpression>,
241 HashSet<EntityUid>,
242 ) {
243 let (uid, attrs, ancestors, mut parents, _) = self.0.into_inner();
244 parents.extend(ancestors);
245
246 let attrs = attrs
247 .into_iter()
248 .map(|(k, v)| {
249 (
250 k.to_string(),
251 match v {
252 ast::PartialValue::Value(val) => {
253 RestrictedExpression(ast::RestrictedExpr::from(val))
254 }
255 ast::PartialValue::Residual(exp) => {
256 RestrictedExpression(ast::RestrictedExpr::new_unchecked(exp))
257 }
258 },
259 )
260 })
261 .collect();
262
263 (
264 uid.into(),
265 attrs,
266 parents.into_iter().map(Into::into).collect(),
267 )
268 }
269
270 /// Parse an entity from an in-memory JSON value
271 /// If a schema is provided, it is handled identically to [`Entities::from_json_str`]
272 pub fn from_json_value(
273 value: serde_json::Value,
274 schema: Option<&Schema>,
275 ) -> Result<Self, EntitiesError> {
276 let schema = schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0));
277 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
278 schema.as_ref(),
279 Extensions::all_available(),
280 cedar_policy_core::entities::TCComputation::ComputeNow,
281 );
282 eparser.single_from_json_value(value).map(Self)
283 }
284
285 /// Parse an entity from a JSON string
286 /// If a schema is provided, it is handled identically to [`Entities::from_json_str`]
287 pub fn from_json_str(
288 src: impl AsRef<str>,
289 schema: Option<&Schema>,
290 ) -> Result<Self, EntitiesError> {
291 let schema = schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0));
292 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
293 schema.as_ref(),
294 Extensions::all_available(),
295 cedar_policy_core::entities::TCComputation::ComputeNow,
296 );
297 eparser.single_from_json_str(src).map(Self)
298 }
299
300 /// Parse an entity from a JSON reader
301 /// If a schema is provided, it is handled identically to [`Entities::from_json_str`]
302 pub fn from_json_file(f: impl Read, schema: Option<&Schema>) -> Result<Self, EntitiesError> {
303 let schema = schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0));
304 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
305 schema.as_ref(),
306 Extensions::all_available(),
307 cedar_policy_core::entities::TCComputation::ComputeNow,
308 );
309 eparser.single_from_json_file(f).map(Self)
310 }
311
312 /// Dump an `Entity` object into an entity JSON file.
313 ///
314 /// The resulting JSON will be suitable for parsing in via
315 /// `from_json_*`, and will be parse-able even with no [`Schema`].
316 ///
317 /// To read an `Entity` object from JSON , use
318 /// [`Self::from_json_file`], [`Self::from_json_value`], or [`Self::from_json_str`].
319 pub fn write_to_json(&self, f: impl std::io::Write) -> Result<(), EntitiesError> {
320 self.0.write_to_json(f)
321 }
322
323 /// Dump an `Entity` object into an in-memory JSON object.
324 ///
325 /// The resulting JSON will be suitable for parsing in via
326 /// `from_json_*`, and will be parse-able even with no `Schema`.
327 ///
328 /// To read an `Entity` object from JSON , use
329 /// [`Self::from_json_file`], [`Self::from_json_value`], or [`Self::from_json_str`].
330 pub fn to_json_value(&self) -> Result<serde_json::Value, EntitiesError> {
331 self.0.to_json_value()
332 }
333
334 /// Dump an `Entity` object into a JSON string.
335 ///
336 /// The resulting JSON will be suitable for parsing in via
337 /// `from_json_*`, and will be parse-able even with no `Schema`.
338 ///
339 /// To read an `Entity` object from JSON , use
340 /// [`Self::from_json_file`], [`Self::from_json_value`], or [`Self::from_json_str`].
341 pub fn to_json_string(&self) -> Result<String, EntitiesError> {
342 self.0.to_json_string()
343 }
344}
345
346impl std::fmt::Display for Entity {
347 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
348 write!(f, "{}", self.0)
349 }
350}
351
352/// Represents an entity hierarchy, and allows looking up `Entity` objects by
353/// Uid.
354#[repr(transparent)]
355#[derive(Debug, Clone, Default, PartialEq, Eq, RefCast)]
356pub struct Entities(pub(crate) cedar_policy_core::entities::Entities);
357
358#[doc(hidden)] // because this converts to a private/internal type
359impl AsRef<cedar_policy_core::entities::Entities> for Entities {
360 fn as_ref(&self) -> &cedar_policy_core::entities::Entities {
361 &self.0
362 }
363}
364
365use entities_errors::EntitiesError;
366
367impl Entities {
368 /// Create a fresh `Entities` with no entities
369 /// ```
370 /// # use cedar_policy::Entities;
371 /// let entities = Entities::empty();
372 /// ```
373 pub fn empty() -> Self {
374 Self(cedar_policy_core::entities::Entities::new())
375 }
376
377 /// Get the `Entity` with the given Uid, if any
378 pub fn get(&self, uid: &EntityUid) -> Option<&Entity> {
379 match self.0.entity(uid.as_ref()) {
380 Dereference::Residual(_) | Dereference::NoSuchEntity => None,
381 Dereference::Data(e) => Some(Entity::ref_cast(e)),
382 }
383 }
384
385 /// Transform the store into a partial store, where
386 /// attempting to dereference a non-existent `EntityUid` results in
387 /// a residual instead of an error.
388 #[doc = include_str!("../experimental_warning.md")]
389 #[must_use]
390 #[cfg(feature = "partial-eval")]
391 pub fn partial(self) -> Self {
392 Self(self.0.partial())
393 }
394
395 /// Iterate over the `Entity`'s in the `Entities`
396 pub fn iter(&self) -> impl Iterator<Item = &Entity> {
397 self.0.iter().map(Entity::ref_cast)
398 }
399
400 /// Create an `Entities` object with the given entities.
401 ///
402 /// `schema` represents a source of `Action` entities, which will be added
403 /// to the entities provided.
404 /// (If any `Action` entities are present in the provided entities, and a
405 /// `schema` is also provided, each `Action` entity in the provided entities
406 /// must exactly match its definition in the schema or an error is
407 /// returned.)
408 ///
409 /// If a `schema` is present, this function will also ensure that the
410 /// produced entities fully conform to the `schema` -- for instance, it will
411 /// error if attributes have the wrong types (e.g., string instead of
412 /// integer), or if required attributes are missing or superfluous
413 /// attributes are provided.
414 /// ## Errors
415 /// - [`EntitiesError::Duplicate`] if there are any duplicate entities in `entities`
416 /// - [`EntitiesError::InvalidEntity`] if `schema` is not none and any entities do not conform
417 /// to the schema
418 pub fn from_entities(
419 entities: impl IntoIterator<Item = Entity>,
420 schema: Option<&Schema>,
421 ) -> Result<Self, EntitiesError> {
422 cedar_policy_core::entities::Entities::from_entities(
423 entities.into_iter().map(|e| e.0),
424 schema
425 .map(|s| cedar_policy_validator::CoreSchema::new(&s.0))
426 .as_ref(),
427 cedar_policy_core::entities::TCComputation::ComputeNow,
428 Extensions::all_available(),
429 )
430 .map(Entities)
431 }
432
433 /// Add all of the [`Entity`]s in the collection to this [`Entities`]
434 /// structure, re-computing the transitive closure.
435 ///
436 /// If a `schema` is provided, this method will ensure that the added
437 /// entities fully conform to the schema -- for instance, it will error if
438 /// attributes have the wrong types (e.g., string instead of integer), or if
439 /// required attributes are missing or superfluous attributes are provided.
440 /// (This method will not add action entities from the `schema`.)
441 ///
442 /// Re-computing the transitive closure can be expensive, so it is advised
443 /// to not call this method in a loop.
444 /// ## Errors
445 /// - [`EntitiesError::Duplicate`] if there is a pair of non-identical entities in `entities` with the same Entity UID,
446 /// or there is an entity in `entities` with the same Entity UID as a non-identical entity in this structure
447 /// - [`EntitiesError::InvalidEntity`] if `schema` is not none and any entities do not conform
448 /// to the schema
449 pub fn add_entities(
450 self,
451 entities: impl IntoIterator<Item = Entity>,
452 schema: Option<&Schema>,
453 ) -> Result<Self, EntitiesError> {
454 Ok(Self(
455 self.0.add_entities(
456 entities.into_iter().map(|e| Arc::new(e.0)),
457 schema
458 .map(|s| cedar_policy_validator::CoreSchema::new(&s.0))
459 .as_ref(),
460 cedar_policy_core::entities::TCComputation::ComputeNow,
461 Extensions::all_available(),
462 )?,
463 ))
464 }
465
466 /// Removes each of the [`EntityUid`]s in the iterator
467 /// from this [`Entities`] structure, re-computing the transitive
468 /// closure after removing all edges to/from the removed entities.
469 ///
470 /// Re-computing the transitive closure can be expensive, so it is
471 /// advised to not call this method in a loop.
472 pub fn remove_entities(
473 self,
474 entity_ids: impl IntoIterator<Item = EntityUid>,
475 ) -> Result<Self, EntitiesError> {
476 Ok(Self(self.0.remove_entities(
477 entity_ids.into_iter().map(|euid| euid.0),
478 cedar_policy_core::entities::TCComputation::ComputeNow,
479 )?))
480 }
481
482 /// Parse an entities JSON file (in [&str] form) and add them into this
483 /// [`Entities`] structure, re-computing the transitive closure
484 ///
485 /// If a `schema` is provided, this will inform the parsing: for instance, it
486 /// will allow `__entity` and `__extn` escapes to be implicit.
487 /// This method will also ensure that the added entities fully conform to the
488 /// schema -- for instance, it will error if attributes have the wrong types
489 /// (e.g., string instead of integer), or if required attributes are missing
490 /// or superfluous attributes are provided.
491 /// (This method will not add action entities from the `schema`.)
492 ///
493 /// Re-computing the transitive closure can be expensive, so it is advised
494 /// to not call this method in a loop.
495 /// ## Errors
496 /// - [`EntitiesError::Duplicate`] if there is a pair of non-identical entities in
497 /// `entities` with the same Entity UID, or there is an entity in `entities` with the
498 /// same Entity UID as a non-identical entity in this structure
499 /// - [`EntitiesError::InvalidEntity`] if `schema` is not none and any entities do not conform
500 /// to the schema
501 /// - [`EntitiesError::Deserialization`] if there are errors while parsing the json
502 pub fn add_entities_from_json_str(
503 self,
504 json: &str,
505 schema: Option<&Schema>,
506 ) -> Result<Self, EntitiesError> {
507 let schema = schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0));
508 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
509 schema.as_ref(),
510 Extensions::all_available(),
511 cedar_policy_core::entities::TCComputation::ComputeNow,
512 );
513 let new_entities = eparser.iter_from_json_str(json)?.map(Arc::new);
514 Ok(Self(self.0.add_entities(
515 new_entities,
516 schema.as_ref(),
517 cedar_policy_core::entities::TCComputation::ComputeNow,
518 Extensions::all_available(),
519 )?))
520 }
521
522 /// Parse an entities JSON file (in [`serde_json::Value`] form) and add them
523 /// into this [`Entities`] structure, re-computing the transitive closure
524 ///
525 /// If a `schema` is provided, this will inform the parsing: for instance, it
526 /// will allow `__entity` and `__extn` escapes to be implicit.
527 /// This method will also ensure that the added entities fully conform to the
528 /// schema -- for instance, it will error if attributes have the wrong types
529 /// (e.g., string instead of integer), or if required attributes are missing
530 /// or superfluous attributes are provided.
531 /// (This method will not add action entities from the `schema`.)
532 ///
533 /// Re-computing the transitive closure can be expensive, so it is advised
534 /// to not call this method in a loop.
535 /// ## Errors
536 /// - [`EntitiesError::Duplicate`] if there is a pair of non-identical entities in
537 /// `entities` with the same Entity UID, or there is an entity in `entities` with the same
538 /// Entity UID as a non-identical entity in this structure
539 /// - [`EntitiesError::InvalidEntity`] if `schema` is not none and any entities do not conform
540 /// to the schema
541 /// - [`EntitiesError::Deserialization`] if there are errors while parsing the json
542 pub fn add_entities_from_json_value(
543 self,
544 json: serde_json::Value,
545 schema: Option<&Schema>,
546 ) -> Result<Self, EntitiesError> {
547 let schema = schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0));
548 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
549 schema.as_ref(),
550 Extensions::all_available(),
551 cedar_policy_core::entities::TCComputation::ComputeNow,
552 );
553 let new_entities = eparser.iter_from_json_value(json)?.map(Arc::new);
554 Ok(Self(self.0.add_entities(
555 new_entities,
556 schema.as_ref(),
557 cedar_policy_core::entities::TCComputation::ComputeNow,
558 Extensions::all_available(),
559 )?))
560 }
561
562 /// Parse an entities JSON file (in [`std::io::Read`] form) and add them
563 /// into this [`Entities`] structure, re-computing the transitive closure
564 ///
565 /// If a `schema` is provided, this will inform the parsing: for instance, it
566 /// will allow `__entity` and `__extn` escapes to be implicit.
567 /// This method will also ensure that the added entities fully conform to the
568 /// schema -- for instance, it will error if attributes have the wrong types
569 /// (e.g., string instead of integer), or if required attributes are missing
570 /// or superfluous attributes are provided.
571 /// (This method will not add action entities from the `schema`.)
572 ///
573 /// Re-computing the transitive closure can be expensive, so it is advised
574 /// to not call this method in a loop.
575 ///
576 /// ## Errors
577 /// - [`EntitiesError::Duplicate`] if there is a pair of non-identical entities in `entities`
578 /// with the same Entity UID, or there is an entity in `entities` with the same Entity UID as a
579 /// non-identical entity in this structure
580 /// - [`EntitiesError::InvalidEntity`] if `schema` is not none and any entities do not conform
581 /// to the schema
582 /// - [`EntitiesError::Deserialization`] if there are errors while parsing the json
583 pub fn add_entities_from_json_file(
584 self,
585 json: impl std::io::Read,
586 schema: Option<&Schema>,
587 ) -> Result<Self, EntitiesError> {
588 let schema = schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0));
589 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
590 schema.as_ref(),
591 Extensions::all_available(),
592 cedar_policy_core::entities::TCComputation::ComputeNow,
593 );
594 let new_entities = eparser.iter_from_json_file(json)?.map(Arc::new);
595 Ok(Self(self.0.add_entities(
596 new_entities,
597 schema.as_ref(),
598 cedar_policy_core::entities::TCComputation::ComputeNow,
599 Extensions::all_available(),
600 )?))
601 }
602
603 /// Parse an entities JSON file (in `&str` form) into an `Entities` object
604 ///
605 /// `schema` represents a source of `Action` entities, which will be added
606 /// to the entities parsed from JSON.
607 /// (If any `Action` entities are present in the JSON, and a `schema` is
608 /// also provided, each `Action` entity in the JSON must exactly match its
609 /// definition in the schema or an error is returned.)
610 ///
611 /// If a `schema` is present, this will also inform the parsing: for
612 /// instance, it will allow `__entity` and `__extn` escapes to be implicit.
613 ///
614 /// Finally, if a `schema` is present, this function will ensure
615 /// that the produced entities fully conform to the `schema` -- for
616 /// instance, it will error if attributes have the wrong types (e.g., string
617 /// instead of integer), or if required attributes are missing or
618 /// superfluous attributes are provided.
619 ///
620 /// ## Errors
621 /// - [`EntitiesError::Duplicate`] if there are any duplicate entities in `entities`
622 /// - [`EntitiesError::InvalidEntity`] if `schema` is not none and any entities do not conform
623 /// to the schema
624 /// - [`EntitiesError::Deserialization`] if there are errors while parsing the json
625 ///
626 /// ```
627 /// # use cedar_policy::{Entities, EntityId, EntityTypeName, EntityUid, EvalResult, Request,PolicySet};
628 /// # use std::str::FromStr;
629 /// let data =r#"
630 /// [
631 /// {
632 /// "uid": {"type":"User","id":"alice"},
633 /// "attrs": {
634 /// "age":19,
635 /// "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
636 /// },
637 /// "parents": [{"type":"Group","id":"admin"}]
638 /// },
639 /// {
640 /// "uid": {"type":"Group","id":"admin"},
641 /// "attrs": {},
642 /// "parents": []
643 /// }
644 /// ]
645 /// "#;
646 /// let entities = Entities::from_json_str(data, None).unwrap();
647 /// # let euid = EntityUid::from_str(r#"User::"alice""#).unwrap();
648 /// # let entity = entities.get(&euid).unwrap();
649 /// # assert_eq!(entity.attr("age").unwrap().unwrap(), EvalResult::Long(19));
650 /// # let ip = entity.attr("ip_addr").unwrap().unwrap();
651 /// # assert_eq!(ip, EvalResult::ExtensionValue("ip(\"10.0.1.101\")".to_string()));
652 /// ```
653 pub fn from_json_str(json: &str, schema: Option<&Schema>) -> Result<Self, EntitiesError> {
654 let schema = schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0));
655 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
656 schema.as_ref(),
657 Extensions::all_available(),
658 cedar_policy_core::entities::TCComputation::ComputeNow,
659 );
660 eparser.from_json_str(json).map(Entities)
661 }
662
663 /// Parse an entities JSON file (in `serde_json::Value` form) into an
664 /// `Entities` object
665 ///
666 /// `schema` represents a source of `Action` entities, which will be added
667 /// to the entities parsed from JSON.
668 /// (If any `Action` entities are present in the JSON, and a `schema` is
669 /// also provided, each `Action` entity in the JSON must exactly match its
670 /// definition in the schema or an error is returned.)
671 ///
672 /// If a `schema` is present, this will also inform the parsing: for
673 /// instance, it will allow `__entity` and `__extn` escapes to be implicit.
674 ///
675 /// Finally, if a `schema` is present, this function will ensure
676 /// that the produced entities fully conform to the `schema` -- for
677 /// instance, it will error if attributes have the wrong types (e.g., string
678 /// instead of integer), or if required attributes are missing or
679 /// superfluous attributes are provided.
680 ///
681 /// ## Errors
682 /// - [`EntitiesError::Duplicate`] if there are any duplicate entities in `entities`
683 /// - [`EntitiesError::InvalidEntity`]if `schema` is not none and any entities do not conform
684 /// to the schema
685 /// - [`EntitiesError::Deserialization`] if there are errors while parsing the json
686 ///
687 /// ```
688 /// # use cedar_policy::{Entities, EntityId, EntityTypeName, EntityUid, EvalResult, Request,PolicySet};
689 /// let data =serde_json::json!(
690 /// [
691 /// {
692 /// "uid": {"type":"User","id":"alice"},
693 /// "attrs": {
694 /// "age":19,
695 /// "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
696 /// },
697 /// "parents": [{"type":"Group","id":"admin"}]
698 /// },
699 /// {
700 /// "uid": {"type":"Group","id":"admin"},
701 /// "attrs": {},
702 /// "parents": []
703 /// }
704 /// ]
705 /// );
706 /// let entities = Entities::from_json_value(data, None).unwrap();
707 /// ```
708 pub fn from_json_value(
709 json: serde_json::Value,
710 schema: Option<&Schema>,
711 ) -> Result<Self, EntitiesError> {
712 let schema = schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0));
713 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
714 schema.as_ref(),
715 Extensions::all_available(),
716 cedar_policy_core::entities::TCComputation::ComputeNow,
717 );
718 eparser.from_json_value(json).map(Entities)
719 }
720
721 /// Parse an entities JSON file (in `std::io::Read` form) into an `Entities`
722 /// object
723 ///
724 /// `schema` represents a source of `Action` entities, which will be added
725 /// to the entities parsed from JSON.
726 /// (If any `Action` entities are present in the JSON, and a `schema` is
727 /// also provided, each `Action` entity in the JSON must exactly match its
728 /// definition in the schema or an error is returned.)
729 ///
730 /// If a `schema` is present, this will also inform the parsing: for
731 /// instance, it will allow `__entity` and `__extn` escapes to be implicit.
732 ///
733 /// Finally, if a `schema` is present, this function will ensure
734 /// that the produced entities fully conform to the `schema` -- for
735 /// instance, it will error if attributes have the wrong types (e.g., string
736 /// instead of integer), or if required attributes are missing or
737 /// superfluous attributes are provided.
738 ///
739 /// ## Errors
740 /// - [`EntitiesError::Duplicate`] if there are any duplicate entities in `entities`
741 /// - [`EntitiesError::InvalidEntity`] if `schema` is not none and any entities do not conform
742 /// to the schema
743 /// - [`EntitiesError::Deserialization`] if there are errors while parsing the json
744 pub fn from_json_file(
745 json: impl std::io::Read,
746 schema: Option<&Schema>,
747 ) -> Result<Self, EntitiesError> {
748 let schema = schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0));
749 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
750 schema.as_ref(),
751 Extensions::all_available(),
752 cedar_policy_core::entities::TCComputation::ComputeNow,
753 );
754 eparser.from_json_file(json).map(Entities)
755 }
756
757 /// Is entity `a` an ancestor of entity `b`?
758 /// Same semantics as `b in a` in the Cedar language
759 pub fn is_ancestor_of(&self, a: &EntityUid, b: &EntityUid) -> bool {
760 match self.0.entity(b.as_ref()) {
761 Dereference::Data(b) => b.is_descendant_of(a.as_ref()),
762 _ => a == b, // if b doesn't exist, `b in a` is only true if `b == a`
763 }
764 }
765
766 /// Get an iterator over the ancestors of the given Euid.
767 /// Returns `None` if the given `Euid` does not exist.
768 pub fn ancestors<'a>(
769 &'a self,
770 euid: &EntityUid,
771 ) -> Option<impl Iterator<Item = &'a EntityUid>> {
772 let entity = match self.0.entity(euid.as_ref()) {
773 Dereference::Residual(_) | Dereference::NoSuchEntity => None,
774 Dereference::Data(e) => Some(e),
775 }?;
776 Some(entity.ancestors().map(EntityUid::ref_cast))
777 }
778
779 /// Returns the number of `Entity`s in the `Entities`
780 pub fn len(&self) -> usize {
781 self.0.len()
782 }
783
784 /// Returns true if the `Entities` contains no `Entity`s
785 pub fn is_empty(&self) -> bool {
786 self.0.is_empty()
787 }
788
789 /// Dump an `Entities` object into an entities JSON file.
790 ///
791 /// The resulting JSON will be suitable for parsing in via
792 /// `from_json_*`, and will be parse-able even with no `Schema`.
793 ///
794 /// To read an `Entities` object from an entities JSON file, use
795 /// `from_json_file`.
796 pub fn write_to_json(&self, f: impl std::io::Write) -> std::result::Result<(), EntitiesError> {
797 self.0.write_to_json(f)
798 }
799
800 #[doc = include_str!("../experimental_warning.md")]
801 /// Visualize an `Entities` object in the graphviz `dot`
802 /// format. Entity visualization is best-effort and not well tested.
803 /// Feel free to submit an issue if you are using this feature and would like it improved.
804 pub fn to_dot_str(&self) -> String {
805 let mut dot_str = String::new();
806 // PANIC SAFETY: Writing to the String `dot_str` cannot fail, so `to_dot_str` will not return an `Err` result.
807 #[allow(clippy::unwrap_used)]
808 self.0.to_dot_str(&mut dot_str).unwrap();
809 dot_str
810 }
811}
812
813/// Utilities for defining `IntoIterator` over `Entities`
814pub mod entities {
815
816 /// `IntoIter` iterator for `Entities`
817 #[derive(Debug)]
818 pub struct IntoIter {
819 pub(super) inner: <cedar_policy_core::entities::Entities as IntoIterator>::IntoIter,
820 }
821
822 impl Iterator for IntoIter {
823 type Item = super::Entity;
824
825 fn next(&mut self) -> Option<Self::Item> {
826 self.inner.next().map(super::Entity)
827 }
828 fn size_hint(&self) -> (usize, Option<usize>) {
829 self.inner.size_hint()
830 }
831 }
832}
833
834impl IntoIterator for Entities {
835 type Item = Entity;
836 type IntoIter = entities::IntoIter;
837
838 fn into_iter(self) -> Self::IntoIter {
839 Self::IntoIter {
840 inner: self.0.into_iter(),
841 }
842 }
843}
844
845/// Authorizer object, which provides responses to authorization queries
846#[repr(transparent)]
847#[derive(Debug, Clone, RefCast)]
848pub struct Authorizer(authorizer::Authorizer);
849
850#[doc(hidden)] // because this converts to a private/internal type
851impl AsRef<authorizer::Authorizer> for Authorizer {
852 fn as_ref(&self) -> &authorizer::Authorizer {
853 &self.0
854 }
855}
856
857impl Default for Authorizer {
858 fn default() -> Self {
859 Self::new()
860 }
861}
862
863impl Authorizer {
864 /// Create a new `Authorizer`
865 ///
866 /// The authorizer uses the `stacker` crate to manage stack size and tries to use a sane default.
867 /// If the default is not right for you, you can try wrapping the authorizer or individual calls
868 /// to `is_authorized` in `stacker::grow`.
869 /// Note that on platforms not supported by `stacker` (e.g., Wasm, Android),
870 /// the authorizer will simply assume that the stack size is sufficient. As a result, large inputs
871 /// may result in stack overflows and crashing the process.
872 /// But on all platforms supported by `stacker` (Linux, macOS, ...), Cedar will return the
873 /// graceful error `RecursionLimit` instead of crashing.
874 /// ```
875 /// # use cedar_policy::{Authorizer, Context, Entities, EntityId, EntityTypeName,
876 /// # EntityUid, Request,PolicySet};
877 /// # use std::str::FromStr;
878 /// # // create a request
879 /// # let p_eid = EntityId::from_str("alice").unwrap();
880 /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
881 /// # let p = EntityUid::from_type_name_and_id(p_name, p_eid);
882 /// #
883 /// # let a_eid = EntityId::from_str("view").unwrap();
884 /// # let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
885 /// # let a = EntityUid::from_type_name_and_id(a_name, a_eid);
886 /// #
887 /// # let r_eid = EntityId::from_str("trip").unwrap();
888 /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
889 /// # let r = EntityUid::from_type_name_and_id(r_name, r_eid);
890 /// #
891 /// # let c = Context::empty();
892 /// #
893 /// # let request: Request = Request::new(p, a, r, c, None).unwrap();
894 /// #
895 /// # // create a policy
896 /// # let s = r#"permit(
897 /// # principal == User::"alice",
898 /// # action == Action::"view",
899 /// # resource == Album::"trip"
900 /// # )when{
901 /// # principal.ip_addr.isIpv4()
902 /// # };
903 /// # "#;
904 /// # let policy = PolicySet::from_str(s).expect("policy error");
905 /// # // create entities
906 /// # let e = r#"[
907 /// # {
908 /// # "uid": {"type":"User","id":"alice"},
909 /// # "attrs": {
910 /// # "age":19,
911 /// # "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
912 /// # },
913 /// # "parents": []
914 /// # }
915 /// # ]"#;
916 /// # let entities = Entities::from_json_str(e, None).expect("entity error");
917 /// let authorizer = Authorizer::new();
918 /// let r = authorizer.is_authorized(&request, &policy, &entities);
919 /// ```
920 pub fn new() -> Self {
921 Self(authorizer::Authorizer::new())
922 }
923
924 /// Returns an authorization response for `r` with respect to the given
925 /// `PolicySet` and `Entities`.
926 ///
927 /// The language spec and formal model give a precise definition of how this
928 /// is computed.
929 /// ```
930 /// # use cedar_policy::{Authorizer,Context,Decision,Entities,EntityId,EntityTypeName, EntityUid, Request,PolicySet};
931 /// # use std::str::FromStr;
932 /// // create a request
933 /// let p_eid = EntityId::from_str("alice").unwrap();
934 /// let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
935 /// let p = EntityUid::from_type_name_and_id(p_name, p_eid);
936 ///
937 /// let a_eid = EntityId::from_str("view").unwrap();
938 /// let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
939 /// let a = EntityUid::from_type_name_and_id(a_name, a_eid);
940 ///
941 /// let r_eid = EntityId::from_str("trip").unwrap();
942 /// let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
943 /// let r = EntityUid::from_type_name_and_id(r_name, r_eid);
944 ///
945 /// let c = Context::empty();
946 ///
947 /// let request: Request = Request::new(p, a, r, c, None).unwrap();
948 ///
949 /// // create a policy
950 /// let s = r#"
951 /// permit (
952 /// principal == User::"alice",
953 /// action == Action::"view",
954 /// resource == Album::"trip"
955 /// )
956 /// when { principal.ip_addr.isIpv4() };
957 /// "#;
958 /// let policy = PolicySet::from_str(s).expect("policy error");
959 ///
960 /// // create entities
961 /// let e = r#"[
962 /// {
963 /// "uid": {"type":"User","id":"alice"},
964 /// "attrs": {
965 /// "age":19,
966 /// "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
967 /// },
968 /// "parents": []
969 /// }
970 /// ]"#;
971 /// let entities = Entities::from_json_str(e, None).expect("entity error");
972 ///
973 /// let authorizer = Authorizer::new();
974 /// let response = authorizer.is_authorized(&request, &policy, &entities);
975 /// assert_eq!(response.decision(), Decision::Allow);
976 /// ```
977 pub fn is_authorized(&self, r: &Request, p: &PolicySet, e: &Entities) -> Response {
978 self.0.is_authorized(r.0.clone(), &p.ast, &e.0).into()
979 }
980
981 /// A partially evaluated authorization request.
982 /// The Authorizer will attempt to make as much progress as possible in the presence of unknowns.
983 /// If the Authorizer can reach a response, it will return that response.
984 /// Otherwise, it will return a list of residual policies that still need to be evaluated.
985 #[doc = include_str!("../experimental_warning.md")]
986 #[cfg(feature = "partial-eval")]
987 pub fn is_authorized_partial(
988 &self,
989 query: &Request,
990 policy_set: &PolicySet,
991 entities: &Entities,
992 ) -> PartialResponse {
993 let response = self
994 .0
995 .is_authorized_core(query.0.clone(), &policy_set.ast, &entities.0);
996 PartialResponse(response)
997 }
998}
999
1000/// Authorization response returned from the `Authorizer`
1001#[derive(Debug, PartialEq, Eq, Clone)]
1002pub struct Response {
1003 /// Authorization decision
1004 pub(crate) decision: Decision,
1005 /// Diagnostics providing more information on how this decision was reached
1006 pub(crate) diagnostics: Diagnostics,
1007}
1008
1009/// A partially evaluated authorization response.
1010///
1011/// Splits the results into several categories: satisfied, false, and residual for each policy effect.
1012/// Also tracks all the errors that were encountered during evaluation.
1013#[doc = include_str!("../experimental_warning.md")]
1014#[cfg(feature = "partial-eval")]
1015#[repr(transparent)]
1016#[derive(Debug, Clone, RefCast)]
1017pub struct PartialResponse(cedar_policy_core::authorizer::PartialResponse);
1018
1019#[cfg(feature = "partial-eval")]
1020impl PartialResponse {
1021 /// Attempt to reach a partial decision; the presence of residuals may result in returning [`None`],
1022 /// indicating that a decision could not be reached given the unknowns
1023 pub fn decision(&self) -> Option<Decision> {
1024 self.0.decision()
1025 }
1026
1027 /// Convert this response into a concrete evaluation response.
1028 /// All residuals are treated as errors
1029 pub fn concretize(self) -> Response {
1030 self.0.concretize().into()
1031 }
1032
1033 /// Returns the set of [`Policy`]s that were definitely satisfied.
1034 /// This will be the set of policies (both `permit` and `forbid`) that evaluated to `true`
1035 pub fn definitely_satisfied(&self) -> impl Iterator<Item = Policy> + '_ {
1036 self.0.definitely_satisfied().map(Policy::from_ast)
1037 }
1038
1039 /// Returns the set of [`PolicyId`]s that encountered errors
1040 pub fn definitely_errored(&self) -> impl Iterator<Item = &PolicyId> {
1041 self.0.definitely_errored().map(PolicyId::ref_cast)
1042 }
1043
1044 /// Returns an over-approximation of the set of determining policies
1045 ///
1046 /// This is all policies that may be determining for any substitution of the unknowns.
1047 /// Policies not in this set will not affect the final decision, regardless of any
1048 /// substitutions.
1049 ///
1050 /// For more information on what counts as "determining" see: <https://docs.cedarpolicy.com/auth/authorization.html#request-authorization>
1051 pub fn may_be_determining(&self) -> impl Iterator<Item = Policy> + '_ {
1052 self.0.may_be_determining().map(Policy::from_ast)
1053 }
1054
1055 /// Returns an under-approximation of the set of determining policies
1056 ///
1057 /// This is all policies that must be determining for all possible substitutions of the unknowns.
1058 /// This set will include policies that evaluated to `true` and are guaranteed to be
1059 /// contributing to the final authorization decision.
1060 ///
1061 /// For more information on what counts as "determining" see: <https://docs.cedarpolicy.com/auth/authorization.html#request-authorization>
1062 pub fn must_be_determining(&self) -> impl Iterator<Item = Policy> + '_ {
1063 self.0.must_be_determining().map(Policy::from_ast)
1064 }
1065
1066 /// Returns the set of non-trivial (meaning more than just `true` or `false`) residuals expressions
1067 pub fn nontrivial_residuals(&'_ self) -> impl Iterator<Item = Policy> + '_ {
1068 self.0.nontrivial_residuals().map(Policy::from_ast)
1069 }
1070
1071 /// Returns every policy as a residual expression
1072 pub fn all_residuals(&'_ self) -> impl Iterator<Item = Policy> + '_ {
1073 self.0.all_residuals().map(Policy::from_ast)
1074 }
1075
1076 /// Returns all unknown entities during the evaluation of the response
1077 pub fn unknown_entities(&self) -> HashSet<EntityUid> {
1078 let mut entity_uids = HashSet::new();
1079 for policy in self.0.all_residuals() {
1080 entity_uids.extend(policy.unknown_entities().into_iter().map(Into::into));
1081 }
1082 entity_uids
1083 }
1084
1085 /// Return the residual for a given [`PolicyId`], if it exists in the response
1086 pub fn get(&self, id: &PolicyId) -> Option<Policy> {
1087 self.0.get(id.as_ref()).map(Policy::from_ast)
1088 }
1089
1090 /// Attempt to re-authorize this response given a mapping from unknowns to values.
1091 #[allow(clippy::needless_pass_by_value)]
1092 #[deprecated = "use reauthorize_with_bindings"]
1093 pub fn reauthorize(
1094 &self,
1095 mapping: HashMap<SmolStr, RestrictedExpression>,
1096 auth: &Authorizer,
1097 es: &Entities,
1098 ) -> Result<Self, ReauthorizationError> {
1099 self.reauthorize_with_bindings(mapping.iter().map(|(k, v)| (k.as_str(), v)), auth, es)
1100 }
1101
1102 /// Attempt to re-authorize this response given a mapping from unknowns to values, provided as an iterator.
1103 /// Exhausts the iterator, returning any evaluation errors in the restricted expressions, regardless whether there is a matching unknown.
1104 pub fn reauthorize_with_bindings<'m>(
1105 &self,
1106 mapping: impl IntoIterator<Item = (&'m str, &'m RestrictedExpression)>,
1107 auth: &Authorizer,
1108 es: &Entities,
1109 ) -> Result<Self, ReauthorizationError> {
1110 let exts = Extensions::all_available();
1111 let evaluator = RestrictedEvaluator::new(exts);
1112 let mapping = mapping
1113 .into_iter()
1114 .map(|(name, expr)| {
1115 evaluator
1116 .interpret(BorrowedRestrictedExpr::new_unchecked(expr.0.as_ref()))
1117 .map(|v| (name.into(), v))
1118 })
1119 .collect::<Result<HashMap<_, _>, EvaluationError>>()?;
1120 let r = self.0.reauthorize(&mapping, &auth.0, &es.0)?;
1121 Ok(Self(r))
1122 }
1123}
1124
1125#[cfg(feature = "partial-eval")]
1126#[doc(hidden)]
1127impl From<cedar_policy_core::authorizer::PartialResponse> for PartialResponse {
1128 fn from(pr: cedar_policy_core::authorizer::PartialResponse) -> Self {
1129 Self(pr)
1130 }
1131}
1132
1133/// Diagnostics providing more information on how a `Decision` was reached
1134#[derive(Debug, PartialEq, Eq, Clone)]
1135pub struct Diagnostics {
1136 /// `PolicyId`s of the policies that contributed to the decision.
1137 /// If no policies applied to the request, this set will be empty.
1138 reason: HashSet<PolicyId>,
1139 /// Errors that occurred during authorization. The errors should be
1140 /// treated as unordered, since policies may be evaluated in any order.
1141 errors: Vec<AuthorizationError>,
1142}
1143
1144#[doc(hidden)]
1145impl From<authorizer::Diagnostics> for Diagnostics {
1146 fn from(diagnostics: authorizer::Diagnostics) -> Self {
1147 Self {
1148 reason: diagnostics.reason.into_iter().map(PolicyId::new).collect(),
1149 errors: diagnostics.errors.into_iter().map(Into::into).collect(),
1150 }
1151 }
1152}
1153
1154impl Diagnostics {
1155 /// Get the `PolicyId`s of the policies that contributed to the decision.
1156 /// If no policies applied to the request, this set will be empty.
1157 /// ```
1158 /// # use cedar_policy::{Authorizer, Context, Decision, Entities, EntityId, EntityTypeName,
1159 /// # EntityUid, Request,PolicySet};
1160 /// # use std::str::FromStr;
1161 /// # // create a request
1162 /// # let p_eid = EntityId::from_str("alice").unwrap();
1163 /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
1164 /// # let p = EntityUid::from_type_name_and_id(p_name, p_eid);
1165 /// #
1166 /// # let a_eid = EntityId::from_str("view").unwrap();
1167 /// # let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
1168 /// # let a = EntityUid::from_type_name_and_id(a_name, a_eid);
1169 /// #
1170 /// # let r_eid = EntityId::from_str("trip").unwrap();
1171 /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
1172 /// # let r = EntityUid::from_type_name_and_id(r_name, r_eid);
1173 /// #
1174 /// # let c = Context::empty();
1175 /// #
1176 /// # let request: Request = Request::new(p, a, r, c, None).unwrap();
1177 /// #
1178 /// # // create a policy
1179 /// # let s = r#"permit(
1180 /// # principal == User::"alice",
1181 /// # action == Action::"view",
1182 /// # resource == Album::"trip"
1183 /// # )when{
1184 /// # principal.ip_addr.isIpv4()
1185 /// # };
1186 /// # "#;
1187 /// # let policy = PolicySet::from_str(s).expect("policy error");
1188 /// # // create entities
1189 /// # let e = r#"[
1190 /// # {
1191 /// # "uid": {"type":"User","id":"alice"},
1192 /// # "attrs": {
1193 /// # "age":19,
1194 /// # "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
1195 /// # },
1196 /// # "parents": []
1197 /// # }
1198 /// # ]"#;
1199 /// # let entities = Entities::from_json_str(e, None).expect("entity error");
1200 /// let authorizer = Authorizer::new();
1201 /// let response = authorizer.is_authorized(&request, &policy, &entities);
1202 /// match response.decision() {
1203 /// Decision::Allow => println!("ALLOW"),
1204 /// Decision::Deny => println!("DENY"),
1205 /// }
1206 /// println!("note: this decision was due to the following policies:");
1207 /// for reason in response.diagnostics().reason() {
1208 /// println!("{}", reason);
1209 /// }
1210 /// ```
1211 pub fn reason(&self) -> impl Iterator<Item = &PolicyId> {
1212 self.reason.iter()
1213 }
1214
1215 /// Get the errors that occurred during authorization. The errors should be
1216 /// treated as unordered, since policies may be evaluated in any order.
1217 /// ```
1218 /// # use cedar_policy::{Authorizer, Context, Decision, Entities, EntityId, EntityTypeName,
1219 /// # EntityUid, Request,PolicySet};
1220 /// # use std::str::FromStr;
1221 /// # // create a request
1222 /// # let p_eid = EntityId::from_str("alice").unwrap();
1223 /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
1224 /// # let p = EntityUid::from_type_name_and_id(p_name, p_eid);
1225 /// #
1226 /// # let a_eid = EntityId::from_str("view").unwrap();
1227 /// # let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
1228 /// # let a = EntityUid::from_type_name_and_id(a_name, a_eid);
1229 /// #
1230 /// # let r_eid = EntityId::from_str("trip").unwrap();
1231 /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
1232 /// # let r = EntityUid::from_type_name_and_id(r_name, r_eid);
1233 /// #
1234 /// # let c = Context::empty();
1235 /// #
1236 /// # let request: Request = Request::new(p, a, r, c, None).unwrap();
1237 /// #
1238 /// # // create a policy
1239 /// # let s = r#"permit(
1240 /// # principal == User::"alice",
1241 /// # action == Action::"view",
1242 /// # resource == Album::"trip"
1243 /// # )when{
1244 /// # principal.ip_addr.isIpv4()
1245 /// # };
1246 /// # "#;
1247 /// # let policy = PolicySet::from_str(s).expect("policy error");
1248 /// # // create entities
1249 /// # let e = r#"[
1250 /// # {
1251 /// # "uid": {"type":"User","id":"alice"},
1252 /// # "attrs": {
1253 /// # "age":19,
1254 /// # "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
1255 /// # },
1256 /// # "parents": []
1257 /// # }
1258 /// # ]"#;
1259 /// # let entities = Entities::from_json_str(e, None).expect("entity error");
1260 /// let authorizer = Authorizer::new();
1261 /// let response = authorizer.is_authorized(&request, &policy, &entities);
1262 /// match response.decision() {
1263 /// Decision::Allow => println!("ALLOW"),
1264 /// Decision::Deny => println!("DENY"),
1265 /// }
1266 /// for err in response.diagnostics().errors() {
1267 /// println!("{}", err);
1268 /// }
1269 /// ```
1270 pub fn errors(&self) -> impl Iterator<Item = &AuthorizationError> + '_ {
1271 self.errors.iter()
1272 }
1273
1274 /// Consume the `Diagnostics`, producing owned versions of `reason()` and `errors()`
1275 pub(crate) fn into_components(
1276 self,
1277 ) -> (
1278 impl Iterator<Item = PolicyId>,
1279 impl Iterator<Item = AuthorizationError>,
1280 ) {
1281 (self.reason.into_iter(), self.errors.into_iter())
1282 }
1283}
1284
1285impl Response {
1286 /// Create a new `Response`
1287 pub fn new(
1288 decision: Decision,
1289 reason: HashSet<PolicyId>,
1290 errors: Vec<AuthorizationError>,
1291 ) -> Self {
1292 Self {
1293 decision,
1294 diagnostics: Diagnostics { reason, errors },
1295 }
1296 }
1297
1298 /// Get the authorization decision
1299 pub fn decision(&self) -> Decision {
1300 self.decision
1301 }
1302
1303 /// Get the authorization diagnostics
1304 pub fn diagnostics(&self) -> &Diagnostics {
1305 &self.diagnostics
1306 }
1307}
1308
1309#[doc(hidden)]
1310impl From<authorizer::Response> for Response {
1311 fn from(a: authorizer::Response) -> Self {
1312 Self {
1313 decision: a.decision,
1314 diagnostics: a.diagnostics.into(),
1315 }
1316 }
1317}
1318
1319/// Used to select how a policy will be validated.
1320#[derive(Default, Eq, PartialEq, Copy, Clone, Debug, Serialize, Deserialize)]
1321#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
1322#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1323#[serde(rename_all = "camelCase")]
1324#[non_exhaustive]
1325pub enum ValidationMode {
1326 /// Validate that policies do not contain any type errors, and additionally
1327 /// have a restricted form which is amenable for analysis.
1328 #[default]
1329 Strict,
1330 /// Validate that policies do not contain any type errors.
1331 #[doc = include_str!("../experimental_warning.md")]
1332 #[cfg(feature = "permissive-validate")]
1333 Permissive,
1334 /// Validate using a partial schema. Policies may contain type errors.
1335 #[doc = include_str!("../experimental_warning.md")]
1336 #[cfg(feature = "partial-validate")]
1337 Partial,
1338}
1339
1340#[doc(hidden)]
1341impl From<ValidationMode> for cedar_policy_validator::ValidationMode {
1342 fn from(mode: ValidationMode) -> Self {
1343 match mode {
1344 ValidationMode::Strict => Self::Strict,
1345 #[cfg(feature = "permissive-validate")]
1346 ValidationMode::Permissive => Self::Permissive,
1347 #[cfg(feature = "partial-validate")]
1348 ValidationMode::Partial => Self::Partial,
1349 }
1350 }
1351}
1352
1353/// Validator object, which provides policy validation and typechecking.
1354#[repr(transparent)]
1355#[derive(Debug, Clone, RefCast)]
1356pub struct Validator(cedar_policy_validator::Validator);
1357
1358#[doc(hidden)] // because this converts to a private/internal type
1359impl AsRef<cedar_policy_validator::Validator> for Validator {
1360 fn as_ref(&self) -> &cedar_policy_validator::Validator {
1361 &self.0
1362 }
1363}
1364
1365impl Validator {
1366 /// Construct a new `Validator` to validate policies using the given
1367 /// `Schema`.
1368 pub fn new(schema: Schema) -> Self {
1369 Self(cedar_policy_validator::Validator::new(schema.0))
1370 }
1371
1372 /// Get the `Schema` this `Validator` is using.
1373 pub fn schema(&self) -> &Schema {
1374 RefCast::ref_cast(self.0.schema())
1375 }
1376
1377 /// Validate all policies in a policy set, collecting all validation errors
1378 /// found into the returned `ValidationResult`. Each error is returned together with the
1379 /// policy id of the policy where the error was found. If a policy id
1380 /// included in the input policy set does not appear in the output iterator, then
1381 /// that policy passed the validator. If the function `validation_passed`
1382 /// returns true, then there were no validation errors found, so all
1383 /// policies in the policy set have passed the validator.
1384 pub fn validate(&self, pset: &PolicySet, mode: ValidationMode) -> ValidationResult {
1385 ValidationResult::from(self.0.validate(&pset.ast, mode.into()))
1386 }
1387
1388 /// Validate all policies in a policy set, collecting all validation errors
1389 /// found into the returned `ValidationResult`. If validation passes, run level
1390 /// validation (RFC 76). Each error is returned together with the policy id of the policy
1391 /// where the error was found. If a policy id included in the input policy set does not
1392 /// appear in the output iterator, then that policy passed the validator. If the function
1393 /// `validation_passed` returns true, then there were no validation errors found, so
1394 /// all policies in the policy set have passed the validator.
1395 pub fn validate_with_level(
1396 &self,
1397 pset: &PolicySet,
1398 mode: ValidationMode,
1399 max_deref_level: u32,
1400 ) -> ValidationResult {
1401 ValidationResult::from(
1402 self.0
1403 .validate_with_level(&pset.ast, mode.into(), max_deref_level),
1404 )
1405 }
1406}
1407
1408/// Contains all the type information used to construct a `Schema` that can be
1409/// used to validate a policy.
1410#[derive(Debug, Clone)]
1411pub struct SchemaFragment {
1412 value: cedar_policy_validator::ValidatorSchemaFragment<
1413 cedar_policy_validator::ConditionalName,
1414 cedar_policy_validator::ConditionalName,
1415 >,
1416 lossless: cedar_policy_validator::json_schema::Fragment<cedar_policy_validator::RawName>,
1417}
1418
1419#[doc(hidden)] // because this converts to a private/internal type
1420impl
1421 AsRef<
1422 cedar_policy_validator::ValidatorSchemaFragment<
1423 cedar_policy_validator::ConditionalName,
1424 cedar_policy_validator::ConditionalName,
1425 >,
1426 > for SchemaFragment
1427{
1428 fn as_ref(
1429 &self,
1430 ) -> &cedar_policy_validator::ValidatorSchemaFragment<
1431 cedar_policy_validator::ConditionalName,
1432 cedar_policy_validator::ConditionalName,
1433 > {
1434 &self.value
1435 }
1436}
1437
1438fn get_annotation_by_key(
1439 annotations: &est::Annotations,
1440 annotation_key: impl AsRef<str>,
1441) -> Option<&str> {
1442 annotations
1443 .0
1444 .get(&annotation_key.as_ref().parse().ok()?)
1445 .map(|value| annotation_value_to_str_ref(value.as_ref()))
1446}
1447
1448fn annotation_value_to_str_ref(value: Option<&ast::Annotation>) -> &str {
1449 value.map_or("", |a| a.as_ref())
1450}
1451
1452fn annotations_to_pairs(annotations: &est::Annotations) -> impl Iterator<Item = (&str, &str)> {
1453 annotations
1454 .0
1455 .iter()
1456 .map(|(key, value)| (key.as_ref(), annotation_value_to_str_ref(value.as_ref())))
1457}
1458
1459impl SchemaFragment {
1460 /// Get annotations of a non-empty namespace.
1461 ///
1462 /// We do not allow namespace-level annotations on the empty namespace.
1463 ///
1464 /// Returns `None` if `namespace` is not found in the [`SchemaFragment`]
1465 pub fn namespace_annotations(
1466 &self,
1467 namespace: EntityNamespace,
1468 ) -> Option<impl Iterator<Item = (&str, &str)>> {
1469 self.lossless
1470 .0
1471 .get(&Some(namespace.0))
1472 .map(|ns_def| annotations_to_pairs(&ns_def.annotations))
1473 }
1474
1475 /// Get annotation value of a non-empty namespace by annotation key
1476 /// `annotation_key`
1477 ///
1478 /// We do not allow namespace-level annotations on the empty namespace.
1479 ///
1480 /// Returns `None` if `namespace` is not found in the [`SchemaFragment`]
1481 /// or `annotation_key` is not a valid annotation key
1482 /// or it does not exist
1483 pub fn namespace_annotation(
1484 &self,
1485 namespace: EntityNamespace,
1486 annotation_key: impl AsRef<str>,
1487 ) -> Option<&str> {
1488 let ns = self.lossless.0.get(&Some(namespace.0))?;
1489 get_annotation_by_key(&ns.annotations, annotation_key)
1490 }
1491
1492 /// Get annotations of a common type declaration
1493 ///
1494 /// Returns `None` if `namespace` is not found in the [`SchemaFragment`] or
1495 /// `ty` is not a valid common type ID or `ty` is not found in the
1496 /// corresponding namespace definition
1497 pub fn common_type_annotations(
1498 &self,
1499 namespace: Option<EntityNamespace>,
1500 ty: &str,
1501 ) -> Option<impl Iterator<Item = (&str, &str)>> {
1502 let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
1503 let ty = json_schema::CommonTypeId::new(ast::UnreservedId::from_normalized_str(ty).ok()?)
1504 .ok()?;
1505 ns_def
1506 .common_types
1507 .get(&ty)
1508 .map(|ty| annotations_to_pairs(&ty.annotations))
1509 }
1510
1511 /// Get annotation value of a common type declaration by annotation key
1512 /// `annotation_key`
1513 ///
1514 /// Returns `None` if `namespace` is not found in the [`SchemaFragment`]
1515 /// or `ty` is not a valid common type ID
1516 /// or `ty` is not found in the corresponding namespace definition
1517 /// or `annotation_key` is not a valid annotation key
1518 /// or it does not exist
1519 pub fn common_type_annotation(
1520 &self,
1521 namespace: Option<EntityNamespace>,
1522 ty: &str,
1523 annotation_key: impl AsRef<str>,
1524 ) -> Option<&str> {
1525 let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
1526 let ty = json_schema::CommonTypeId::new(ast::UnreservedId::from_normalized_str(ty).ok()?)
1527 .ok()?;
1528 get_annotation_by_key(&ns_def.common_types.get(&ty)?.annotations, annotation_key)
1529 }
1530
1531 /// Get annotations of an entity type declaration
1532 ///
1533 /// Returns `None` if `namespace` is not found in the [`SchemaFragment`] or
1534 /// `ty` is not a valid entity type name or `ty` is not found in the
1535 /// corresponding namespace definition
1536 pub fn entity_type_annotations(
1537 &self,
1538 namespace: Option<EntityNamespace>,
1539 ty: &str,
1540 ) -> Option<impl Iterator<Item = (&str, &str)>> {
1541 let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
1542 let ty = ast::UnreservedId::from_normalized_str(ty).ok()?;
1543 ns_def
1544 .entity_types
1545 .get(&ty)
1546 .map(|ty| annotations_to_pairs(&ty.annotations))
1547 }
1548
1549 /// Get annotation value of an entity type declaration by annotation key
1550 /// `annotation_key`
1551 ///
1552 /// Returns `None` if `namespace` is not found in the [`SchemaFragment`]
1553 /// or `ty` is not a valid entity type name
1554 /// or `ty` is not found in the corresponding namespace definition
1555 /// or `annotation_key` is not a valid annotation key
1556 /// or it does not exist
1557 pub fn entity_type_annotation(
1558 &self,
1559 namespace: Option<EntityNamespace>,
1560 ty: &str,
1561 annotation_key: impl AsRef<str>,
1562 ) -> Option<&str> {
1563 let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
1564 let ty = ast::UnreservedId::from_normalized_str(ty).ok()?;
1565 get_annotation_by_key(&ns_def.entity_types.get(&ty)?.annotations, annotation_key)
1566 }
1567
1568 /// Get annotations of an action declaration
1569 ///
1570 /// Returns `None` if `namespace` is not found in the [`SchemaFragment`] or
1571 /// `id` is not found in the corresponding namespace definition
1572 pub fn action_annotations(
1573 &self,
1574 namespace: Option<EntityNamespace>,
1575 id: &EntityId,
1576 ) -> Option<impl Iterator<Item = (&str, &str)>> {
1577 let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
1578 ns_def
1579 .actions
1580 .get(id.unescaped())
1581 .map(|a| annotations_to_pairs(&a.annotations))
1582 }
1583
1584 /// Get annotation value of an action declaration by annotation key
1585 /// `annotation_key`
1586 ///
1587 /// Returns `None` if `namespace` is not found in the [`SchemaFragment`]
1588 /// or `id` is not found in the corresponding namespace definition
1589 /// or `annotation_key` is not a valid annotation key
1590 /// or it does not exist
1591 pub fn action_annotation(
1592 &self,
1593 namespace: Option<EntityNamespace>,
1594 id: &EntityId,
1595 annotation_key: impl AsRef<str>,
1596 ) -> Option<&str> {
1597 let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
1598 get_annotation_by_key(
1599 &ns_def.actions.get(id.unescaped())?.annotations,
1600 annotation_key,
1601 )
1602 }
1603
1604 /// Extract namespaces defined in this [`SchemaFragment`].
1605 ///
1606 /// `None` indicates the empty namespace.
1607 pub fn namespaces(&self) -> impl Iterator<Item = Option<EntityNamespace>> + '_ {
1608 self.value.namespaces().filter_map(|ns| {
1609 match ns.map(|ns| ast::Name::try_from(ns.clone())) {
1610 Some(Ok(n)) => Some(Some(EntityNamespace(n))),
1611 None => Some(None), // empty namespace, which we want to surface to the user
1612 Some(Err(_)) => {
1613 // if the `SchemaFragment` contains namespaces with
1614 // reserved `__cedar` components, that's an internal
1615 // implementation detail; hide that from the user.
1616 // Also note that `EntityNamespace` is backed by `Name`
1617 // which can't even contain names with reserved
1618 // `__cedar` components.
1619 None
1620 }
1621 }
1622 })
1623 }
1624
1625 /// Create a [`SchemaFragment`] from a string containing JSON in the
1626 /// JSON schema format.
1627 pub fn from_json_str(src: &str) -> Result<Self, SchemaError> {
1628 let lossless = cedar_policy_validator::json_schema::Fragment::from_json_str(src)?;
1629 Ok(Self {
1630 value: lossless.clone().try_into()?,
1631 lossless,
1632 })
1633 }
1634
1635 /// Create a [`SchemaFragment`] from a JSON value (which should be an
1636 /// object of the shape required for the JSON schema format).
1637 pub fn from_json_value(json: serde_json::Value) -> Result<Self, SchemaError> {
1638 let lossless = cedar_policy_validator::json_schema::Fragment::from_json_value(json)?;
1639 Ok(Self {
1640 value: lossless.clone().try_into()?,
1641 lossless,
1642 })
1643 }
1644
1645 /// Parse a [`SchemaFragment`] from a reader containing the Cedar schema syntax
1646 pub fn from_cedarschema_file(
1647 r: impl std::io::Read,
1648 ) -> Result<(Self, impl Iterator<Item = SchemaWarning>), CedarSchemaError> {
1649 let (lossless, warnings) =
1650 cedar_policy_validator::json_schema::Fragment::from_cedarschema_file(
1651 r,
1652 Extensions::all_available(),
1653 )?;
1654 Ok((
1655 Self {
1656 value: lossless.clone().try_into()?,
1657 lossless,
1658 },
1659 warnings,
1660 ))
1661 }
1662
1663 /// Parse a [`SchemaFragment`] from a string containing the Cedar schema syntax
1664 pub fn from_cedarschema_str(
1665 src: &str,
1666 ) -> Result<(Self, impl Iterator<Item = SchemaWarning>), CedarSchemaError> {
1667 let (lossless, warnings) =
1668 cedar_policy_validator::json_schema::Fragment::from_cedarschema_str(
1669 src,
1670 Extensions::all_available(),
1671 )?;
1672 Ok((
1673 Self {
1674 value: lossless.clone().try_into()?,
1675 lossless,
1676 },
1677 warnings,
1678 ))
1679 }
1680
1681 /// Create a [`SchemaFragment`] directly from a JSON file (which should
1682 /// contain an object of the shape required for the JSON schema format).
1683 pub fn from_json_file(file: impl std::io::Read) -> Result<Self, SchemaError> {
1684 let lossless = cedar_policy_validator::json_schema::Fragment::from_json_file(file)?;
1685 Ok(Self {
1686 value: lossless.clone().try_into()?,
1687 lossless,
1688 })
1689 }
1690
1691 /// Serialize this [`SchemaFragment`] as a JSON value
1692 pub fn to_json_value(self) -> Result<serde_json::Value, SchemaError> {
1693 serde_json::to_value(self.lossless).map_err(|e| SchemaError::JsonSerialization(e.into()))
1694 }
1695
1696 /// Serialize this [`SchemaFragment`] as a JSON string
1697 pub fn to_json_string(&self) -> Result<String, SchemaError> {
1698 serde_json::to_string(&self.lossless).map_err(|e| SchemaError::JsonSerialization(e.into()))
1699 }
1700
1701 /// Serialize this [`SchemaFragment`] into a string in the Cedar schema
1702 /// syntax
1703 pub fn to_cedarschema(&self) -> Result<String, ToCedarSchemaError> {
1704 let str = self.lossless.to_cedarschema()?;
1705 Ok(str)
1706 }
1707}
1708
1709impl TryInto<Schema> for SchemaFragment {
1710 type Error = SchemaError;
1711
1712 /// Convert [`SchemaFragment`] into a [`Schema`]. To build the [`Schema`] we
1713 /// need to have all entity types defined, so an error will be returned if
1714 /// any undeclared entity types are referenced in the schema fragment.
1715 fn try_into(self) -> Result<Schema, Self::Error> {
1716 Ok(Schema(
1717 cedar_policy_validator::ValidatorSchema::from_schema_fragments(
1718 [self.value],
1719 Extensions::all_available(),
1720 )?,
1721 ))
1722 }
1723}
1724
1725impl FromStr for SchemaFragment {
1726 type Err = CedarSchemaError;
1727 /// Construct [`SchemaFragment`] from a string containing a schema formatted
1728 /// in the Cedar schema format. This can fail if the string is not a valid
1729 /// schema. This function does not check for consistency in the schema
1730 /// (e.g., references to undefined entities) because this is not required
1731 /// until a `Schema` is constructed.
1732 fn from_str(src: &str) -> Result<Self, Self::Err> {
1733 Self::from_cedarschema_str(src).map(|(frag, _)| frag)
1734 }
1735}
1736
1737/// Object containing schema information used by the validator.
1738#[repr(transparent)]
1739#[derive(Debug, Clone, RefCast)]
1740pub struct Schema(pub(crate) cedar_policy_validator::ValidatorSchema);
1741
1742#[doc(hidden)] // because this converts to a private/internal type
1743impl AsRef<cedar_policy_validator::ValidatorSchema> for Schema {
1744 fn as_ref(&self) -> &cedar_policy_validator::ValidatorSchema {
1745 &self.0
1746 }
1747}
1748
1749impl FromStr for Schema {
1750 type Err = CedarSchemaError;
1751
1752 /// Construct a [`Schema`] from a string containing a schema formatted in
1753 /// the Cedar schema format. This can fail if it is not possible to parse a
1754 /// schema from the string, or if errors in values in the schema are
1755 /// uncovered after parsing. For instance, when an entity attribute name is
1756 /// found to not be a valid attribute name according to the Cedar
1757 /// grammar.
1758 fn from_str(schema_src: &str) -> Result<Self, Self::Err> {
1759 Self::from_cedarschema_str(schema_src).map(|(schema, _)| schema)
1760 }
1761}
1762
1763impl Schema {
1764 /// Create a [`Schema`] from multiple [`SchemaFragment`]. The individual
1765 /// fragments may reference entity or common types that are not declared in that
1766 /// fragment, but all referenced entity and common types must be declared in some
1767 /// fragment.
1768 pub fn from_schema_fragments(
1769 fragments: impl IntoIterator<Item = SchemaFragment>,
1770 ) -> Result<Self, SchemaError> {
1771 Ok(Self(
1772 cedar_policy_validator::ValidatorSchema::from_schema_fragments(
1773 fragments.into_iter().map(|f| f.value),
1774 Extensions::all_available(),
1775 )?,
1776 ))
1777 }
1778
1779 /// Create a [`Schema`] from a JSON value (which should be an object of the
1780 /// shape required for the JSON schema format).
1781 pub fn from_json_value(json: serde_json::Value) -> Result<Self, SchemaError> {
1782 Ok(Self(
1783 cedar_policy_validator::ValidatorSchema::from_json_value(
1784 json,
1785 Extensions::all_available(),
1786 )?,
1787 ))
1788 }
1789
1790 /// Create a [`Schema`] from a string containing JSON in the appropriate
1791 /// shape.
1792 pub fn from_json_str(json: &str) -> Result<Self, SchemaError> {
1793 Ok(Self(
1794 cedar_policy_validator::ValidatorSchema::from_json_str(
1795 json,
1796 Extensions::all_available(),
1797 )?,
1798 ))
1799 }
1800
1801 /// Create a [`Schema`] directly from a file containing JSON in the
1802 /// appropriate shape.
1803 pub fn from_json_file(file: impl std::io::Read) -> Result<Self, SchemaError> {
1804 Ok(Self(
1805 cedar_policy_validator::ValidatorSchema::from_json_file(
1806 file,
1807 Extensions::all_available(),
1808 )?,
1809 ))
1810 }
1811
1812 /// Parse the schema from a reader, in the Cedar schema format.
1813 pub fn from_cedarschema_file(
1814 file: impl std::io::Read,
1815 ) -> Result<(Self, impl Iterator<Item = SchemaWarning> + 'static), CedarSchemaError> {
1816 let (schema, warnings) = cedar_policy_validator::ValidatorSchema::from_cedarschema_file(
1817 file,
1818 Extensions::all_available(),
1819 )?;
1820 Ok((Self(schema), warnings))
1821 }
1822
1823 /// Parse the schema from a string, in the Cedar schema format.
1824 pub fn from_cedarschema_str(
1825 src: &str,
1826 ) -> Result<(Self, impl Iterator<Item = SchemaWarning>), CedarSchemaError> {
1827 let (schema, warnings) = cedar_policy_validator::ValidatorSchema::from_cedarschema_str(
1828 src,
1829 Extensions::all_available(),
1830 )?;
1831 Ok((Self(schema), warnings))
1832 }
1833
1834 /// Extract from the schema an [`Entities`] containing the action entities
1835 /// declared in the schema.
1836 pub fn action_entities(&self) -> Result<Entities, EntitiesError> {
1837 Ok(Entities(self.0.action_entities()?))
1838 }
1839
1840 /// Returns an iterator over every entity type that can be a principal for any action in this schema
1841 ///
1842 /// Note: this iterator may contain duplicates.
1843 ///
1844 /// # Examples
1845 /// Here's an example of using a [`std::collections::HashSet`] to get a de-duplicated set of principals
1846 /// ```
1847 /// use std::collections::HashSet;
1848 /// use cedar_policy::Schema;
1849 /// let schema : Schema = r#"
1850 /// entity User;
1851 /// entity Folder;
1852 /// action Access appliesTo {
1853 /// principal : User,
1854 /// resource : Folder,
1855 /// };
1856 /// action Delete appliesTo {
1857 /// principal : User,
1858 /// resource : Folder,
1859 /// };
1860 /// "#.parse().unwrap();
1861 /// let principals = schema.principals().collect::<HashSet<_>>();
1862 /// assert_eq!(principals, HashSet::from([&"User".parse().unwrap()]));
1863 /// ```
1864 pub fn principals(&self) -> impl Iterator<Item = &EntityTypeName> {
1865 self.0.principals().map(RefCast::ref_cast)
1866 }
1867
1868 /// Returns an iterator over every entity type that can be a resource for any action in this schema
1869 ///
1870 /// Note: this iterator may contain duplicates.
1871 /// # Examples
1872 /// Here's an example of using a [`std::collections::HashSet`] to get a de-duplicated set of resources
1873 /// ```
1874 /// use std::collections::HashSet;
1875 /// use cedar_policy::Schema;
1876 /// let schema : Schema = r#"
1877 /// entity User;
1878 /// entity Folder;
1879 /// action Access appliesTo {
1880 /// principal : User,
1881 /// resource : Folder,
1882 /// };
1883 /// action Delete appliesTo {
1884 /// principal : User,
1885 /// resource : Folder,
1886 /// };
1887 /// "#.parse().unwrap();
1888 /// let resources = schema.resources().collect::<HashSet<_>>();
1889 /// assert_eq!(resources, HashSet::from([&"Folder".parse().unwrap()]));
1890 /// ```
1891 pub fn resources(&self) -> impl Iterator<Item = &EntityTypeName> {
1892 self.0.resources().map(RefCast::ref_cast)
1893 }
1894
1895 /// Returns an iterator over every entity type that can be a principal for `action` in this schema
1896 ///
1897 /// ## Errors
1898 ///
1899 /// Returns [`None`] if `action` is not found in the schema
1900 pub fn principals_for_action(
1901 &self,
1902 action: &EntityUid,
1903 ) -> Option<impl Iterator<Item = &EntityTypeName>> {
1904 self.0
1905 .principals_for_action(&action.0)
1906 .map(|iter| iter.map(RefCast::ref_cast))
1907 }
1908
1909 /// Returns an iterator over every entity type that can be a resource for `action` in this schema
1910 ///
1911 /// ## Errors
1912 ///
1913 /// Returns [`None`] if `action` is not found in the schema
1914 pub fn resources_for_action(
1915 &self,
1916 action: &EntityUid,
1917 ) -> Option<impl Iterator<Item = &EntityTypeName>> {
1918 self.0
1919 .resources_for_action(&action.0)
1920 .map(|iter| iter.map(RefCast::ref_cast))
1921 }
1922
1923 /// Returns an iterator over all the [`RequestEnv`]s that are valid
1924 /// according to this schema.
1925 pub fn request_envs(&self) -> impl Iterator<Item = RequestEnv> + '_ {
1926 self.0
1927 .unlinked_request_envs(cedar_policy_validator::ValidationMode::Strict)
1928 .map(Into::into)
1929 }
1930
1931 /// Returns an iterator over all the entity types that can be an ancestor of `ty`
1932 ///
1933 /// ## Errors
1934 ///
1935 /// Returns [`None`] if the `ty` is not found in the schema
1936 pub fn ancestors<'a>(
1937 &'a self,
1938 ty: &'a EntityTypeName,
1939 ) -> Option<impl Iterator<Item = &'a EntityTypeName> + 'a> {
1940 self.0
1941 .ancestors(&ty.0)
1942 .map(|iter| iter.map(RefCast::ref_cast))
1943 }
1944
1945 /// Returns an iterator over all the action groups defined in this schema
1946 pub fn action_groups(&self) -> impl Iterator<Item = &EntityUid> {
1947 self.0.action_groups().map(RefCast::ref_cast)
1948 }
1949
1950 /// Returns an iterator over all entity types defined in this schema
1951 pub fn entity_types(&self) -> impl Iterator<Item = &EntityTypeName> {
1952 self.0
1953 .entity_types()
1954 .map(|ety| RefCast::ref_cast(ety.name()))
1955 }
1956
1957 /// Returns an iterator over all actions defined in this schema
1958 pub fn actions(&self) -> impl Iterator<Item = &EntityUid> {
1959 self.0.actions().map(RefCast::ref_cast)
1960 }
1961}
1962
1963/// Contains the result of policy validation.
1964///
1965/// The result includes the list of issues found by validation and whether validation succeeds or fails.
1966/// Validation succeeds if there are no fatal errors. There may still be
1967/// non-fatal warnings present when validation passes.
1968#[derive(Debug, Clone)]
1969pub struct ValidationResult {
1970 validation_errors: Vec<ValidationError>,
1971 validation_warnings: Vec<ValidationWarning>,
1972}
1973
1974impl ValidationResult {
1975 /// True when validation passes. There are no errors, but there may be
1976 /// non-fatal warnings. Use [`ValidationResult::validation_passed_without_warnings`]
1977 /// to check that there are also no warnings.
1978 pub fn validation_passed(&self) -> bool {
1979 self.validation_errors.is_empty()
1980 }
1981
1982 /// True when validation passes (i.e., there are no errors) and there are
1983 /// additionally no non-fatal warnings.
1984 pub fn validation_passed_without_warnings(&self) -> bool {
1985 self.validation_errors.is_empty() && self.validation_warnings.is_empty()
1986 }
1987
1988 /// Get an iterator over the errors found by the validator.
1989 pub fn validation_errors(&self) -> impl Iterator<Item = &ValidationError> {
1990 self.validation_errors.iter()
1991 }
1992
1993 /// Get an iterator over the warnings found by the validator.
1994 pub fn validation_warnings(&self) -> impl Iterator<Item = &ValidationWarning> {
1995 self.validation_warnings.iter()
1996 }
1997
1998 fn first_error_or_warning(&self) -> Option<&dyn Diagnostic> {
1999 self.validation_errors
2000 .first()
2001 .map(|e| e as &dyn Diagnostic)
2002 .or_else(|| {
2003 self.validation_warnings
2004 .first()
2005 .map(|w| w as &dyn Diagnostic)
2006 })
2007 }
2008
2009 pub(crate) fn into_errors_and_warnings(
2010 self,
2011 ) -> (
2012 impl Iterator<Item = ValidationError>,
2013 impl Iterator<Item = ValidationWarning>,
2014 ) {
2015 (
2016 self.validation_errors.into_iter(),
2017 self.validation_warnings.into_iter(),
2018 )
2019 }
2020}
2021
2022#[doc(hidden)]
2023impl From<cedar_policy_validator::ValidationResult> for ValidationResult {
2024 fn from(r: cedar_policy_validator::ValidationResult) -> Self {
2025 let (errors, warnings) = r.into_errors_and_warnings();
2026 Self {
2027 validation_errors: errors.map(ValidationError::from).collect(),
2028 validation_warnings: warnings.map(ValidationWarning::from).collect(),
2029 }
2030 }
2031}
2032
2033impl std::fmt::Display for ValidationResult {
2034 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2035 match self.first_error_or_warning() {
2036 Some(diagnostic) => write!(f, "{diagnostic}"),
2037 None => write!(f, "no errors or warnings"),
2038 }
2039 }
2040}
2041
2042impl std::error::Error for ValidationResult {
2043 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
2044 self.first_error_or_warning()
2045 .and_then(std::error::Error::source)
2046 }
2047
2048 #[allow(deprecated)]
2049 fn description(&self) -> &str {
2050 self.first_error_or_warning()
2051 .map_or("no errors or warnings", std::error::Error::description)
2052 }
2053
2054 #[allow(deprecated)]
2055 fn cause(&self) -> Option<&dyn std::error::Error> {
2056 self.first_error_or_warning()
2057 .and_then(std::error::Error::cause)
2058 }
2059}
2060
2061// Except for `.related()`, and `.severity` everything is forwarded to the first
2062// error, or to the first warning if there are no errors. This is done for the
2063// same reason as policy parse errors.
2064impl Diagnostic for ValidationResult {
2065 fn related(&self) -> Option<Box<dyn Iterator<Item = &dyn Diagnostic> + '_>> {
2066 let mut related = self
2067 .validation_errors
2068 .iter()
2069 .map(|err| err as &dyn Diagnostic)
2070 .chain(
2071 self.validation_warnings
2072 .iter()
2073 .map(|warn| warn as &dyn Diagnostic),
2074 );
2075 related.next().map(move |first| match first.related() {
2076 Some(first_related) => Box::new(first_related.chain(related)),
2077 None => Box::new(related) as Box<dyn Iterator<Item = _>>,
2078 })
2079 }
2080
2081 fn severity(&self) -> Option<miette::Severity> {
2082 self.first_error_or_warning()
2083 .map_or(Some(miette::Severity::Advice), Diagnostic::severity)
2084 }
2085
2086 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
2087 self.first_error_or_warning().and_then(Diagnostic::labels)
2088 }
2089
2090 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
2091 self.first_error_or_warning()
2092 .and_then(Diagnostic::source_code)
2093 }
2094
2095 fn code(&self) -> Option<Box<dyn std::fmt::Display + '_>> {
2096 self.first_error_or_warning().and_then(Diagnostic::code)
2097 }
2098
2099 fn url(&self) -> Option<Box<dyn std::fmt::Display + '_>> {
2100 self.first_error_or_warning().and_then(Diagnostic::url)
2101 }
2102
2103 fn help(&self) -> Option<Box<dyn std::fmt::Display + '_>> {
2104 self.first_error_or_warning().and_then(Diagnostic::help)
2105 }
2106
2107 fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
2108 self.first_error_or_warning()
2109 .and_then(Diagnostic::diagnostic_source)
2110 }
2111}
2112
2113/// Scan a set of policies for potentially confusing/obfuscating text.
2114///
2115/// These checks are also provided through [`Validator::validate`] which provides more
2116/// comprehensive error detection, but this function can be used to check for
2117/// confusable strings without defining a schema.
2118pub fn confusable_string_checker<'a>(
2119 templates: impl Iterator<Item = &'a Template> + 'a,
2120) -> impl Iterator<Item = ValidationWarning> + 'a {
2121 cedar_policy_validator::confusable_string_checks(templates.map(|t| &t.ast))
2122 .map(std::convert::Into::into)
2123}
2124
2125/// Represents a namespace.
2126///
2127/// An `EntityNamespace` can can be constructed using
2128/// [`EntityNamespace::from_str`] or by calling `parse()` on a string.
2129/// _This can fail_, so it is important to properly handle an `Err` result.
2130///
2131/// ```
2132/// # use cedar_policy::EntityNamespace;
2133/// let id : Result<EntityNamespace, _> = "My::Name::Space".parse();
2134/// # assert_eq!(id.unwrap().to_string(), "My::Name::Space".to_string());
2135/// ```
2136#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
2137pub struct EntityNamespace(pub(crate) ast::Name);
2138
2139#[doc(hidden)] // because this converts to a private/internal type
2140impl AsRef<ast::Name> for EntityNamespace {
2141 fn as_ref(&self) -> &ast::Name {
2142 &self.0
2143 }
2144}
2145
2146/// This `FromStr` implementation requires the _normalized_ representation of the
2147/// namespace. See <https://github.com/cedar-policy/rfcs/pull/9/>.
2148impl FromStr for EntityNamespace {
2149 type Err = ParseErrors;
2150
2151 fn from_str(namespace_str: &str) -> Result<Self, Self::Err> {
2152 ast::Name::from_normalized_str(namespace_str)
2153 .map(EntityNamespace)
2154 .map_err(Into::into)
2155 }
2156}
2157
2158impl std::fmt::Display for EntityNamespace {
2159 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2160 write!(f, "{}", self.0)
2161 }
2162}
2163
2164/// Represents a set of `Policy`s
2165#[derive(Debug, Clone, Default)]
2166pub struct PolicySet {
2167 /// AST representation. Technically partially redundant with the other fields.
2168 /// Internally, we ensure that the duplicated information remains consistent.
2169 pub(crate) ast: ast::PolicySet,
2170 /// Policies in the set (this includes both static policies and template linked-policies)
2171 policies: HashMap<PolicyId, Policy>,
2172 /// Templates in the set
2173 templates: HashMap<PolicyId, Template>,
2174}
2175
2176impl PartialEq for PolicySet {
2177 fn eq(&self, other: &Self) -> bool {
2178 // eq is based on just the `ast`
2179 self.ast.eq(&other.ast)
2180 }
2181}
2182impl Eq for PolicySet {}
2183
2184#[doc(hidden)] // because this converts to a private/internal type
2185impl AsRef<ast::PolicySet> for PolicySet {
2186 fn as_ref(&self) -> &ast::PolicySet {
2187 &self.ast
2188 }
2189}
2190
2191impl FromStr for PolicySet {
2192 type Err = ParseErrors;
2193
2194 /// Create a policy set from multiple statements.
2195 ///
2196 /// Policy ids will default to "policy*" with numbers from 0.
2197 /// If you load more policies, do not use the default id, or there will be conflicts.
2198 ///
2199 /// See [`Policy`] for more.
2200 fn from_str(policies: &str) -> Result<Self, Self::Err> {
2201 let (texts, pset) = parser::parse_policyset_and_also_return_policy_text(policies)?;
2202 // PANIC SAFETY: By the invariant on `parse_policyset_and_also_return_policy_text(policies)`, every `PolicyId` in `pset.policies()` occurs as a key in `text`.
2203 #[allow(clippy::expect_used)]
2204 let policies = pset.policies().map(|p|
2205 (
2206 PolicyId::new(p.id().clone()),
2207 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() }
2208 )
2209 ).collect();
2210 // PANIC SAFETY: By the same invariant, every `PolicyId` in `pset.templates()` also occurs as a key in `text`.
2211 #[allow(clippy::expect_used)]
2212 let templates = pset.templates().map(|t|
2213 (
2214 PolicyId::new(t.id().clone()),
2215 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() }
2216 )
2217 ).collect();
2218 Ok(Self {
2219 ast: pset,
2220 policies,
2221 templates,
2222 })
2223 }
2224}
2225
2226impl PolicySet {
2227 /// Build the policy set AST from the EST
2228 fn from_est(est: &est::PolicySet) -> Result<Self, PolicySetError> {
2229 let ast: ast::PolicySet = est.clone().try_into()?;
2230 // PANIC SAFETY: Since conversion from EST to AST succeeded, every `PolicyId` in `ast.policies()` occurs in `est`
2231 #[allow(clippy::expect_used)]
2232 let policies = ast
2233 .policies()
2234 .map(|p| {
2235 (
2236 PolicyId::new(p.id().clone()),
2237 Policy {
2238 lossless: LosslessPolicy::Est(est.get_policy(p.id()).expect(
2239 "internal invariant violation: policy id exists in asts but not ests",
2240 )),
2241 ast: p.clone(),
2242 },
2243 )
2244 })
2245 .collect();
2246 // PANIC SAFETY: Since conversion from EST to AST succeeded, every `PolicyId` in `ast.templates()` occurs in `est`
2247 #[allow(clippy::expect_used)]
2248 let templates = ast
2249 .templates()
2250 .map(|t| {
2251 (
2252 PolicyId::new(t.id().clone()),
2253 Template {
2254 lossless: LosslessPolicy::Est(est.get_template(t.id()).expect(
2255 "internal invariant violation: template id exists in asts but not ests",
2256 )),
2257 ast: t.clone(),
2258 },
2259 )
2260 })
2261 .collect();
2262 Ok(Self {
2263 ast,
2264 policies,
2265 templates,
2266 })
2267 }
2268
2269 /// Build the [`PolicySet`] from just the AST information
2270 #[cfg_attr(not(feature = "protobufs"), allow(dead_code))]
2271 pub(crate) fn from_ast(ast: ast::PolicySet) -> Result<Self, PolicySetError> {
2272 Self::from_policies(ast.into_policies().map(Policy::from_ast))
2273 }
2274
2275 /// Deserialize the [`PolicySet`] from a JSON string
2276 pub fn from_json_str(src: impl AsRef<str>) -> Result<Self, PolicySetError> {
2277 let est: est::PolicySet = serde_json::from_str(src.as_ref())
2278 .map_err(|e| policy_set_errors::JsonPolicySetError { inner: e })?;
2279 Self::from_est(&est)
2280 }
2281
2282 /// Deserialize the [`PolicySet`] from a JSON value
2283 pub fn from_json_value(src: serde_json::Value) -> Result<Self, PolicySetError> {
2284 let est: est::PolicySet = serde_json::from_value(src)
2285 .map_err(|e| policy_set_errors::JsonPolicySetError { inner: e })?;
2286 Self::from_est(&est)
2287 }
2288
2289 /// Deserialize the [`PolicySet`] from a JSON reader
2290 pub fn from_json_file(r: impl std::io::Read) -> Result<Self, PolicySetError> {
2291 let est: est::PolicySet = serde_json::from_reader(r)
2292 .map_err(|e| policy_set_errors::JsonPolicySetError { inner: e })?;
2293 Self::from_est(&est)
2294 }
2295
2296 /// Serialize the [`PolicySet`] as a JSON value
2297 pub fn to_json(self) -> Result<serde_json::Value, PolicySetError> {
2298 let est = self.est()?;
2299 let value = serde_json::to_value(est)
2300 .map_err(|e| policy_set_errors::JsonPolicySetError { inner: e })?;
2301 Ok(value)
2302 }
2303
2304 /// Get the EST representation of the [`PolicySet`]
2305 fn est(self) -> Result<est::PolicySet, PolicyToJsonError> {
2306 let (static_policies, template_links): (Vec<_>, Vec<_>) =
2307 fold_partition(self.policies, is_static_or_link)?;
2308 let static_policies = static_policies.into_iter().collect::<HashMap<_, _>>();
2309 let templates = self
2310 .templates
2311 .into_iter()
2312 .map(|(id, template)| template.lossless.est().map(|est| (id.into(), est)))
2313 .collect::<Result<HashMap<_, _>, _>>()?;
2314 let est = est::PolicySet {
2315 templates,
2316 static_policies,
2317 template_links,
2318 };
2319
2320 Ok(est)
2321 }
2322
2323 /// Get the human-readable Cedar syntax representation of this policy set.
2324 /// This function is primarily intended for rendering JSON policies in the
2325 /// human-readable syntax, but it will also return the original policy text
2326 /// (though possibly re-ordering policies within the policy set) when the
2327 /// policy-set contains policies parsed from the human-readable syntax.
2328 ///
2329 /// This will return `None` if there are any linked policies in the policy
2330 /// set because they cannot be directly rendered in Cedar syntax. It also
2331 /// cannot record policy ids because these cannot be specified in the Cedar
2332 /// syntax. The policies may be reordered, so parsing the resulting string
2333 /// with [`PolicySet::from_str`] is likely to yield different policy id
2334 /// assignments. For these reasons you should prefer serializing as JSON (or protobuf) and
2335 /// only using this function to obtain a representation to display to human
2336 /// users.
2337 ///
2338 /// This function does not format the policy according to any particular
2339 /// rules. Policy formatting can be done through the Cedar policy CLI or
2340 /// the `cedar-policy-formatter` crate.
2341 pub fn to_cedar(&self) -> Option<String> {
2342 let policies = self
2343 .policies
2344 .values()
2345 // We'd like to print policies in a deterministic order, so we sort
2346 // before printing, hoping that the size of policy sets is fairly
2347 // small.
2348 .sorted_by_key(|p| AsRef::<str>::as_ref(p.id()))
2349 .map(Policy::to_cedar)
2350 .collect::<Option<Vec<_>>>()?;
2351 let templates = self
2352 .templates
2353 .values()
2354 .sorted_by_key(|t| AsRef::<str>::as_ref(t.id()))
2355 .map(Template::to_cedar);
2356
2357 Some(policies.into_iter().chain(templates).join("\n\n"))
2358 }
2359
2360 /// Create a fresh empty `PolicySet`
2361 pub fn new() -> Self {
2362 Self {
2363 ast: ast::PolicySet::new(),
2364 policies: HashMap::new(),
2365 templates: HashMap::new(),
2366 }
2367 }
2368
2369 /// Create a `PolicySet` from the given policies
2370 pub fn from_policies(
2371 policies: impl IntoIterator<Item = Policy>,
2372 ) -> Result<Self, PolicySetError> {
2373 let mut set = Self::new();
2374 for policy in policies {
2375 set.add(policy)?;
2376 }
2377 Ok(set)
2378 }
2379
2380 /// Helper function for `merge_policyset`
2381 /// Merges two sets and avoids name clashes by using the provided
2382 /// renaming. The type parameter `T` allows this code to be used for
2383 /// both Templates and Policies.
2384 fn merge_sets<T>(
2385 this: &mut HashMap<PolicyId, T>,
2386 other: &HashMap<PolicyId, T>,
2387 renaming: &HashMap<PolicyId, PolicyId>,
2388 ) where
2389 T: PartialEq + Clone,
2390 {
2391 for (pid, ot) in other {
2392 match renaming.get(pid) {
2393 Some(new_pid) => {
2394 this.insert(new_pid.clone(), ot.clone());
2395 }
2396 None => {
2397 if this.get(pid).is_none() {
2398 this.insert(pid.clone(), ot.clone());
2399 }
2400 // If pid is not in the renaming but is in both
2401 // this and other, then by assumption
2402 // the element at pid in this and other are equal
2403 // i.e., the renaming is expected to track all
2404 // conflicting pids.
2405 }
2406 }
2407 }
2408 }
2409
2410 /// Merges this `PolicySet` with another `PolicySet`.
2411 /// This `PolicySet` is modified while the other `PolicySet`
2412 /// remains unchanged.
2413 ///
2414 /// The flag `rename_duplicates` controls the expected behavior
2415 /// when a `PolicyId` in this and the other `PolicySet` conflict.
2416 ///
2417 /// When `rename_duplicates` is false, conflicting `PolicyId`s result
2418 /// in a `PolicySetError::AlreadyDefined` error.
2419 ///
2420 /// Otherwise, when `rename_duplicates` is true, conflicting `PolicyId`s from
2421 /// the other `PolicySet` are automatically renamed to avoid conflict.
2422 /// This renaming is returned as a Hashmap from the old `PolicyId` to the
2423 /// renamed `PolicyId`.
2424 pub fn merge(
2425 &mut self,
2426 other: &Self,
2427 rename_duplicates: bool,
2428 ) -> Result<HashMap<PolicyId, PolicyId>, PolicySetError> {
2429 match self.ast.merge_policyset(&other.ast, rename_duplicates) {
2430 Ok(renaming) => {
2431 let renaming: HashMap<PolicyId, PolicyId> = renaming
2432 .into_iter()
2433 .map(|(old_pid, new_pid)| (PolicyId::new(old_pid), PolicyId::new(new_pid)))
2434 .collect();
2435 Self::merge_sets(&mut self.templates, &other.templates, &renaming);
2436 Self::merge_sets(&mut self.policies, &other.policies, &renaming);
2437 Ok(renaming)
2438 }
2439 Err(ast::PolicySetError::Occupied { id }) => Err(PolicySetError::AlreadyDefined(
2440 policy_set_errors::AlreadyDefined {
2441 id: PolicyId::new(id),
2442 },
2443 )),
2444 }
2445 }
2446
2447 /// Add an static policy to the `PolicySet`. To add a template instance, use
2448 /// `link` instead. This function will return an error (and not modify
2449 /// the `PolicySet`) if a template-linked policy is passed in.
2450 pub fn add(&mut self, policy: Policy) -> Result<(), PolicySetError> {
2451 if policy.is_static() {
2452 let id = PolicyId::new(policy.ast.id().clone());
2453 self.ast.add(policy.ast.clone())?;
2454 self.policies.insert(id, policy);
2455 Ok(())
2456 } else {
2457 Err(PolicySetError::ExpectedStatic(
2458 policy_set_errors::ExpectedStatic::new(),
2459 ))
2460 }
2461 }
2462
2463 /// Remove a static `Policy` from the `PolicySet`.
2464 ///
2465 /// This will error if the policy is not a static policy.
2466 pub fn remove_static(&mut self, policy_id: PolicyId) -> Result<Policy, PolicySetError> {
2467 let Some(policy) = self.policies.remove(&policy_id) else {
2468 return Err(PolicySetError::PolicyNonexistent(
2469 policy_set_errors::PolicyNonexistentError { policy_id },
2470 ));
2471 };
2472 if self
2473 .ast
2474 .remove_static(&ast::PolicyID::from_string(&policy_id))
2475 .is_ok()
2476 {
2477 Ok(policy)
2478 } else {
2479 //Restore self.policies
2480 self.policies.insert(policy_id.clone(), policy);
2481 Err(PolicySetError::PolicyNonexistent(
2482 policy_set_errors::PolicyNonexistentError { policy_id },
2483 ))
2484 }
2485 }
2486
2487 /// Add a `Template` to the `PolicySet`
2488 pub fn add_template(&mut self, template: Template) -> Result<(), PolicySetError> {
2489 let id = PolicyId::new(template.ast.id().clone());
2490 self.ast.add_template(template.ast.clone())?;
2491 self.templates.insert(id, template);
2492 Ok(())
2493 }
2494
2495 /// Remove a `Template` from the `PolicySet`.
2496 ///
2497 /// This will error if any policy is linked to the template.
2498 /// This will error if `policy_id` is not a template.
2499 pub fn remove_template(&mut self, template_id: PolicyId) -> Result<Template, PolicySetError> {
2500 let Some(template) = self.templates.remove(&template_id) else {
2501 return Err(PolicySetError::TemplateNonexistent(
2502 policy_set_errors::TemplateNonexistentError { template_id },
2503 ));
2504 };
2505 // If self.templates and self.ast disagree, authorization cannot be trusted.
2506 // PANIC SAFETY: We just found the policy in self.templates.
2507 #[allow(clippy::panic)]
2508 match self
2509 .ast
2510 .remove_template(&ast::PolicyID::from_string(&template_id))
2511 {
2512 Ok(_) => Ok(template),
2513 Err(ast::PolicySetTemplateRemovalError::RemoveTemplateWithLinksError(_)) => {
2514 self.templates.insert(template_id.clone(), template);
2515 Err(PolicySetError::RemoveTemplateWithActiveLinks(
2516 policy_set_errors::RemoveTemplateWithActiveLinksError { template_id },
2517 ))
2518 }
2519 Err(ast::PolicySetTemplateRemovalError::NotTemplateError(_)) => {
2520 self.templates.insert(template_id.clone(), template);
2521 Err(PolicySetError::RemoveTemplateNotTemplate(
2522 policy_set_errors::RemoveTemplateNotTemplateError { template_id },
2523 ))
2524 }
2525 Err(ast::PolicySetTemplateRemovalError::RemovePolicyNoTemplateError(_)) => {
2526 panic!("Found template policy in self.templates but not in self.ast");
2527 }
2528 }
2529 }
2530
2531 /// Get policies linked to a `Template` in the `PolicySet`.
2532 /// If any policy is linked to the template, this will error
2533 pub fn get_linked_policies(
2534 &self,
2535 template_id: PolicyId,
2536 ) -> Result<impl Iterator<Item = &PolicyId>, PolicySetError> {
2537 self.ast
2538 .get_linked_policies(&ast::PolicyID::from_string(&template_id))
2539 .map_or_else(
2540 |_| {
2541 Err(PolicySetError::TemplateNonexistent(
2542 policy_set_errors::TemplateNonexistentError { template_id },
2543 ))
2544 },
2545 |v| Ok(v.map(PolicyId::ref_cast)),
2546 )
2547 }
2548
2549 /// Iterate over all the `Policy`s in the `PolicySet`.
2550 ///
2551 /// This will include both static and template-linked policies.
2552 pub fn policies(&self) -> impl Iterator<Item = &Policy> {
2553 self.policies.values()
2554 }
2555
2556 /// Iterate over the `Template`'s in the `PolicySet`.
2557 pub fn templates(&self) -> impl Iterator<Item = &Template> {
2558 self.templates.values()
2559 }
2560
2561 /// Get a `Template` by its `PolicyId`
2562 pub fn template(&self, id: &PolicyId) -> Option<&Template> {
2563 self.templates.get(id)
2564 }
2565
2566 /// Get a `Policy` by its `PolicyId`
2567 pub fn policy(&self, id: &PolicyId) -> Option<&Policy> {
2568 self.policies.get(id)
2569 }
2570
2571 /// Extract annotation data from a `Policy` by its `PolicyId` and annotation key.
2572 /// If the annotation is present without an explicit value (e.g., `@annotation`),
2573 /// then this function returns `Some("")`. It returns `None` only when the
2574 /// annotation is not present.
2575 pub fn annotation(&self, id: &PolicyId, key: impl AsRef<str>) -> Option<&str> {
2576 self.ast
2577 .get(id.as_ref())?
2578 .annotation(&key.as_ref().parse().ok()?)
2579 .map(AsRef::as_ref)
2580 }
2581
2582 /// Extract annotation data from a `Template` by its `PolicyId` and annotation key.
2583 /// If the annotation is present without an explicit value (e.g., `@annotation`),
2584 /// then this function returns `Some("")`. It returns `None` only when the
2585 /// annotation is not present.
2586 pub fn template_annotation(&self, id: &PolicyId, key: impl AsRef<str>) -> Option<&str> {
2587 self.ast
2588 .get_template(id.as_ref())?
2589 .annotation(&key.as_ref().parse().ok()?)
2590 .map(AsRef::as_ref)
2591 }
2592
2593 /// Returns true iff the `PolicySet` is empty
2594 pub fn is_empty(&self) -> bool {
2595 debug_assert_eq!(
2596 self.ast.is_empty(),
2597 self.policies.is_empty() && self.templates.is_empty()
2598 );
2599 self.ast.is_empty()
2600 }
2601
2602 /// Returns the number of `Policy`s in the `PolicySet`.
2603 ///
2604 /// This will include both static and template-linked policies.
2605 pub fn num_of_policies(&self) -> usize {
2606 self.policies.len()
2607 }
2608
2609 /// Returns the number of `Template`s in the `PolicySet`.
2610 pub fn num_of_templates(&self) -> usize {
2611 self.templates.len()
2612 }
2613
2614 /// Attempt to link a template and add the new template-linked policy to the policy set.
2615 /// If link fails, the `PolicySet` is not modified.
2616 /// Failure can happen for three reasons
2617 /// 1) The map passed in `vals` may not match the slots in the template
2618 /// 2) The `new_id` may conflict w/ a policy that already exists in the set
2619 /// 3) `template_id` does not correspond to a template. Either the id is
2620 /// not in the policy set, or it is in the policy set but is either a
2621 /// linked or static policy rather than a template
2622 #[allow(clippy::needless_pass_by_value)]
2623 pub fn link(
2624 &mut self,
2625 template_id: PolicyId,
2626 new_id: PolicyId,
2627 vals: HashMap<SlotId, EntityUid>,
2628 ) -> Result<(), PolicySetError> {
2629 let unwrapped_vals: HashMap<ast::SlotId, ast::EntityUID> = vals
2630 .into_iter()
2631 .map(|(key, value)| (key.into(), value.into()))
2632 .collect();
2633
2634 // Try to get the template with the id we're linking from. We do this
2635 // _before_ calling `self.ast.link` because `link` mutates the policy
2636 // set by creating a new link entry in a hashmap. This happens even when
2637 // trying to link a static policy, which we want to error on here.
2638 let Some(template) = self.templates.get(&template_id) else {
2639 return Err(if self.policies.contains_key(&template_id) {
2640 policy_set_errors::ExpectedTemplate::new().into()
2641 } else {
2642 policy_set_errors::LinkingError {
2643 inner: ast::LinkingError::NoSuchTemplate {
2644 id: template_id.into(),
2645 },
2646 }
2647 .into()
2648 });
2649 };
2650
2651 let linked_ast = self.ast.link(
2652 template_id.into(),
2653 new_id.clone().into(),
2654 unwrapped_vals.clone(),
2655 )?;
2656
2657 // PANIC SAFETY: `lossless.link()` will not fail after `ast.link()` succeeds
2658 #[allow(clippy::expect_used)]
2659 let linked_lossless = template
2660 .lossless
2661 .clone()
2662 .link(unwrapped_vals.iter().map(|(k, v)| (*k, v)))
2663 // The only error case for `lossless.link()` is a template with
2664 // slots which are not filled by the provided values. `ast.link()`
2665 // will have already errored if there are any unfilled slots in the
2666 // template.
2667 .expect("ast.link() didn't fail above, so this shouldn't fail");
2668 self.policies.insert(
2669 new_id,
2670 Policy {
2671 ast: linked_ast.clone(),
2672 lossless: linked_lossless,
2673 },
2674 );
2675 Ok(())
2676 }
2677
2678 /// Get all the unknown entities from the policy set
2679 #[doc = include_str!("../experimental_warning.md")]
2680 #[cfg(feature = "partial-eval")]
2681 pub fn unknown_entities(&self) -> HashSet<EntityUid> {
2682 let mut entity_uids = HashSet::new();
2683 for policy in self.policies.values() {
2684 entity_uids.extend(policy.unknown_entities());
2685 }
2686 entity_uids
2687 }
2688
2689 /// Unlink a template-linked policy from the policy set.
2690 /// Returns the policy that was unlinked.
2691 pub fn unlink(&mut self, policy_id: PolicyId) -> Result<Policy, PolicySetError> {
2692 let Some(policy) = self.policies.remove(&policy_id) else {
2693 return Err(PolicySetError::LinkNonexistent(
2694 policy_set_errors::LinkNonexistentError { policy_id },
2695 ));
2696 };
2697 // If self.policies and self.ast disagree, authorization cannot be trusted.
2698 // PANIC SAFETY: We just found the policy in self.policies.
2699 #[allow(clippy::panic)]
2700 match self.ast.unlink(&ast::PolicyID::from_string(&policy_id)) {
2701 Ok(_) => Ok(policy),
2702 Err(ast::PolicySetUnlinkError::NotLinkError(_)) => {
2703 //Restore self.policies
2704 self.policies.insert(policy_id.clone(), policy);
2705 Err(PolicySetError::UnlinkLinkNotLink(
2706 policy_set_errors::UnlinkLinkNotLinkError { policy_id },
2707 ))
2708 }
2709 Err(ast::PolicySetUnlinkError::UnlinkingError(_)) => {
2710 panic!("Found linked policy in self.policies but not in self.ast")
2711 }
2712 }
2713 }
2714}
2715
2716impl std::fmt::Display for PolicySet {
2717 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2718 // prefer to display the lossless format
2719 write!(f, "{}", self.policies().map(|p| &p.lossless).join("\n"))
2720 }
2721}
2722
2723/// Given a [`PolicyId`] and a [`Policy`], determine if the policy represents a static policy or a
2724/// link
2725fn is_static_or_link(
2726 (id, policy): (PolicyId, Policy),
2727) -> Result<Either<(ast::PolicyID, est::Policy), TemplateLink>, PolicyToJsonError> {
2728 match policy.template_id() {
2729 Some(template_id) => {
2730 let values = policy
2731 .ast
2732 .env()
2733 .iter()
2734 .map(|(id, euid)| (*id, euid.clone()))
2735 .collect();
2736 Ok(Either::Right(TemplateLink {
2737 new_id: id.into(),
2738 template_id: template_id.clone().into(),
2739 values,
2740 }))
2741 }
2742 None => policy
2743 .lossless
2744 .est()
2745 .map(|est| Either::Left((id.into(), est))),
2746 }
2747}
2748
2749/// Like [`itertools::Itertools::partition_map`], but accepts a function that can fail.
2750/// The first invocation of `f` that fails causes the whole computation to fail
2751#[allow(clippy::redundant_pub_crate)] // can't be private because it's used in tests
2752pub(crate) fn fold_partition<T, A, B, E>(
2753 i: impl IntoIterator<Item = T>,
2754 f: impl Fn(T) -> Result<Either<A, B>, E>,
2755) -> Result<(Vec<A>, Vec<B>), E> {
2756 let mut lefts = vec![];
2757 let mut rights = vec![];
2758
2759 for item in i {
2760 match f(item)? {
2761 Either::Left(left) => lefts.push(left),
2762 Either::Right(right) => rights.push(right),
2763 }
2764 }
2765
2766 Ok((lefts, rights))
2767}
2768
2769/// The "type" of a [`Request`], i.e., the [`EntityTypeName`]s of principal
2770/// and resource, and the [`EntityUid`] of action
2771#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
2772pub struct RequestEnv {
2773 pub(crate) principal: EntityTypeName,
2774 pub(crate) action: EntityUid,
2775 pub(crate) resource: EntityTypeName,
2776}
2777
2778impl RequestEnv {
2779 /// Construct a [`RequestEnv`]
2780 pub fn new(principal: EntityTypeName, action: EntityUid, resource: EntityTypeName) -> Self {
2781 Self {
2782 principal,
2783 action,
2784 resource,
2785 }
2786 }
2787 /// Get the principal type name
2788 pub fn principal(&self) -> &EntityTypeName {
2789 &self.principal
2790 }
2791
2792 /// Get the action [`EntityUid`]
2793 pub fn action(&self) -> &EntityUid {
2794 &self.action
2795 }
2796
2797 /// Get the resource type name
2798 pub fn resource(&self) -> &EntityTypeName {
2799 &self.resource
2800 }
2801}
2802
2803#[doc(hidden)]
2804impl From<cedar_policy_validator::types::RequestEnv<'_>> for RequestEnv {
2805 fn from(renv: cedar_policy_validator::types::RequestEnv<'_>) -> Self {
2806 match renv {
2807 cedar_policy_validator::types::RequestEnv::DeclaredAction {
2808 principal,
2809 action,
2810 resource,
2811 ..
2812 } => Self {
2813 principal: principal.clone().into(),
2814 action: action.clone().into(),
2815 resource: resource.clone().into(),
2816 },
2817 // PANIC SAFETY: partial validation is not enabled and hence `RequestEnv::UndeclaredAction` should not show up
2818 #[allow(clippy::unreachable)]
2819 cedar_policy_validator::types::RequestEnv::UndeclaredAction => {
2820 unreachable!("used unsupported feature")
2821 }
2822 }
2823 }
2824}
2825
2826/// Get valid request envs for an `ast::Template`
2827///
2828/// This function is called by [`Template::get_valid_request_envs`] and
2829/// [`Policy::get_valid_request_envs`]
2830fn get_valid_request_envs(ast: &ast::Template, s: &Schema) -> impl Iterator<Item = RequestEnv> {
2831 let tc = Typechecker::new(&s.0, cedar_policy_validator::ValidationMode::default());
2832 tc.typecheck_by_request_env(ast)
2833 .into_iter()
2834 .filter_map(|(env, pc)| {
2835 if matches!(pc, PolicyCheck::Success(_)) {
2836 Some(env.into())
2837 } else {
2838 None
2839 }
2840 })
2841 .collect::<BTreeSet<_>>()
2842 .into_iter()
2843}
2844
2845/// Policy template datatype
2846//
2847// NOTE: Unlike the internal type [`ast::Template`], this type only supports
2848// templates. The `Template` constructors will return an error if provided with
2849// a static policy.
2850#[derive(Debug, Clone)]
2851pub struct Template {
2852 /// AST representation of the template, used for most operations.
2853 /// In particular, the `ast` contains the authoritative `PolicyId` for the template.
2854 pub(crate) ast: ast::Template,
2855
2856 /// Some "lossless" representation of the template, whichever is most
2857 /// convenient to provide (and can be provided with the least overhead).
2858 /// This is used just for `to_json()`.
2859 /// We can't just derive this on-demand from `ast`, because the AST is lossy:
2860 /// we can't reconstruct an accurate CST/EST/policy-text from the AST, but
2861 /// we can from the EST (modulo whitespace and a few other things like the
2862 /// order of annotations).
2863 ///
2864 /// This is a `LosslessPolicy` (rather than something like `LosslessTemplate`)
2865 /// because the EST doesn't distinguish between static policies and templates.
2866 pub(crate) lossless: LosslessPolicy,
2867}
2868
2869impl PartialEq for Template {
2870 fn eq(&self, other: &Self) -> bool {
2871 // eq is based on just the `ast`
2872 self.ast.eq(&other.ast)
2873 }
2874}
2875impl Eq for Template {}
2876
2877#[doc(hidden)] // because this converts to a private/internal type
2878impl AsRef<ast::Template> for Template {
2879 fn as_ref(&self) -> &ast::Template {
2880 &self.ast
2881 }
2882}
2883
2884impl Template {
2885 /// Attempt to parse a [`Template`] from source.
2886 /// Returns an error if the input is a static policy (i.e., has no slots).
2887 /// If `id` is Some, then the resulting template will have that `id`.
2888 /// If the `id` is None, the parser will use the default "policy0".
2889 /// The behavior around None may change in the future.
2890 pub fn parse(id: Option<PolicyId>, src: impl AsRef<str>) -> Result<Self, ParseErrors> {
2891 let ast = parser::parse_template(id.map(Into::into), src.as_ref())?;
2892 Ok(Self {
2893 ast,
2894 lossless: LosslessPolicy::policy_or_template_text(src.as_ref()),
2895 })
2896 }
2897
2898 /// Get the `PolicyId` of this `Template`
2899 pub fn id(&self) -> &PolicyId {
2900 PolicyId::ref_cast(self.ast.id())
2901 }
2902
2903 /// Clone this `Template` with a new `PolicyId`
2904 #[must_use]
2905 pub fn new_id(&self, id: PolicyId) -> Self {
2906 Self {
2907 ast: self.ast.new_id(id.into()),
2908 lossless: self.lossless.clone(), // Lossless representation doesn't include the `PolicyId`
2909 }
2910 }
2911
2912 /// Get the `Effect` (`Forbid` or `Permit`) of this `Template`
2913 pub fn effect(&self) -> Effect {
2914 self.ast.effect()
2915 }
2916
2917 /// Get an annotation value of this `Template`.
2918 /// If the annotation is present without an explicit value (e.g., `@annotation`),
2919 /// then this function returns `Some("")`. Returns `None` when the
2920 /// annotation is not present or when `key` is not a valid annotation identifier.
2921 pub fn annotation(&self, key: impl AsRef<str>) -> Option<&str> {
2922 self.ast
2923 .annotation(&key.as_ref().parse().ok()?)
2924 .map(AsRef::as_ref)
2925 }
2926
2927 /// Iterate through annotation data of this `Template` as key-value pairs.
2928 /// Annotations which do not have an explicit value (e.g., `@annotation`),
2929 /// are included in the iterator with the value `""`.
2930 pub fn annotations(&self) -> impl Iterator<Item = (&str, &str)> {
2931 self.ast
2932 .annotations()
2933 .map(|(k, v)| (k.as_ref(), v.as_ref()))
2934 }
2935
2936 /// Iterate over the open slots in this `Template`
2937 pub fn slots(&self) -> impl Iterator<Item = &SlotId> {
2938 self.ast.slots().map(|slot| SlotId::ref_cast(&slot.id))
2939 }
2940
2941 /// Get the scope constraint on this policy's principal
2942 pub fn principal_constraint(&self) -> TemplatePrincipalConstraint {
2943 match self.ast.principal_constraint().as_inner() {
2944 ast::PrincipalOrResourceConstraint::Any => TemplatePrincipalConstraint::Any,
2945 ast::PrincipalOrResourceConstraint::In(eref) => {
2946 TemplatePrincipalConstraint::In(match eref {
2947 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
2948 ast::EntityReference::Slot(_) => None,
2949 })
2950 }
2951 ast::PrincipalOrResourceConstraint::Eq(eref) => {
2952 TemplatePrincipalConstraint::Eq(match eref {
2953 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
2954 ast::EntityReference::Slot(_) => None,
2955 })
2956 }
2957 ast::PrincipalOrResourceConstraint::Is(entity_type) => {
2958 TemplatePrincipalConstraint::Is(entity_type.as_ref().clone().into())
2959 }
2960 ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
2961 TemplatePrincipalConstraint::IsIn(
2962 entity_type.as_ref().clone().into(),
2963 match eref {
2964 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
2965 ast::EntityReference::Slot(_) => None,
2966 },
2967 )
2968 }
2969 }
2970 }
2971
2972 /// Get the scope constraint on this policy's action
2973 pub fn action_constraint(&self) -> ActionConstraint {
2974 // Clone the data from Core to be consistent with the other constraints
2975 match self.ast.action_constraint() {
2976 ast::ActionConstraint::Any => ActionConstraint::Any,
2977 ast::ActionConstraint::In(ids) => {
2978 ActionConstraint::In(ids.iter().map(|id| id.as_ref().clone().into()).collect())
2979 }
2980 ast::ActionConstraint::Eq(id) => ActionConstraint::Eq(id.as_ref().clone().into()),
2981 #[cfg(feature = "tolerant-ast")]
2982 ast::ActionConstraint::ErrorConstraint => {
2983 // We will only have an ErrorConstraint if we are using a parser that allows Error nodes
2984 // It is not recommended to evaluate an AST that allows error nodes
2985 // If somehow someone tries to evaluate an AST that includes an Action constraint error, we will
2986 // treat it as `Any`
2987 ActionConstraint::Any
2988 }
2989 }
2990 }
2991
2992 /// Get the scope constraint on this policy's resource
2993 pub fn resource_constraint(&self) -> TemplateResourceConstraint {
2994 match self.ast.resource_constraint().as_inner() {
2995 ast::PrincipalOrResourceConstraint::Any => TemplateResourceConstraint::Any,
2996 ast::PrincipalOrResourceConstraint::In(eref) => {
2997 TemplateResourceConstraint::In(match eref {
2998 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
2999 ast::EntityReference::Slot(_) => None,
3000 })
3001 }
3002 ast::PrincipalOrResourceConstraint::Eq(eref) => {
3003 TemplateResourceConstraint::Eq(match eref {
3004 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3005 ast::EntityReference::Slot(_) => None,
3006 })
3007 }
3008 ast::PrincipalOrResourceConstraint::Is(entity_type) => {
3009 TemplateResourceConstraint::Is(entity_type.as_ref().clone().into())
3010 }
3011 ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
3012 TemplateResourceConstraint::IsIn(
3013 entity_type.as_ref().clone().into(),
3014 match eref {
3015 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3016 ast::EntityReference::Slot(_) => None,
3017 },
3018 )
3019 }
3020 }
3021 }
3022
3023 /// Create a [`Template`] from its JSON representation.
3024 /// Returns an error if the input is a static policy (i.e., has no slots).
3025 /// If `id` is Some, the policy will be given that Policy Id.
3026 /// If `id` is None, then "JSON policy" will be used.
3027 /// The behavior around None may change in the future.
3028 pub fn from_json(
3029 id: Option<PolicyId>,
3030 json: serde_json::Value,
3031 ) -> Result<Self, PolicyFromJsonError> {
3032 let est: est::Policy = serde_json::from_value(json)
3033 .map_err(|e| entities_json_errors::JsonDeserializationError::Serde(e.into()))
3034 .map_err(cedar_policy_core::est::FromJsonError::from)?;
3035 Self::from_est(id, est)
3036 }
3037
3038 fn from_est(id: Option<PolicyId>, est: est::Policy) -> Result<Self, PolicyFromJsonError> {
3039 Ok(Self {
3040 ast: est.clone().try_into_ast_template(id.map(PolicyId::into))?,
3041 lossless: LosslessPolicy::Est(est),
3042 })
3043 }
3044
3045 #[cfg_attr(not(feature = "protobufs"), allow(dead_code))]
3046 pub(crate) fn from_ast(ast: ast::Template) -> Self {
3047 Self {
3048 lossless: LosslessPolicy::Est(ast.clone().into()),
3049 ast,
3050 }
3051 }
3052
3053 /// Get the JSON representation of this `Template`.
3054 pub fn to_json(&self) -> Result<serde_json::Value, PolicyToJsonError> {
3055 let est = self.lossless.est()?;
3056 serde_json::to_value(est).map_err(Into::into)
3057 }
3058
3059 /// Get the human-readable Cedar syntax representation of this template.
3060 /// This function is primarily intended for rendering JSON policies in the
3061 /// human-readable syntax, but it will also return the original policy text
3062 /// when given a policy parsed from the human-readable syntax.
3063 ///
3064 /// It also does not format the policy according to any particular rules.
3065 /// Policy formatting can be done through the Cedar policy CLI or
3066 /// the `cedar-policy-formatter` crate.
3067 pub fn to_cedar(&self) -> String {
3068 match &self.lossless {
3069 LosslessPolicy::Est(_) => self.ast.to_string(),
3070 LosslessPolicy::Text { text, .. } => text.clone(),
3071 }
3072 }
3073
3074 /// Get the valid [`RequestEnv`]s for this template, according to the schema.
3075 ///
3076 /// That is, all the [`RequestEnv`]s in the schema for which this template is
3077 /// not trivially false.
3078 pub fn get_valid_request_envs(&self, s: &Schema) -> impl Iterator<Item = RequestEnv> {
3079 get_valid_request_envs(&self.ast, s)
3080 }
3081}
3082
3083impl std::fmt::Display for Template {
3084 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3085 // prefer to display the lossless format
3086 self.lossless.fmt(f)
3087 }
3088}
3089
3090impl FromStr for Template {
3091 type Err = ParseErrors;
3092
3093 fn from_str(src: &str) -> Result<Self, Self::Err> {
3094 Self::parse(None, src)
3095 }
3096}
3097
3098/// Scope constraint on policy principals.
3099#[derive(Debug, Clone, PartialEq, Eq)]
3100pub enum PrincipalConstraint {
3101 /// Un-constrained
3102 Any,
3103 /// Must be In the given [`EntityUid`]
3104 In(EntityUid),
3105 /// Must be equal to the given [`EntityUid`]
3106 Eq(EntityUid),
3107 /// Must be the given [`EntityTypeName`]
3108 Is(EntityTypeName),
3109 /// Must be the given [`EntityTypeName`], and `in` the [`EntityUid`]
3110 IsIn(EntityTypeName, EntityUid),
3111}
3112
3113/// Scope constraint on policy principals for templates.
3114#[derive(Debug, Clone, PartialEq, Eq)]
3115pub enum TemplatePrincipalConstraint {
3116 /// Un-constrained
3117 Any,
3118 /// Must be In the given [`EntityUid`].
3119 /// If [`None`], then it is a template slot.
3120 In(Option<EntityUid>),
3121 /// Must be equal to the given [`EntityUid`].
3122 /// If [`None`], then it is a template slot.
3123 Eq(Option<EntityUid>),
3124 /// Must be the given [`EntityTypeName`].
3125 Is(EntityTypeName),
3126 /// Must be the given [`EntityTypeName`], and `in` the [`EntityUid`].
3127 /// If the [`EntityUid`] is [`Option::None`], then it is a template slot.
3128 IsIn(EntityTypeName, Option<EntityUid>),
3129}
3130
3131impl TemplatePrincipalConstraint {
3132 /// Does this constraint contain a slot?
3133 pub fn has_slot(&self) -> bool {
3134 match self {
3135 Self::Any | Self::Is(_) => false,
3136 Self::In(o) | Self::Eq(o) | Self::IsIn(_, o) => o.is_none(),
3137 }
3138 }
3139}
3140
3141/// Scope constraint on policy actions.
3142#[derive(Debug, Clone, PartialEq, Eq)]
3143pub enum ActionConstraint {
3144 /// Un-constrained
3145 Any,
3146 /// Must be In the given [`EntityUid`]
3147 In(Vec<EntityUid>),
3148 /// Must be equal to the given [`EntityUid]`
3149 Eq(EntityUid),
3150}
3151
3152/// Scope constraint on policy resources.
3153#[derive(Debug, Clone, PartialEq, Eq)]
3154pub enum ResourceConstraint {
3155 /// Un-constrained
3156 Any,
3157 /// Must be In the given [`EntityUid`]
3158 In(EntityUid),
3159 /// Must be equal to the given [`EntityUid`]
3160 Eq(EntityUid),
3161 /// Must be the given [`EntityTypeName`]
3162 Is(EntityTypeName),
3163 /// Must be the given [`EntityTypeName`], and `in` the [`EntityUid`]
3164 IsIn(EntityTypeName, EntityUid),
3165}
3166
3167/// Scope constraint on policy resources for templates.
3168#[derive(Debug, Clone, PartialEq, Eq)]
3169pub enum TemplateResourceConstraint {
3170 /// Un-constrained
3171 Any,
3172 /// Must be In the given [`EntityUid`].
3173 /// If [`None`], then it is a template slot.
3174 In(Option<EntityUid>),
3175 /// Must be equal to the given [`EntityUid`].
3176 /// If [`None`], then it is a template slot.
3177 Eq(Option<EntityUid>),
3178 /// Must be the given [`EntityTypeName`].
3179 Is(EntityTypeName),
3180 /// Must be the given [`EntityTypeName`], and `in` the [`EntityUid`].
3181 /// If the [`EntityUid`] is [`Option::None`], then it is a template slot.
3182 IsIn(EntityTypeName, Option<EntityUid>),
3183}
3184
3185impl TemplateResourceConstraint {
3186 /// Does this constraint contain a slot?
3187 pub fn has_slot(&self) -> bool {
3188 match self {
3189 Self::Any | Self::Is(_) => false,
3190 Self::In(o) | Self::Eq(o) | Self::IsIn(_, o) => o.is_none(),
3191 }
3192 }
3193}
3194
3195/// Structure for a `Policy`. Includes both static policies and template-linked policies.
3196#[derive(Debug, Clone)]
3197pub struct Policy {
3198 /// AST representation of the policy, used for most operations.
3199 /// In particular, the `ast` contains the authoritative `PolicyId` for the policy.
3200 pub(crate) ast: ast::Policy,
3201 /// Some "lossless" representation of the policy, whichever is most
3202 /// convenient to provide (and can be provided with the least overhead).
3203 /// This is used just for `to_json()`.
3204 /// We can't just derive this on-demand from `ast`, because the AST is lossy:
3205 /// we can't reconstruct an accurate CST/EST/policy-text from the AST, but
3206 /// we can from the EST (modulo whitespace and a few other things like the
3207 /// order of annotations).
3208 pub(crate) lossless: LosslessPolicy,
3209}
3210
3211impl PartialEq for Policy {
3212 fn eq(&self, other: &Self) -> bool {
3213 // eq is based on just the `ast`
3214 self.ast.eq(&other.ast)
3215 }
3216}
3217impl Eq for Policy {}
3218
3219#[doc(hidden)] // because this converts to a private/internal type
3220impl AsRef<ast::Policy> for Policy {
3221 fn as_ref(&self) -> &ast::Policy {
3222 &self.ast
3223 }
3224}
3225
3226impl Policy {
3227 /// Get the `PolicyId` of the `Template` this is linked to.
3228 /// If this is a static policy, this will return `None`.
3229 pub fn template_id(&self) -> Option<&PolicyId> {
3230 if self.is_static() {
3231 None
3232 } else {
3233 Some(PolicyId::ref_cast(self.ast.template().id()))
3234 }
3235 }
3236
3237 /// Get the values this `Template` is linked to, expressed as a map from `SlotId` to `EntityUid`.
3238 /// If this is a static policy, this will return `None`.
3239 pub fn template_links(&self) -> Option<HashMap<SlotId, EntityUid>> {
3240 if self.is_static() {
3241 None
3242 } else {
3243 let wrapped_vals: HashMap<SlotId, EntityUid> = self
3244 .ast
3245 .env()
3246 .iter()
3247 .map(|(key, value)| ((*key).into(), value.clone().into()))
3248 .collect();
3249 Some(wrapped_vals)
3250 }
3251 }
3252
3253 /// Get the `Effect` (`Permit` or `Forbid`) for this instance
3254 pub fn effect(&self) -> Effect {
3255 self.ast.effect()
3256 }
3257
3258 /// Get an annotation value of this template-linked or static policy.
3259 /// If the annotation is present without an explicit value (e.g., `@annotation`),
3260 /// then this function returns `Some("")`. Returns `None` when the
3261 /// annotation is not present or when `key` is not a valid annotations identifier.
3262 pub fn annotation(&self, key: impl AsRef<str>) -> Option<&str> {
3263 self.ast
3264 .annotation(&key.as_ref().parse().ok()?)
3265 .map(AsRef::as_ref)
3266 }
3267
3268 /// Iterate through annotation data of this template-linked or static policy.
3269 /// Annotations which do not have an explicit value (e.g., `@annotation`),
3270 /// are included in the iterator with the value `""`.
3271 pub fn annotations(&self) -> impl Iterator<Item = (&str, &str)> {
3272 self.ast
3273 .annotations()
3274 .map(|(k, v)| (k.as_ref(), v.as_ref()))
3275 }
3276
3277 /// Get the `PolicyId` for this template-linked or static policy
3278 pub fn id(&self) -> &PolicyId {
3279 PolicyId::ref_cast(self.ast.id())
3280 }
3281
3282 /// Clone this `Policy` with a new `PolicyId`
3283 #[must_use]
3284 pub fn new_id(&self, id: PolicyId) -> Self {
3285 Self {
3286 ast: self.ast.new_id(id.into()),
3287 lossless: self.lossless.clone(), // Lossless representation doesn't include the `PolicyId`
3288 }
3289 }
3290
3291 /// Returns `true` if this is a static policy, `false` otherwise.
3292 pub fn is_static(&self) -> bool {
3293 self.ast.is_static()
3294 }
3295
3296 /// Get the scope constraint on this policy's principal
3297 pub fn principal_constraint(&self) -> PrincipalConstraint {
3298 let slot_id = ast::SlotId::principal();
3299 match self.ast.template().principal_constraint().as_inner() {
3300 ast::PrincipalOrResourceConstraint::Any => PrincipalConstraint::Any,
3301 ast::PrincipalOrResourceConstraint::In(eref) => {
3302 PrincipalConstraint::In(self.convert_entity_reference(eref, slot_id).clone())
3303 }
3304 ast::PrincipalOrResourceConstraint::Eq(eref) => {
3305 PrincipalConstraint::Eq(self.convert_entity_reference(eref, slot_id).clone())
3306 }
3307 ast::PrincipalOrResourceConstraint::Is(entity_type) => {
3308 PrincipalConstraint::Is(entity_type.as_ref().clone().into())
3309 }
3310 ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
3311 PrincipalConstraint::IsIn(
3312 entity_type.as_ref().clone().into(),
3313 self.convert_entity_reference(eref, slot_id).clone(),
3314 )
3315 }
3316 }
3317 }
3318
3319 /// Get the scope constraint on this policy's action
3320 pub fn action_constraint(&self) -> ActionConstraint {
3321 // Clone the data from Core to be consistant with the other constraints
3322 match self.ast.template().action_constraint() {
3323 ast::ActionConstraint::Any => ActionConstraint::Any,
3324 ast::ActionConstraint::In(ids) => ActionConstraint::In(
3325 ids.iter()
3326 .map(|euid| EntityUid::ref_cast(euid.as_ref()))
3327 .cloned()
3328 .collect(),
3329 ),
3330 ast::ActionConstraint::Eq(id) => ActionConstraint::Eq(EntityUid::ref_cast(id).clone()),
3331 #[cfg(feature = "tolerant-ast")]
3332 ast::ActionConstraint::ErrorConstraint => {
3333 // We will only have an ErrorConstraint if we are using a parser that allows Error nodes
3334 // It is not recommended to evaluate an AST that allows error nodes
3335 // If somehow someone tries to evaluate an AST that includes an Action constraint error, we will
3336 // treat it as `Any`
3337 ActionConstraint::Any
3338 }
3339 }
3340 }
3341
3342 /// Get the scope constraint on this policy's resource
3343 pub fn resource_constraint(&self) -> ResourceConstraint {
3344 let slot_id = ast::SlotId::resource();
3345 match self.ast.template().resource_constraint().as_inner() {
3346 ast::PrincipalOrResourceConstraint::Any => ResourceConstraint::Any,
3347 ast::PrincipalOrResourceConstraint::In(eref) => {
3348 ResourceConstraint::In(self.convert_entity_reference(eref, slot_id).clone())
3349 }
3350 ast::PrincipalOrResourceConstraint::Eq(eref) => {
3351 ResourceConstraint::Eq(self.convert_entity_reference(eref, slot_id).clone())
3352 }
3353 ast::PrincipalOrResourceConstraint::Is(entity_type) => {
3354 ResourceConstraint::Is(entity_type.as_ref().clone().into())
3355 }
3356 ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
3357 ResourceConstraint::IsIn(
3358 entity_type.as_ref().clone().into(),
3359 self.convert_entity_reference(eref, slot_id).clone(),
3360 )
3361 }
3362 }
3363 }
3364
3365 /// To avoid panicking, this function may only be called when `slot` is the
3366 /// `SlotId` corresponding to the scope constraint from which the entity
3367 /// reference `r` was extracted. I.e., If `r` is taken from the principal
3368 /// scope constraint, `slot` must be `?principal`. This ensures that the
3369 /// `SlotId` exists in the policy (and therefore the slot environment map)
3370 /// whenever the `EntityReference` `r` is the Slot variant.
3371 fn convert_entity_reference<'a>(
3372 &'a self,
3373 r: &'a ast::EntityReference,
3374 slot: ast::SlotId,
3375 ) -> &'a EntityUid {
3376 match r {
3377 ast::EntityReference::EUID(euid) => EntityUid::ref_cast(euid),
3378 // PANIC SAFETY: This `unwrap` here is safe due the invariant (values total map) on policies.
3379 #[allow(clippy::unwrap_used)]
3380 ast::EntityReference::Slot(_) => {
3381 EntityUid::ref_cast(self.ast.env().get(&slot).unwrap())
3382 }
3383 }
3384 }
3385
3386 /// Parse a single policy.
3387 /// If `id` is Some, the policy will be given that Policy Id.
3388 /// If `id` is None, then "policy0" will be used.
3389 /// The behavior around None may change in the future.
3390 ///
3391 /// This can fail if the policy fails to parse.
3392 /// It can also fail if a template was passed in, as this function only accepts static
3393 /// policies
3394 pub fn parse(id: Option<PolicyId>, policy_src: impl AsRef<str>) -> Result<Self, ParseErrors> {
3395 let inline_ast = parser::parse_policy(id.map(Into::into), policy_src.as_ref())?;
3396 let (_, ast) = ast::Template::link_static_policy(inline_ast);
3397 Ok(Self {
3398 ast,
3399 lossless: LosslessPolicy::policy_or_template_text(policy_src.as_ref()),
3400 })
3401 }
3402
3403 /// Create a `Policy` from its JSON representation.
3404 /// If `id` is Some, the policy will be given that Policy Id.
3405 /// If `id` is None, then "JSON policy" will be used.
3406 /// The behavior around None may change in the future.
3407 ///
3408 /// ```
3409 /// # use cedar_policy::{Policy, PolicyId};
3410 ///
3411 /// let json: serde_json::Value = serde_json::json!(
3412 /// {
3413 /// "effect":"permit",
3414 /// "principal":{
3415 /// "op":"==",
3416 /// "entity":{
3417 /// "type":"User",
3418 /// "id":"bob"
3419 /// }
3420 /// },
3421 /// "action":{
3422 /// "op":"==",
3423 /// "entity":{
3424 /// "type":"Action",
3425 /// "id":"view"
3426 /// }
3427 /// },
3428 /// "resource":{
3429 /// "op":"==",
3430 /// "entity":{
3431 /// "type":"Album",
3432 /// "id":"trip"
3433 /// }
3434 /// },
3435 /// "conditions":[
3436 /// {
3437 /// "kind":"when",
3438 /// "body":{
3439 /// ">":{
3440 /// "left":{
3441 /// ".":{
3442 /// "left":{
3443 /// "Var":"principal"
3444 /// },
3445 /// "attr":"age"
3446 /// }
3447 /// },
3448 /// "right":{
3449 /// "Value":18
3450 /// }
3451 /// }
3452 /// }
3453 /// }
3454 /// ]
3455 /// }
3456 /// );
3457 /// let json_policy = Policy::from_json(None, json).unwrap();
3458 /// let src = r#"
3459 /// permit(
3460 /// principal == User::"bob",
3461 /// action == Action::"view",
3462 /// resource == Album::"trip"
3463 /// )
3464 /// when { principal.age > 18 };"#;
3465 /// let text_policy = Policy::parse(None, src).unwrap();
3466 /// assert_eq!(json_policy.to_json().unwrap(), text_policy.to_json().unwrap());
3467 /// ```
3468 pub fn from_json(
3469 id: Option<PolicyId>,
3470 json: serde_json::Value,
3471 ) -> Result<Self, PolicyFromJsonError> {
3472 let est: est::Policy = serde_json::from_value(json)
3473 .map_err(|e| entities_json_errors::JsonDeserializationError::Serde(e.into()))
3474 .map_err(cedar_policy_core::est::FromJsonError::from)?;
3475 Self::from_est(id, est)
3476 }
3477
3478 /// Get the valid [`RequestEnv`]s for this policy, according to the schema.
3479 ///
3480 /// That is, all the [`RequestEnv`]s in the schema for which this policy is
3481 /// not trivially false.
3482 pub fn get_valid_request_envs(&self, s: &Schema) -> impl Iterator<Item = RequestEnv> {
3483 get_valid_request_envs(self.ast.template(), s)
3484 }
3485
3486 /// Get all entity literals occuring in a `Policy`
3487 pub fn entity_literals(&self) -> Vec<EntityUid> {
3488 self.ast
3489 .condition()
3490 .subexpressions()
3491 .filter_map(|e| match e.expr_kind() {
3492 cedar_policy_core::ast::ExprKind::Lit(
3493 cedar_policy_core::ast::Literal::EntityUID(euid),
3494 ) => Some(EntityUid((*euid).as_ref().clone())),
3495 _ => None,
3496 })
3497 .collect()
3498 }
3499
3500 /// Return a new policy where all occurrences of key `EntityUid`s are replaced by value `EntityUid`
3501 /// (as a single, non-sequential substitution).
3502 pub fn sub_entity_literals(
3503 &self,
3504 mapping: BTreeMap<EntityUid, EntityUid>,
3505 ) -> Result<Self, PolicyFromJsonError> {
3506 // PANIC SAFETY: This can't fail for a policy that was already constructed
3507 #[allow(clippy::expect_used)]
3508 let cloned_est = self
3509 .lossless
3510 .est()
3511 .expect("Internal error, failed to construct est.");
3512
3513 let mapping = mapping.into_iter().map(|(k, v)| (k.0, v.0)).collect();
3514
3515 // PANIC SAFETY: This can't fail for a policy that was already constructed
3516 #[allow(clippy::expect_used)]
3517 let est = cloned_est
3518 .sub_entity_literals(&mapping)
3519 .expect("Internal error, failed to sub entity literals.");
3520
3521 let ast = match est.clone().try_into_ast_policy(Some(self.ast.id().clone())) {
3522 Ok(ast) => ast,
3523 Err(e) => return Err(e.into()),
3524 };
3525
3526 Ok(Self {
3527 ast,
3528 lossless: LosslessPolicy::Est(est),
3529 })
3530 }
3531
3532 fn from_est(id: Option<PolicyId>, est: est::Policy) -> Result<Self, PolicyFromJsonError> {
3533 Ok(Self {
3534 ast: est.clone().try_into_ast_policy(id.map(PolicyId::into))?,
3535 lossless: LosslessPolicy::Est(est),
3536 })
3537 }
3538
3539 /// Get the JSON representation of this `Policy`.
3540 /// ```
3541 /// # use cedar_policy::Policy;
3542 /// let src = r#"
3543 /// permit(
3544 /// principal == User::"bob",
3545 /// action == Action::"view",
3546 /// resource == Album::"trip"
3547 /// )
3548 /// when { principal.age > 18 };"#;
3549 ///
3550 /// let policy = Policy::parse(None, src).unwrap();
3551 /// println!("{}", policy);
3552 /// // convert the policy to JSON
3553 /// let json = policy.to_json().unwrap();
3554 /// println!("{}", json);
3555 /// assert_eq!(json, Policy::from_json(None, json.clone()).unwrap().to_json().unwrap());
3556 /// ```
3557 pub fn to_json(&self) -> Result<serde_json::Value, PolicyToJsonError> {
3558 let est = self.lossless.est()?;
3559 serde_json::to_value(est).map_err(Into::into)
3560 }
3561
3562 /// Get the human-readable Cedar syntax representation of this policy. This
3563 /// function is primarily intended for rendering JSON policies in the
3564 /// human-readable syntax, but it will also return the original policy text
3565 /// when given a policy parsed from the human-readable syntax.
3566 ///
3567 /// It will return `None` for linked policies because they cannot be
3568 /// directly rendered in Cedar syntax. You can instead render the unlinked
3569 /// template if you do not need to preserve links. If serializing links is
3570 /// important, then you will need to serialize the whole policy set
3571 /// containing the template and link to JSON (or protobuf).
3572 ///
3573 /// It also does not format the policy according to any particular rules.
3574 /// Policy formatting can be done through the Cedar policy CLI or
3575 /// the `cedar-policy-formatter` crate.
3576 pub fn to_cedar(&self) -> Option<String> {
3577 match &self.lossless {
3578 LosslessPolicy::Est(_) => Some(self.ast.to_string()),
3579 LosslessPolicy::Text { text, slots } => {
3580 if slots.is_empty() {
3581 Some(text.clone())
3582 } else {
3583 None
3584 }
3585 }
3586 }
3587 }
3588
3589 /// Get all the unknown entities from the policy
3590 #[doc = include_str!("../experimental_warning.md")]
3591 #[cfg(feature = "partial-eval")]
3592 pub fn unknown_entities(&self) -> HashSet<EntityUid> {
3593 self.ast
3594 .unknown_entities()
3595 .into_iter()
3596 .map(Into::into)
3597 .collect()
3598 }
3599
3600 /// Create a `Policy` from its AST representation only. The `LosslessPolicy`
3601 /// will reflect the AST structure. When possible, don't use this method and
3602 /// create the `Policy` from the policy text, CST, or EST instead, as the
3603 /// conversion to AST is lossy. ESTs for policies generated by this method
3604 /// will reflect the AST and not the original policy syntax.
3605 #[cfg_attr(
3606 not(any(feature = "partial-eval", feature = "protobufs")),
3607 allow(unused)
3608 )]
3609 pub(crate) fn from_ast(ast: ast::Policy) -> Self {
3610 let text = ast.to_string(); // assume that pretty-printing is faster than `est::Policy::from(ast.clone())`; is that true?
3611 Self {
3612 ast,
3613 lossless: LosslessPolicy::policy_or_template_text(text),
3614 }
3615 }
3616}
3617
3618impl std::fmt::Display for Policy {
3619 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3620 // prefer to display the lossless format
3621 self.lossless.fmt(f)
3622 }
3623}
3624
3625impl FromStr for Policy {
3626 type Err = ParseErrors;
3627 /// Create a policy
3628 ///
3629 /// Important note: Policies have ids, but this interface does not
3630 /// allow them to be set. It will use the default "policy0", which
3631 /// may cause id conflicts if not handled. Use `Policy::parse` to set
3632 /// the id when parsing, or `Policy::new_id` to clone a policy with
3633 /// a new id.
3634 fn from_str(policy: &str) -> Result<Self, Self::Err> {
3635 Self::parse(None, policy)
3636 }
3637}
3638
3639/// See comments on `Policy` and `Template`.
3640///
3641/// This structure can be used for static policies, linked policies, and templates.
3642#[derive(Debug, Clone)]
3643pub(crate) enum LosslessPolicy {
3644 /// EST representation
3645 Est(est::Policy),
3646 /// Text representation
3647 Text {
3648 /// actual policy text, of the policy or template
3649 text: String,
3650 /// For linked policies, map of slot to UID. Only linked policies have
3651 /// this; static policies and (unlinked) templates have an empty map
3652 /// here
3653 slots: HashMap<ast::SlotId, ast::EntityUID>,
3654 },
3655}
3656
3657impl LosslessPolicy {
3658 /// Create a new `LosslessPolicy` from the text of a policy or template.
3659 fn policy_or_template_text(text: impl Into<String>) -> Self {
3660 Self::Text {
3661 text: text.into(),
3662 slots: HashMap::new(),
3663 }
3664 }
3665
3666 /// Get the EST representation of this static policy, linked policy, or template
3667 fn est(&self) -> Result<est::Policy, PolicyToJsonError> {
3668 match self {
3669 Self::Est(est) => Ok(est.clone()),
3670 Self::Text { text, slots } => {
3671 let est =
3672 parser::parse_policy_or_template_to_est(text).map_err(ParseErrors::from)?;
3673 if slots.is_empty() {
3674 Ok(est)
3675 } else {
3676 let unwrapped_vals = slots.iter().map(|(k, v)| (*k, v.into())).collect();
3677 Ok(est.link(&unwrapped_vals)?)
3678 }
3679 }
3680 }
3681 }
3682
3683 fn link<'a>(
3684 self,
3685 vals: impl IntoIterator<Item = (ast::SlotId, &'a ast::EntityUID)>,
3686 ) -> Result<Self, est::LinkingError> {
3687 match self {
3688 Self::Est(est) => {
3689 let unwrapped_est_vals: HashMap<
3690 ast::SlotId,
3691 cedar_policy_core::entities::EntityUidJson,
3692 > = vals.into_iter().map(|(k, v)| (k, v.into())).collect();
3693 Ok(Self::Est(est.link(&unwrapped_est_vals)?))
3694 }
3695 Self::Text { text, slots } => {
3696 debug_assert!(
3697 slots.is_empty(),
3698 "shouldn't call link() on an already-linked policy"
3699 );
3700 let slots = vals.into_iter().map(|(k, v)| (k, v.clone())).collect();
3701 Ok(Self::Text { text, slots })
3702 }
3703 }
3704 }
3705}
3706
3707impl std::fmt::Display for LosslessPolicy {
3708 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3709 match self {
3710 Self::Est(est) => write!(f, "{est}"),
3711 Self::Text { text, slots } => {
3712 if slots.is_empty() {
3713 write!(f, "{text}")
3714 } else {
3715 // need to replace placeholders according to `slots`.
3716 // just find-and-replace wouldn't be safe/perfect, we
3717 // want to use the actual parser; right now we reuse
3718 // another implementation by just converting to EST and
3719 // printing that
3720 match self.est() {
3721 Ok(est) => write!(f, "{est}"),
3722 Err(e) => write!(f, "<invalid linked policy: {e}>"),
3723 }
3724 }
3725 }
3726 }
3727 }
3728}
3729
3730/// Expressions to be evaluated
3731#[repr(transparent)]
3732#[derive(Debug, Clone, RefCast)]
3733pub struct Expression(pub(crate) ast::Expr);
3734
3735#[doc(hidden)] // because this converts to a private/internal type
3736impl AsRef<ast::Expr> for Expression {
3737 fn as_ref(&self) -> &ast::Expr {
3738 &self.0
3739 }
3740}
3741
3742impl Expression {
3743 /// Create an expression representing a literal string.
3744 pub fn new_string(value: String) -> Self {
3745 Self(ast::Expr::val(value))
3746 }
3747
3748 /// Create an expression representing a literal bool.
3749 pub fn new_bool(value: bool) -> Self {
3750 Self(ast::Expr::val(value))
3751 }
3752
3753 /// Create an expression representing a literal long.
3754 pub fn new_long(value: ast::Integer) -> Self {
3755 Self(ast::Expr::val(value))
3756 }
3757
3758 /// Create an expression representing a record.
3759 ///
3760 /// Error if any key appears two or more times in `fields`.
3761 pub fn new_record(
3762 fields: impl IntoIterator<Item = (String, Self)>,
3763 ) -> Result<Self, ExpressionConstructionError> {
3764 Ok(Self(ast::Expr::record(
3765 fields.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
3766 )?))
3767 }
3768
3769 /// Create an expression representing a Set.
3770 pub fn new_set(values: impl IntoIterator<Item = Self>) -> Self {
3771 Self(ast::Expr::set(values.into_iter().map(|v| v.0)))
3772 }
3773
3774 /// Create an expression representing an ip address.
3775 /// This function does not perform error checking on the source string,
3776 /// it creates an expression that calls the `ip` constructor.
3777 pub fn new_ip(src: impl AsRef<str>) -> Self {
3778 let src_expr = ast::Expr::val(src.as_ref());
3779 Self(ast::Expr::call_extension_fn(
3780 ip_extension_name(),
3781 vec![src_expr],
3782 ))
3783 }
3784
3785 /// Create an expression representing a fixed precision decimal number.
3786 /// This function does not perform error checking on the source string,
3787 /// it creates an expression that calls the `decimal` constructor.
3788 pub fn new_decimal(src: impl AsRef<str>) -> Self {
3789 let src_expr = ast::Expr::val(src.as_ref());
3790 Self(ast::Expr::call_extension_fn(
3791 decimal_extension_name(),
3792 vec![src_expr],
3793 ))
3794 }
3795}
3796
3797#[cfg(test)]
3798impl Expression {
3799 /// Deconstruct an [`Expression`] to get the internal type.
3800 /// This function is only intended to be used internally.
3801 pub(crate) fn into_inner(self) -> ast::Expr {
3802 self.0
3803 }
3804}
3805
3806impl FromStr for Expression {
3807 type Err = ParseErrors;
3808
3809 /// create an Expression using Cedar syntax
3810 fn from_str(expression: &str) -> Result<Self, Self::Err> {
3811 ast::Expr::from_str(expression)
3812 .map(Expression)
3813 .map_err(Into::into)
3814 }
3815}
3816
3817/// "Restricted" expressions are used for attribute values and `context`.
3818///
3819/// Restricted expressions can contain only the following:
3820/// - bool, int, and string literals
3821/// - literal `EntityUid`s such as `User::"alice"`
3822/// - extension function calls, where the arguments must be other things
3823/// on this list
3824/// - set and record literals, where the values must be other things on
3825/// this list
3826///
3827/// That means the following are not allowed in restricted expressions:
3828/// - `principal`, `action`, `resource`, `context`
3829/// - builtin operators and functions, including `.`, `in`, `has`, `like`,
3830/// `.contains()`
3831/// - if-then-else expressions
3832#[repr(transparent)]
3833#[derive(Debug, Clone, RefCast)]
3834pub struct RestrictedExpression(ast::RestrictedExpr);
3835
3836#[doc(hidden)] // because this converts to a private/internal type
3837impl AsRef<ast::RestrictedExpr> for RestrictedExpression {
3838 fn as_ref(&self) -> &ast::RestrictedExpr {
3839 &self.0
3840 }
3841}
3842
3843impl RestrictedExpression {
3844 /// Create an expression representing a literal string.
3845 pub fn new_string(value: String) -> Self {
3846 Self(ast::RestrictedExpr::val(value))
3847 }
3848
3849 /// Create an expression representing a literal bool.
3850 pub fn new_bool(value: bool) -> Self {
3851 Self(ast::RestrictedExpr::val(value))
3852 }
3853
3854 /// Create an expression representing a literal long.
3855 pub fn new_long(value: ast::Integer) -> Self {
3856 Self(ast::RestrictedExpr::val(value))
3857 }
3858
3859 /// Create an expression representing a literal `EntityUid`.
3860 pub fn new_entity_uid(value: EntityUid) -> Self {
3861 Self(ast::RestrictedExpr::val(ast::EntityUID::from(value)))
3862 }
3863
3864 /// Create an expression representing a record.
3865 ///
3866 /// Error if any key appears two or more times in `fields`.
3867 pub fn new_record(
3868 fields: impl IntoIterator<Item = (String, Self)>,
3869 ) -> Result<Self, ExpressionConstructionError> {
3870 Ok(Self(ast::RestrictedExpr::record(
3871 fields.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
3872 )?))
3873 }
3874
3875 /// Create an expression representing a Set.
3876 pub fn new_set(values: impl IntoIterator<Item = Self>) -> Self {
3877 Self(ast::RestrictedExpr::set(values.into_iter().map(|v| v.0)))
3878 }
3879
3880 /// Create an expression representing an ip address.
3881 /// This function does not perform error checking on the source string,
3882 /// it creates an expression that calls the `ip` constructor.
3883 pub fn new_ip(src: impl AsRef<str>) -> Self {
3884 let src_expr = ast::RestrictedExpr::val(src.as_ref());
3885 Self(ast::RestrictedExpr::call_extension_fn(
3886 ip_extension_name(),
3887 [src_expr],
3888 ))
3889 }
3890
3891 /// Create an expression representing a fixed precision decimal number.
3892 /// This function does not perform error checking on the source string,
3893 /// it creates an expression that calls the `decimal` constructor.
3894 pub fn new_decimal(src: impl AsRef<str>) -> Self {
3895 let src_expr = ast::RestrictedExpr::val(src.as_ref());
3896 Self(ast::RestrictedExpr::call_extension_fn(
3897 decimal_extension_name(),
3898 [src_expr],
3899 ))
3900 }
3901
3902 /// Create an unknown expression
3903 #[cfg(feature = "partial-eval")]
3904 pub fn new_unknown(name: impl AsRef<str>) -> Self {
3905 Self(ast::RestrictedExpr::unknown(ast::Unknown::new_untyped(
3906 name.as_ref(),
3907 )))
3908 }
3909}
3910
3911#[cfg(test)]
3912impl RestrictedExpression {
3913 /// Deconstruct an [`RestrictedExpression`] to get the internal type.
3914 /// This function is only intended to be used internally.
3915 pub(crate) fn into_inner(self) -> ast::RestrictedExpr {
3916 self.0
3917 }
3918}
3919
3920fn decimal_extension_name() -> ast::Name {
3921 // PANIC SAFETY: This is a constant and is known to be safe, verified by a test
3922 #[allow(clippy::unwrap_used)]
3923 ast::Name::unqualified_name("decimal".parse().unwrap())
3924}
3925
3926fn ip_extension_name() -> ast::Name {
3927 // PANIC SAFETY: This is a constant and is known to be safe, verified by a test
3928 #[allow(clippy::unwrap_used)]
3929 ast::Name::unqualified_name("ip".parse().unwrap())
3930}
3931
3932impl FromStr for RestrictedExpression {
3933 type Err = RestrictedExpressionParseError;
3934
3935 /// create a `RestrictedExpression` using Cedar syntax
3936 fn from_str(expression: &str) -> Result<Self, Self::Err> {
3937 ast::RestrictedExpr::from_str(expression)
3938 .map(RestrictedExpression)
3939 .map_err(Into::into)
3940 }
3941}
3942
3943/// Builder for a [`Request`]
3944///
3945/// The default for principal, action, resource, and context fields is Unknown
3946/// for partial evaluation.
3947#[doc = include_str!("../experimental_warning.md")]
3948#[cfg(feature = "partial-eval")]
3949#[derive(Debug, Clone)]
3950pub struct RequestBuilder<S> {
3951 principal: ast::EntityUIDEntry,
3952 action: ast::EntityUIDEntry,
3953 resource: ast::EntityUIDEntry,
3954 /// Here, `None` means unknown
3955 context: Option<ast::Context>,
3956 schema: S,
3957}
3958
3959/// A marker type that indicates [`Schema`] is not set for a request
3960#[doc = include_str!("../experimental_warning.md")]
3961#[cfg(feature = "partial-eval")]
3962#[derive(Debug, Clone, Copy)]
3963pub struct UnsetSchema;
3964
3965#[cfg(feature = "partial-eval")]
3966impl Default for RequestBuilder<UnsetSchema> {
3967 fn default() -> Self {
3968 Self {
3969 principal: ast::EntityUIDEntry::unknown(),
3970 action: ast::EntityUIDEntry::unknown(),
3971 resource: ast::EntityUIDEntry::unknown(),
3972 context: None,
3973 schema: UnsetSchema,
3974 }
3975 }
3976}
3977
3978#[cfg(feature = "partial-eval")]
3979impl<S> RequestBuilder<S> {
3980 /// Set the principal.
3981 ///
3982 /// Note that you can create the `EntityUid` using `.parse()` on any
3983 /// string (via the `FromStr` implementation for `EntityUid`).
3984 #[must_use]
3985 pub fn principal(self, principal: EntityUid) -> Self {
3986 Self {
3987 principal: ast::EntityUIDEntry::known(principal.into(), None),
3988 ..self
3989 }
3990 }
3991
3992 /// Set the principal to be unknown, but known to belong to a certain entity type.
3993 ///
3994 /// This information is taken into account when evaluating 'is', '==' and '!=' expressions.
3995 #[must_use]
3996 pub fn unknown_principal_with_type(self, principal_type: EntityTypeName) -> Self {
3997 Self {
3998 principal: ast::EntityUIDEntry::unknown_with_type(principal_type.0, None),
3999 ..self
4000 }
4001 }
4002
4003 /// Set the action.
4004 ///
4005 /// Note that you can create the `EntityUid` using `.parse()` on any
4006 /// string (via the `FromStr` implementation for `EntityUid`).
4007 #[must_use]
4008 pub fn action(self, action: EntityUid) -> Self {
4009 Self {
4010 action: ast::EntityUIDEntry::known(action.into(), None),
4011 ..self
4012 }
4013 }
4014
4015 /// Set the resource.
4016 ///
4017 /// Note that you can create the `EntityUid` using `.parse()` on any
4018 /// string (via the `FromStr` implementation for `EntityUid`).
4019 #[must_use]
4020 pub fn resource(self, resource: EntityUid) -> Self {
4021 Self {
4022 resource: ast::EntityUIDEntry::known(resource.into(), None),
4023 ..self
4024 }
4025 }
4026
4027 /// Set the resource to be unknown, but known to belong to a certain entity type.
4028 ///
4029 /// This information is taken into account when evaluating 'is', '==' and '!=' expressions.
4030 #[must_use]
4031 pub fn unknown_resource_with_type(self, resource_type: EntityTypeName) -> Self {
4032 Self {
4033 resource: ast::EntityUIDEntry::unknown_with_type(resource_type.0, None),
4034 ..self
4035 }
4036 }
4037
4038 /// Set the context.
4039 #[must_use]
4040 pub fn context(self, context: Context) -> Self {
4041 Self {
4042 context: Some(context.0),
4043 ..self
4044 }
4045 }
4046}
4047
4048#[cfg(feature = "partial-eval")]
4049impl RequestBuilder<UnsetSchema> {
4050 /// Set the schema. If present, this will be used for request validation.
4051 #[must_use]
4052 pub fn schema(self, schema: &Schema) -> RequestBuilder<&Schema> {
4053 RequestBuilder {
4054 principal: self.principal,
4055 action: self.action,
4056 resource: self.resource,
4057 context: self.context,
4058 schema,
4059 }
4060 }
4061
4062 /// Create the [`Request`]
4063 pub fn build(self) -> Request {
4064 Request(ast::Request::new_unchecked(
4065 self.principal,
4066 self.action,
4067 self.resource,
4068 self.context,
4069 ))
4070 }
4071}
4072
4073#[cfg(feature = "partial-eval")]
4074impl RequestBuilder<&Schema> {
4075 /// Create the [`Request`]
4076 pub fn build(self) -> Result<Request, RequestValidationError> {
4077 Ok(Request(ast::Request::new_with_unknowns(
4078 self.principal,
4079 self.action,
4080 self.resource,
4081 self.context,
4082 Some(&self.schema.0),
4083 Extensions::all_available(),
4084 )?))
4085 }
4086}
4087
4088/// An authorization request is a tuple `<P, A, R, C>` where
4089/// * P is the principal [`EntityUid`],
4090/// * A is the action [`EntityUid`],
4091/// * R is the resource [`EntityUid`], and
4092/// * C is the request [`Context`] record.
4093///
4094/// It represents an authorization request asking the question, "Can this
4095/// principal take this action on this resource in this context?"
4096#[repr(transparent)]
4097#[derive(Debug, Clone, RefCast)]
4098pub struct Request(pub(crate) ast::Request);
4099
4100#[doc(hidden)] // because this converts to a private/internal type
4101impl AsRef<ast::Request> for Request {
4102 fn as_ref(&self) -> &ast::Request {
4103 &self.0
4104 }
4105}
4106
4107impl Request {
4108 /// Create a [`RequestBuilder`]
4109 #[doc = include_str!("../experimental_warning.md")]
4110 #[cfg(feature = "partial-eval")]
4111 pub fn builder() -> RequestBuilder<UnsetSchema> {
4112 RequestBuilder::default()
4113 }
4114
4115 /// Create a Request.
4116 ///
4117 /// Note that you can create the `EntityUid`s using `.parse()` on any
4118 /// string (via the `FromStr` implementation for `EntityUid`).
4119 /// The principal, action, and resource fields are optional to support
4120 /// the case where these fields do not contribute to authorization
4121 /// decisions (e.g., because they are not used in your policies).
4122 /// If any of the fields are `None`, we will automatically generate
4123 /// a unique entity UID that is not equal to any UID in the store.
4124 ///
4125 /// If `schema` is present, this constructor will validate that the
4126 /// `Request` complies with the given `schema`.
4127 pub fn new(
4128 principal: EntityUid,
4129 action: EntityUid,
4130 resource: EntityUid,
4131 context: Context,
4132 schema: Option<&Schema>,
4133 ) -> Result<Self, RequestValidationError> {
4134 Ok(Self(ast::Request::new(
4135 (principal.into(), None),
4136 (action.into(), None),
4137 (resource.into(), None),
4138 context.0,
4139 schema.map(|schema| &schema.0),
4140 Extensions::all_available(),
4141 )?))
4142 }
4143
4144 /// Get the context component of the request. Returns `None` if the context is
4145 /// "unknown" (i.e., constructed using the partial evaluation APIs).
4146 pub fn context(&self) -> Option<&Context> {
4147 self.0.context().map(Context::ref_cast)
4148 }
4149
4150 /// Get the principal component of the request. Returns `None` if the principal is
4151 /// "unknown" (i.e., constructed using the partial evaluation APIs).
4152 pub fn principal(&self) -> Option<&EntityUid> {
4153 match self.0.principal() {
4154 ast::EntityUIDEntry::Known { euid, .. } => Some(EntityUid::ref_cast(euid.as_ref())),
4155 ast::EntityUIDEntry::Unknown { .. } => None,
4156 }
4157 }
4158
4159 /// Get the action component of the request. Returns `None` if the action is
4160 /// "unknown" (i.e., constructed using the partial evaluation APIs).
4161 pub fn action(&self) -> Option<&EntityUid> {
4162 match self.0.action() {
4163 ast::EntityUIDEntry::Known { euid, .. } => Some(EntityUid::ref_cast(euid.as_ref())),
4164 ast::EntityUIDEntry::Unknown { .. } => None,
4165 }
4166 }
4167
4168 /// Get the resource component of the request. Returns `None` if the resource is
4169 /// "unknown" (i.e., constructed using the partial evaluation APIs).
4170 pub fn resource(&self) -> Option<&EntityUid> {
4171 match self.0.resource() {
4172 ast::EntityUIDEntry::Known { euid, .. } => Some(EntityUid::ref_cast(euid.as_ref())),
4173 ast::EntityUIDEntry::Unknown { .. } => None,
4174 }
4175 }
4176}
4177
4178/// the Context object for an authorization request
4179#[repr(transparent)]
4180#[derive(Debug, Clone, RefCast)]
4181pub struct Context(ast::Context);
4182
4183#[doc(hidden)] // because this converts to a private/internal type
4184impl AsRef<ast::Context> for Context {
4185 fn as_ref(&self) -> &ast::Context {
4186 &self.0
4187 }
4188}
4189
4190impl Context {
4191 /// Create an empty `Context`
4192 /// ```
4193 /// # use cedar_policy::Context;
4194 /// let context = Context::empty();
4195 /// ```
4196 pub fn empty() -> Self {
4197 Self(ast::Context::empty())
4198 }
4199
4200 /// Create a `Context` from a map of key to "restricted expression",
4201 /// or a Vec of `(key, restricted expression)` pairs, or any other iterator
4202 /// of `(key, restricted expression)` pairs.
4203 /// ```
4204 /// # use cedar_policy::{Context, EntityUid, RestrictedExpression, Request};
4205 /// # use std::str::FromStr;
4206 /// let context = Context::from_pairs([
4207 /// ("key".to_string(), RestrictedExpression::from_str(r#""value""#).unwrap()),
4208 /// ("age".to_string(), RestrictedExpression::from_str("18").unwrap()),
4209 /// ]).unwrap();
4210 /// # // create a request
4211 /// # let p = EntityUid::from_str(r#"User::"alice""#).unwrap();
4212 /// # let a = EntityUid::from_str(r#"Action::"view""#).unwrap();
4213 /// # let r = EntityUid::from_str(r#"Album::"trip""#).unwrap();
4214 /// # let request: Request = Request::new(p, a, r, context, None).unwrap();
4215 /// ```
4216 pub fn from_pairs(
4217 pairs: impl IntoIterator<Item = (String, RestrictedExpression)>,
4218 ) -> Result<Self, ContextCreationError> {
4219 Ok(Self(ast::Context::from_pairs(
4220 pairs.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
4221 Extensions::all_available(),
4222 )?))
4223 }
4224
4225 /// Retrieves a value from the Context by its key.
4226 ///
4227 /// # Arguments
4228 ///
4229 /// * `key` - The key to look up in the context
4230 ///
4231 /// # Returns
4232 ///
4233 /// * `Some(EvalResult)` - If the key exists in the context, returns its value
4234 /// * `None` - If the key doesn't exist or if the context is not a Value type
4235 ///
4236 /// # Examples
4237 ///
4238 /// ```
4239 /// # use cedar_policy::{Context, Request, EntityUid};
4240 /// # use std::str::FromStr;
4241 /// let context = Context::from_json_str(r#"{"rayId": "abc123"}"#, None).unwrap();
4242 /// if let Some(value) = context.get("rayId") {
4243 /// // value here is an EvalResult, convertible from the internal Value type
4244 /// println!("Found value: {:?}", value);
4245 /// }
4246 /// assert_eq!(context.get("nonexistent"), None);
4247 /// ```
4248 pub fn get(&self, key: &str) -> Option<EvalResult> {
4249 match &self.0 {
4250 ast::Context::Value(map) => map.get(key).map(|v| EvalResult::from(v.clone())),
4251 ast::Context::RestrictedResidual(_) => None,
4252 }
4253 }
4254
4255 /// Create a `Context` from a string containing JSON (which must be a JSON
4256 /// object, not any other JSON type, or you will get an error here).
4257 /// JSON here must use the `__entity` and `__extn` escapes for entity
4258 /// references, extension values, etc.
4259 ///
4260 /// If a `schema` is provided, this will inform the parsing: for instance, it
4261 /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
4262 /// if attributes have the wrong types (e.g., string instead of integer).
4263 /// Since different Actions have different schemas for `Context`, you also
4264 /// must specify the `Action` for schema-based parsing.
4265 /// ```
4266 /// # use cedar_policy::{Context, EntityUid, RestrictedExpression, Request};
4267 /// # use std::str::FromStr;
4268 /// let json_data = r#"{
4269 /// "sub": "1234",
4270 /// "groups": {
4271 /// "1234": {
4272 /// "group_id": "abcd",
4273 /// "group_name": "test-group"
4274 /// }
4275 /// }
4276 /// }"#;
4277 /// let context = Context::from_json_str(json_data, None).unwrap();
4278 /// # // create a request
4279 /// # let p = EntityUid::from_str(r#"User::"alice""#).unwrap();
4280 /// # let a = EntityUid::from_str(r#"Action::"view""#).unwrap();
4281 /// # let r = EntityUid::from_str(r#"Album::"trip""#).unwrap();
4282 /// # let request: Request = Request::new(p, a, r, context, None).unwrap();
4283 /// ```
4284 pub fn from_json_str(
4285 json: &str,
4286 schema: Option<(&Schema, &EntityUid)>,
4287 ) -> Result<Self, ContextJsonError> {
4288 let schema = schema
4289 .map(|(s, uid)| Self::get_context_schema(s, uid))
4290 .transpose()?;
4291 let context = cedar_policy_core::entities::ContextJsonParser::new(
4292 schema.as_ref(),
4293 Extensions::all_available(),
4294 )
4295 .from_json_str(json)?;
4296 Ok(Self(context))
4297 }
4298
4299 /// Create a `Context` from a `serde_json::Value` (which must be a JSON object,
4300 /// not any other JSON type, or you will get an error here).
4301 /// JSON here must use the `__entity` and `__extn` escapes for entity
4302 /// references, extension values, etc.
4303 ///
4304 /// If a `schema` is provided, this will inform the parsing: for instance, it
4305 /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
4306 /// if attributes have the wrong types (e.g., string instead of integer).
4307 /// Since different Actions have different schemas for `Context`, you also
4308 /// must specify the `Action` for schema-based parsing.
4309 /// ```
4310 /// # use cedar_policy::{Context, EntityUid, EntityId, EntityTypeName, RestrictedExpression, Request, Schema};
4311 /// # use std::str::FromStr;
4312 /// let schema_json = serde_json::json!(
4313 /// {
4314 /// "": {
4315 /// "entityTypes": {
4316 /// "User": {},
4317 /// "Album": {},
4318 /// },
4319 /// "actions": {
4320 /// "view": {
4321 /// "appliesTo": {
4322 /// "principalTypes": ["User"],
4323 /// "resourceTypes": ["Album"],
4324 /// "context": {
4325 /// "type": "Record",
4326 /// "attributes": {
4327 /// "sub": { "type": "Long" }
4328 /// }
4329 /// }
4330 /// }
4331 /// }
4332 /// }
4333 /// }
4334 /// });
4335 /// let schema = Schema::from_json_value(schema_json).unwrap();
4336 ///
4337 /// let a_eid = EntityId::from_str("view").unwrap();
4338 /// let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
4339 /// let action = EntityUid::from_type_name_and_id(a_name, a_eid);
4340 /// let data = serde_json::json!({
4341 /// "sub": 1234
4342 /// });
4343 /// let context = Context::from_json_value(data, Some((&schema, &action))).unwrap();
4344 /// # let p = EntityUid::from_str(r#"User::"alice""#).unwrap();
4345 /// # let r = EntityUid::from_str(r#"Album::"trip""#).unwrap();
4346 /// # let request: Request = Request::new(p, action, r, context, Some(&schema)).unwrap();
4347 /// ```
4348 pub fn from_json_value(
4349 json: serde_json::Value,
4350 schema: Option<(&Schema, &EntityUid)>,
4351 ) -> Result<Self, ContextJsonError> {
4352 let schema = schema
4353 .map(|(s, uid)| Self::get_context_schema(s, uid))
4354 .transpose()?;
4355 let context = cedar_policy_core::entities::ContextJsonParser::new(
4356 schema.as_ref(),
4357 Extensions::all_available(),
4358 )
4359 .from_json_value(json)?;
4360 Ok(Self(context))
4361 }
4362
4363 /// Create a `Context` from a JSON file. The JSON file must contain a JSON
4364 /// object, not any other JSON type, or you will get an error here.
4365 /// JSON here must use the `__entity` and `__extn` escapes for entity
4366 /// references, extension values, etc.
4367 ///
4368 /// If a `schema` is provided, this will inform the parsing: for instance, it
4369 /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
4370 /// if attributes have the wrong types (e.g., string instead of integer).
4371 /// Since different Actions have different schemas for `Context`, you also
4372 /// must specify the `Action` for schema-based parsing.
4373 /// ```no_run
4374 /// # use cedar_policy::{Context, RestrictedExpression};
4375 /// # use cedar_policy::{Entities, EntityId, EntityTypeName, EntityUid, Request,PolicySet};
4376 /// # use std::collections::HashMap;
4377 /// # use std::str::FromStr;
4378 /// # use std::fs::File;
4379 /// let mut json = File::open("json_file.json").unwrap();
4380 /// let context = Context::from_json_file(&json, None).unwrap();
4381 /// # // create a request
4382 /// # let p_eid = EntityId::from_str("alice").unwrap();
4383 /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
4384 /// # let p = EntityUid::from_type_name_and_id(p_name, p_eid);
4385 /// #
4386 /// # let a_eid = EntityId::from_str("view").unwrap();
4387 /// # let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
4388 /// # let a = EntityUid::from_type_name_and_id(a_name, a_eid);
4389 /// # let r_eid = EntityId::from_str("trip").unwrap();
4390 /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
4391 /// # let r = EntityUid::from_type_name_and_id(r_name, r_eid);
4392 /// # let request: Request = Request::new(p, a, r, context, None).unwrap();
4393 /// ```
4394 pub fn from_json_file(
4395 json: impl std::io::Read,
4396 schema: Option<(&Schema, &EntityUid)>,
4397 ) -> Result<Self, ContextJsonError> {
4398 let schema = schema
4399 .map(|(s, uid)| Self::get_context_schema(s, uid))
4400 .transpose()?;
4401 let context = cedar_policy_core::entities::ContextJsonParser::new(
4402 schema.as_ref(),
4403 Extensions::all_available(),
4404 )
4405 .from_json_file(json)?;
4406 Ok(Self(context))
4407 }
4408
4409 /// Internal helper function to convert `(&Schema, &EntityUid)` to `impl ContextSchema`
4410 fn get_context_schema(
4411 schema: &Schema,
4412 action: &EntityUid,
4413 ) -> Result<impl ContextSchema, ContextJsonError> {
4414 cedar_policy_validator::context_schema_for_action(&schema.0, action.as_ref())
4415 .ok_or_else(|| ContextJsonError::missing_action(action.clone()))
4416 }
4417
4418 /// Merge this [`Context`] with another context (or iterator over
4419 /// `(String, RestrictedExpression)` pairs), returning an error if the two
4420 /// contain overlapping keys
4421 pub fn merge(
4422 self,
4423 other_context: impl IntoIterator<Item = (String, RestrictedExpression)>,
4424 ) -> Result<Self, ContextCreationError> {
4425 Self::from_pairs(self.into_iter().chain(other_context))
4426 }
4427}
4428
4429/// Utilities for implementing `IntoIterator` for `Context`
4430mod context {
4431 use super::{ast, RestrictedExpression};
4432
4433 /// `IntoIter` iterator for `Context`
4434 #[derive(Debug)]
4435 pub struct IntoIter {
4436 pub(super) inner: <ast::Context as IntoIterator>::IntoIter,
4437 }
4438
4439 impl Iterator for IntoIter {
4440 type Item = (String, RestrictedExpression);
4441
4442 fn next(&mut self) -> Option<Self::Item> {
4443 self.inner
4444 .next()
4445 .map(|(k, v)| (k.to_string(), RestrictedExpression(v)))
4446 }
4447 }
4448}
4449
4450impl IntoIterator for Context {
4451 type Item = (String, RestrictedExpression);
4452
4453 type IntoIter = context::IntoIter;
4454
4455 fn into_iter(self) -> Self::IntoIter {
4456 Self::IntoIter {
4457 inner: self.0.into_iter(),
4458 }
4459 }
4460}
4461
4462#[doc(hidden)]
4463impl From<ast::Context> for Context {
4464 fn from(c: ast::Context) -> Self {
4465 Self(c)
4466 }
4467}
4468
4469impl std::fmt::Display for Request {
4470 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4471 write!(f, "{}", self.0)
4472 }
4473}
4474
4475impl std::fmt::Display for Context {
4476 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4477 write!(f, "{}", self.0)
4478 }
4479}
4480
4481/// Result of Evaluation
4482#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
4483pub enum EvalResult {
4484 /// Boolean value
4485 Bool(bool),
4486 /// Signed integer value
4487 Long(ast::Integer),
4488 /// String value
4489 String(String),
4490 /// Entity Uid
4491 EntityUid(EntityUid),
4492 /// A first-class set
4493 Set(Set),
4494 /// A first-class anonymous record
4495 Record(Record),
4496 /// An extension value, currently limited to String results
4497 ExtensionValue(String),
4498 // ExtensionValue(std::sync::Arc<dyn InternalExtensionValue>),
4499}
4500
4501/// Sets of Cedar values
4502#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord)]
4503pub struct Set(BTreeSet<EvalResult>);
4504
4505impl Set {
4506 /// Iterate over the members of the set
4507 pub fn iter(&self) -> impl Iterator<Item = &EvalResult> {
4508 self.0.iter()
4509 }
4510
4511 /// Is a given element in the set
4512 pub fn contains(&self, elem: &EvalResult) -> bool {
4513 self.0.contains(elem)
4514 }
4515
4516 /// Get the number of members of the set
4517 pub fn len(&self) -> usize {
4518 self.0.len()
4519 }
4520
4521 /// Test if the set is empty
4522 pub fn is_empty(&self) -> bool {
4523 self.0.is_empty()
4524 }
4525}
4526
4527/// A record of Cedar values
4528#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord)]
4529pub struct Record(BTreeMap<String, EvalResult>);
4530
4531impl Record {
4532 /// Iterate over the attribute/value pairs in the record
4533 pub fn iter(&self) -> impl Iterator<Item = (&String, &EvalResult)> {
4534 self.0.iter()
4535 }
4536
4537 /// Check if a given attribute is in the record
4538 pub fn contains_attribute(&self, key: impl AsRef<str>) -> bool {
4539 self.0.contains_key(key.as_ref())
4540 }
4541
4542 /// Get a given attribute from the record
4543 pub fn get(&self, key: impl AsRef<str>) -> Option<&EvalResult> {
4544 self.0.get(key.as_ref())
4545 }
4546
4547 /// Get the number of attributes in the record
4548 pub fn len(&self) -> usize {
4549 self.0.len()
4550 }
4551
4552 /// Test if the record is empty
4553 pub fn is_empty(&self) -> bool {
4554 self.0.is_empty()
4555 }
4556}
4557
4558#[doc(hidden)]
4559impl From<ast::Value> for EvalResult {
4560 fn from(v: ast::Value) -> Self {
4561 match v.value {
4562 ast::ValueKind::Lit(ast::Literal::Bool(b)) => Self::Bool(b),
4563 ast::ValueKind::Lit(ast::Literal::Long(i)) => Self::Long(i),
4564 ast::ValueKind::Lit(ast::Literal::String(s)) => Self::String(s.to_string()),
4565 ast::ValueKind::Lit(ast::Literal::EntityUID(e)) => {
4566 Self::EntityUid(ast::EntityUID::clone(&e).into())
4567 }
4568 ast::ValueKind::Set(set) => Self::Set(Set(set
4569 .authoritative
4570 .iter()
4571 .map(|v| v.clone().into())
4572 .collect())),
4573 ast::ValueKind::Record(record) => Self::Record(Record(
4574 record
4575 .iter()
4576 .map(|(k, v)| (k.to_string(), v.clone().into()))
4577 .collect(),
4578 )),
4579 ast::ValueKind::ExtensionValue(ev) => {
4580 Self::ExtensionValue(RestrictedExpr::from(ev.as_ref().clone()).to_string())
4581 }
4582 }
4583 }
4584}
4585impl std::fmt::Display for EvalResult {
4586 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4587 match self {
4588 Self::Bool(b) => write!(f, "{b}"),
4589 Self::Long(l) => write!(f, "{l}"),
4590 Self::String(s) => write!(f, "\"{}\"", s.escape_debug()),
4591 Self::EntityUid(uid) => write!(f, "{uid}"),
4592 Self::Set(s) => {
4593 write!(f, "[")?;
4594 for (i, ev) in s.iter().enumerate() {
4595 write!(f, "{ev}")?;
4596 if (i + 1) < s.len() {
4597 write!(f, ", ")?;
4598 }
4599 }
4600 write!(f, "]")?;
4601 Ok(())
4602 }
4603 Self::Record(r) => {
4604 write!(f, "{{")?;
4605 for (i, (k, v)) in r.iter().enumerate() {
4606 write!(f, "\"{}\": {v}", k.escape_debug())?;
4607 if (i + 1) < r.len() {
4608 write!(f, ", ")?;
4609 }
4610 }
4611 write!(f, "}}")?;
4612 Ok(())
4613 }
4614 Self::ExtensionValue(s) => write!(f, "{s}"),
4615 }
4616 }
4617}
4618
4619/// Evaluates an expression.
4620///
4621/// If evaluation results in an error (e.g., attempting to access a non-existent Entity or Record,
4622/// passing the wrong number of arguments to a function etc.), that error is returned as a String
4623pub fn eval_expression(
4624 request: &Request,
4625 entities: &Entities,
4626 expr: &Expression,
4627) -> Result<EvalResult, EvaluationError> {
4628 let all_ext = Extensions::all_available();
4629 let eval = Evaluator::new(request.0.clone(), &entities.0, all_ext);
4630 Ok(EvalResult::from(
4631 // Evaluate under the empty slot map, as an expression should not have slots
4632 eval.interpret(&expr.0, &ast::SlotEnv::new())?,
4633 ))
4634}
4635
4636// These are the same tests in validator, just ensuring all the plumbing is done correctly
4637#[cfg(test)]
4638mod test_access {
4639 use cedar_policy_core::ast;
4640
4641 use super::*;
4642
4643 fn schema() -> Schema {
4644 let src = r#"
4645 type Task = {
4646 "id": Long,
4647 "name": String,
4648 "state": String,
4649};
4650
4651type T = String;
4652
4653type Tasks = Set<Task>;
4654entity List in [Application] = {
4655 "editors": Team,
4656 "name": String,
4657 "owner": User,
4658 "readers": Team,
4659 "tasks": Tasks,
4660};
4661entity Application;
4662entity User in [Team, Application] = {
4663 "joblevel": Long,
4664 "location": String,
4665};
4666
4667entity CoolList;
4668
4669entity Team in [Team, Application];
4670
4671action Read, Write, Create;
4672
4673action DeleteList, EditShare, UpdateList, CreateTask, UpdateTask, DeleteTask in Write appliesTo {
4674 principal: [User],
4675 resource : [List]
4676};
4677
4678action GetList in Read appliesTo {
4679 principal : [User],
4680 resource : [List, CoolList]
4681};
4682
4683action GetLists in Read appliesTo {
4684 principal : [User],
4685 resource : [Application]
4686};
4687
4688action CreateList in Create appliesTo {
4689 principal : [User],
4690 resource : [Application]
4691};
4692
4693 "#;
4694
4695 src.parse().unwrap()
4696 }
4697
4698 #[test]
4699 fn principals() {
4700 let schema = schema();
4701 let principals = schema.principals().collect::<HashSet<_>>();
4702 assert_eq!(principals.len(), 1);
4703 let user: EntityTypeName = "User".parse().unwrap();
4704 assert!(principals.contains(&user));
4705 let principals = schema.principals().collect::<Vec<_>>();
4706 assert!(principals.len() > 1);
4707 assert!(principals.iter().all(|ety| **ety == user));
4708 assert!(principals.iter().all(|ety| ety.0.loc().is_some()));
4709
4710 let et = ast::EntityType::EntityType(ast::Name::from_normalized_str("User").unwrap());
4711 let et = schema.0.get_entity_type(&et).unwrap();
4712 assert!(et.loc.as_ref().is_some());
4713 }
4714
4715 #[cfg(feature = "extended-schema")]
4716 #[test]
4717 fn common_types_extended() {
4718 use cool_asserts::assert_matches;
4719
4720 use cedar_policy_validator::{
4721 types::{EntityRecordKind, Type},
4722 ValidatorCommonType,
4723 };
4724
4725 let schema = schema();
4726 assert_eq!(schema.0.common_types().collect::<HashSet<_>>().len(), 3);
4727 let task_type = ValidatorCommonType {
4728 name: "Task".into(),
4729 name_loc: None,
4730 type_loc: None,
4731 };
4732 assert!(schema.0.common_types().contains(&task_type));
4733
4734 let tasks_type = ValidatorCommonType {
4735 name: "Tasks".into(),
4736 name_loc: None,
4737 type_loc: None,
4738 };
4739 assert!(schema.0.common_types().contains(&tasks_type));
4740 assert!(schema.0.common_types().all(|ct| ct.name_loc.is_some()));
4741 assert!(schema.0.common_types().all(|ct| ct.type_loc.is_some()));
4742
4743 let tasks_type = ValidatorCommonType {
4744 name: "T".into(),
4745 name_loc: None,
4746 type_loc: None,
4747 };
4748 assert!(schema.0.common_types().contains(&tasks_type));
4749
4750 let et = ast::EntityType::EntityType(ast::Name::from_normalized_str("List").unwrap());
4751 let et = schema.0.get_entity_type(&et).unwrap();
4752 let attrs = et.attributes();
4753
4754 // Assert that attributes that are resolved from common types still get source locations
4755 let t = attrs.get_attr("tasks").unwrap();
4756 assert!(t.loc.is_some());
4757 assert_matches!(&t.attr_type, cedar_policy_validator::types::Type::Set { ref element_type } => {
4758 let el = *element_type.clone().unwrap();
4759 assert_matches!(el, Type::EntityOrRecord(EntityRecordKind::Record { attrs, .. }) => {
4760 assert!(attrs.get_attr("name").unwrap().loc.is_some());
4761 assert!(attrs.get_attr("id").unwrap().loc.is_some());
4762 assert!(attrs.get_attr("state").unwrap().loc.is_some());
4763 });
4764 });
4765 }
4766
4767 #[cfg(feature = "extended-schema")]
4768 #[test]
4769 fn namespace_extended() {
4770 let schema = schema();
4771 assert_eq!(schema.0.namespaces().collect::<HashSet<_>>().len(), 1);
4772 let default_namespace = schema.0.namespaces().last().unwrap();
4773 assert_eq!(default_namespace.name, SmolStr::from("__cedar"));
4774 assert!(default_namespace.name_loc.is_none());
4775 assert!(default_namespace.def_loc.is_none());
4776 }
4777
4778 #[test]
4779 fn empty_schema_principals_and_resources() {
4780 let empty: Schema = "".parse().unwrap();
4781 assert!(empty.principals().next().is_none());
4782 assert!(empty.resources().next().is_none());
4783 }
4784
4785 #[test]
4786 fn resources() {
4787 let schema = schema();
4788 let resources = schema.resources().cloned().collect::<HashSet<_>>();
4789 let expected: HashSet<EntityTypeName> = HashSet::from([
4790 "List".parse().unwrap(),
4791 "Application".parse().unwrap(),
4792 "CoolList".parse().unwrap(),
4793 ]);
4794 assert_eq!(resources, expected);
4795 assert!(resources.iter().all(|ety| ety.0.loc().is_some()));
4796 }
4797
4798 #[test]
4799 fn principals_for_action() {
4800 let schema = schema();
4801 let delete_list: EntityUid = r#"Action::"DeleteList""#.parse().unwrap();
4802 let delete_user: EntityUid = r#"Action::"DeleteUser""#.parse().unwrap();
4803 let got = schema
4804 .principals_for_action(&delete_list)
4805 .unwrap()
4806 .cloned()
4807 .collect::<Vec<_>>();
4808 assert_eq!(got, vec!["User".parse().unwrap()]);
4809 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
4810 assert!(schema.principals_for_action(&delete_user).is_none());
4811 }
4812
4813 #[test]
4814 fn resources_for_action() {
4815 let schema = schema();
4816 let delete_list: EntityUid = r#"Action::"DeleteList""#.parse().unwrap();
4817 let delete_user: EntityUid = r#"Action::"DeleteUser""#.parse().unwrap();
4818 let create_list: EntityUid = r#"Action::"CreateList""#.parse().unwrap();
4819 let get_list: EntityUid = r#"Action::"GetList""#.parse().unwrap();
4820 let got = schema
4821 .resources_for_action(&delete_list)
4822 .unwrap()
4823 .cloned()
4824 .collect::<Vec<_>>();
4825 assert_eq!(got, vec!["List".parse().unwrap()]);
4826 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
4827 let got = schema
4828 .resources_for_action(&create_list)
4829 .unwrap()
4830 .cloned()
4831 .collect::<Vec<_>>();
4832 assert_eq!(got, vec!["Application".parse().unwrap()]);
4833 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
4834 let got = schema
4835 .resources_for_action(&get_list)
4836 .unwrap()
4837 .cloned()
4838 .collect::<HashSet<_>>();
4839 assert_eq!(
4840 got,
4841 HashSet::from(["List".parse().unwrap(), "CoolList".parse().unwrap()])
4842 );
4843 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
4844 assert!(schema.principals_for_action(&delete_user).is_none());
4845 }
4846
4847 #[test]
4848 fn principal_parents() {
4849 let schema = schema();
4850 let user: EntityTypeName = "User".parse().unwrap();
4851 let parents = schema
4852 .ancestors(&user)
4853 .unwrap()
4854 .cloned()
4855 .collect::<HashSet<_>>();
4856 assert!(parents.iter().all(|ety| ety.0.loc().is_some()));
4857 let expected = HashSet::from(["Team".parse().unwrap(), "Application".parse().unwrap()]);
4858 assert_eq!(parents, expected);
4859 let parents = schema
4860 .ancestors(&"List".parse().unwrap())
4861 .unwrap()
4862 .cloned()
4863 .collect::<HashSet<_>>();
4864 assert!(parents.iter().all(|ety| ety.0.loc().is_some()));
4865 let expected = HashSet::from(["Application".parse().unwrap()]);
4866 assert_eq!(parents, expected);
4867 assert!(schema.ancestors(&"Foo".parse().unwrap()).is_none());
4868 let parents = schema
4869 .ancestors(&"CoolList".parse().unwrap())
4870 .unwrap()
4871 .cloned()
4872 .collect::<HashSet<_>>();
4873 assert!(parents.iter().all(|ety| ety.0.loc().is_some()));
4874 let expected = HashSet::from([]);
4875 assert_eq!(parents, expected);
4876 }
4877
4878 #[test]
4879 fn action_groups() {
4880 let schema = schema();
4881 let groups = schema.action_groups().cloned().collect::<HashSet<_>>();
4882 let expected = ["Read", "Write", "Create"]
4883 .into_iter()
4884 .map(|ty| format!("Action::\"{ty}\"").parse().unwrap())
4885 .collect::<HashSet<EntityUid>>();
4886 #[cfg(feature = "extended-schema")]
4887 assert!(groups.iter().all(|ety| ety.0.loc().is_some()));
4888 assert_eq!(groups, expected);
4889 }
4890
4891 #[test]
4892 fn actions() {
4893 let schema = schema();
4894 let actions = schema.actions().cloned().collect::<HashSet<_>>();
4895 let expected = [
4896 "Read",
4897 "Write",
4898 "Create",
4899 "DeleteList",
4900 "EditShare",
4901 "UpdateList",
4902 "CreateTask",
4903 "UpdateTask",
4904 "DeleteTask",
4905 "GetList",
4906 "GetLists",
4907 "CreateList",
4908 ]
4909 .into_iter()
4910 .map(|ty| format!("Action::\"{ty}\"").parse().unwrap())
4911 .collect::<HashSet<EntityUid>>();
4912 assert_eq!(actions, expected);
4913 #[cfg(feature = "extended-schema")]
4914 assert!(actions.iter().all(|ety| ety.0.loc().is_some()));
4915 }
4916
4917 #[test]
4918 fn entities() {
4919 let schema = schema();
4920 let entities = schema.entity_types().cloned().collect::<HashSet<_>>();
4921 let expected = ["List", "Application", "User", "CoolList", "Team"]
4922 .into_iter()
4923 .map(|ty| ty.parse().unwrap())
4924 .collect::<HashSet<EntityTypeName>>();
4925 assert_eq!(entities, expected);
4926 }
4927}
4928
4929#[cfg(test)]
4930mod test_access_namespace {
4931 use super::*;
4932
4933 fn schema() -> Schema {
4934 let src = r#"
4935 namespace Foo {
4936 type Task = {
4937 "id": Long,
4938 "name": String,
4939 "state": String,
4940};
4941
4942type Tasks = Set<Task>;
4943entity List in [Application] = {
4944 "editors": Team,
4945 "name": String,
4946 "owner": User,
4947 "readers": Team,
4948 "tasks": Tasks,
4949};
4950entity Application;
4951entity User in [Team, Application] = {
4952 "joblevel": Long,
4953 "location": String,
4954};
4955
4956entity CoolList;
4957
4958entity Team in [Team, Application];
4959
4960action Read, Write, Create;
4961
4962action DeleteList, EditShare, UpdateList, CreateTask, UpdateTask, DeleteTask in Write appliesTo {
4963 principal: [User],
4964 resource : [List]
4965};
4966
4967action GetList in Read appliesTo {
4968 principal : [User],
4969 resource : [List, CoolList]
4970};
4971
4972action GetLists in Read appliesTo {
4973 principal : [User],
4974 resource : [Application]
4975};
4976
4977action CreateList in Create appliesTo {
4978 principal : [User],
4979 resource : [Application]
4980};
4981 }
4982
4983 "#;
4984
4985 src.parse().unwrap()
4986 }
4987
4988 #[test]
4989 fn principals() {
4990 let schema = schema();
4991 let principals = schema.principals().collect::<HashSet<_>>();
4992 assert_eq!(principals.len(), 1);
4993 let user: EntityTypeName = "Foo::User".parse().unwrap();
4994 assert!(principals.contains(&user));
4995 let principals = schema.principals().collect::<Vec<_>>();
4996 assert!(principals.len() > 1);
4997 assert!(principals.iter().all(|ety| **ety == user));
4998 assert!(principals.iter().all(|ety| ety.0.loc().is_some()));
4999 }
5000
5001 #[test]
5002 fn empty_schema_principals_and_resources() {
5003 let empty: Schema = "".parse().unwrap();
5004 assert!(empty.principals().next().is_none());
5005 assert!(empty.resources().next().is_none());
5006 }
5007
5008 #[test]
5009 fn resources() {
5010 let schema = schema();
5011 let resources = schema.resources().cloned().collect::<HashSet<_>>();
5012 let expected: HashSet<EntityTypeName> = HashSet::from([
5013 "Foo::List".parse().unwrap(),
5014 "Foo::Application".parse().unwrap(),
5015 "Foo::CoolList".parse().unwrap(),
5016 ]);
5017 assert_eq!(resources, expected);
5018 assert!(resources.iter().all(|ety| ety.0.loc().is_some()));
5019 }
5020
5021 #[test]
5022 fn principals_for_action() {
5023 let schema = schema();
5024 let delete_list: EntityUid = r#"Foo::Action::"DeleteList""#.parse().unwrap();
5025 let delete_user: EntityUid = r#"Foo::Action::"DeleteUser""#.parse().unwrap();
5026 let got = schema
5027 .principals_for_action(&delete_list)
5028 .unwrap()
5029 .cloned()
5030 .collect::<Vec<_>>();
5031 assert_eq!(got, vec!["Foo::User".parse().unwrap()]);
5032 assert!(schema.principals_for_action(&delete_user).is_none());
5033 }
5034
5035 #[test]
5036 fn resources_for_action() {
5037 let schema = schema();
5038 let delete_list: EntityUid = r#"Foo::Action::"DeleteList""#.parse().unwrap();
5039 let delete_user: EntityUid = r#"Foo::Action::"DeleteUser""#.parse().unwrap();
5040 let create_list: EntityUid = r#"Foo::Action::"CreateList""#.parse().unwrap();
5041 let get_list: EntityUid = r#"Foo::Action::"GetList""#.parse().unwrap();
5042 let got = schema
5043 .resources_for_action(&delete_list)
5044 .unwrap()
5045 .cloned()
5046 .collect::<Vec<_>>();
5047 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
5048
5049 assert_eq!(got, vec!["Foo::List".parse().unwrap()]);
5050 let got = schema
5051 .resources_for_action(&create_list)
5052 .unwrap()
5053 .cloned()
5054 .collect::<Vec<_>>();
5055 assert_eq!(got, vec!["Foo::Application".parse().unwrap()]);
5056 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
5057
5058 let got = schema
5059 .resources_for_action(&get_list)
5060 .unwrap()
5061 .cloned()
5062 .collect::<HashSet<_>>();
5063 assert_eq!(
5064 got,
5065 HashSet::from([
5066 "Foo::List".parse().unwrap(),
5067 "Foo::CoolList".parse().unwrap()
5068 ])
5069 );
5070 assert!(schema.principals_for_action(&delete_user).is_none());
5071 }
5072
5073 #[test]
5074 fn principal_parents() {
5075 let schema = schema();
5076 let user: EntityTypeName = "Foo::User".parse().unwrap();
5077 let parents = schema
5078 .ancestors(&user)
5079 .unwrap()
5080 .cloned()
5081 .collect::<HashSet<_>>();
5082 let expected = HashSet::from([
5083 "Foo::Team".parse().unwrap(),
5084 "Foo::Application".parse().unwrap(),
5085 ]);
5086 assert_eq!(parents, expected);
5087 let parents = schema
5088 .ancestors(&"Foo::List".parse().unwrap())
5089 .unwrap()
5090 .cloned()
5091 .collect::<HashSet<_>>();
5092 let expected = HashSet::from(["Foo::Application".parse().unwrap()]);
5093 assert_eq!(parents, expected);
5094 assert!(schema.ancestors(&"Foo::Foo".parse().unwrap()).is_none());
5095 let parents = schema
5096 .ancestors(&"Foo::CoolList".parse().unwrap())
5097 .unwrap()
5098 .cloned()
5099 .collect::<HashSet<_>>();
5100 let expected = HashSet::from([]);
5101 assert_eq!(parents, expected);
5102 }
5103
5104 #[test]
5105 fn action_groups() {
5106 let schema = schema();
5107 let groups = schema.action_groups().cloned().collect::<HashSet<_>>();
5108 let expected = ["Read", "Write", "Create"]
5109 .into_iter()
5110 .map(|ty| format!("Foo::Action::\"{ty}\"").parse().unwrap())
5111 .collect::<HashSet<EntityUid>>();
5112 assert_eq!(groups, expected);
5113 }
5114
5115 #[test]
5116 fn actions() {
5117 let schema = schema();
5118 let actions = schema.actions().cloned().collect::<HashSet<_>>();
5119 let expected = [
5120 "Read",
5121 "Write",
5122 "Create",
5123 "DeleteList",
5124 "EditShare",
5125 "UpdateList",
5126 "CreateTask",
5127 "UpdateTask",
5128 "DeleteTask",
5129 "GetList",
5130 "GetLists",
5131 "CreateList",
5132 ]
5133 .into_iter()
5134 .map(|ty| format!("Foo::Action::\"{ty}\"").parse().unwrap())
5135 .collect::<HashSet<EntityUid>>();
5136 assert_eq!(actions, expected);
5137 }
5138
5139 #[test]
5140 fn entities() {
5141 let schema = schema();
5142 let entities = schema.entity_types().cloned().collect::<HashSet<_>>();
5143 let expected = [
5144 "Foo::List",
5145 "Foo::Application",
5146 "Foo::User",
5147 "Foo::CoolList",
5148 "Foo::Team",
5149 ]
5150 .into_iter()
5151 .map(|ty| ty.parse().unwrap())
5152 .collect::<HashSet<EntityTypeName>>();
5153 assert_eq!(entities, expected);
5154 }
5155
5156 #[test]
5157 fn test_request_context() {
5158 // Create a context with some test data
5159 let context =
5160 Context::from_json_str(r#"{"testKey": "testValue", "numKey": 42}"#, None).unwrap();
5161
5162 // Create entity UIDs for the request
5163 let principal: EntityUid = "User::\"alice\"".parse().unwrap();
5164 let action: EntityUid = "Action::\"view\"".parse().unwrap();
5165 let resource: EntityUid = "Resource::\"doc123\"".parse().unwrap();
5166
5167 // Create the request
5168 let request = Request::new(
5169 principal, action, resource, context, None, // no schema validation for this test
5170 )
5171 .unwrap();
5172
5173 // Test context() method
5174 let retrieved_context = request.context().expect("Context should be present");
5175
5176 // Test get() method on the retrieved context
5177 assert!(retrieved_context.get("testKey").is_some());
5178 assert!(retrieved_context.get("numKey").is_some());
5179 assert!(retrieved_context.get("nonexistent").is_none());
5180 }
5181
5182 #[cfg(feature = "extended-schema")]
5183 #[test]
5184 fn namespace_extended() {
5185 let schema = schema();
5186 assert_eq!(schema.0.namespaces().collect::<HashSet<_>>().len(), 2);
5187 let default_namespace = schema
5188 .0
5189 .namespaces()
5190 .filter(|n| n.name == *"__cedar")
5191 .last()
5192 .unwrap();
5193 assert!(default_namespace.name_loc.is_none());
5194 assert!(default_namespace.def_loc.is_none());
5195
5196 let default_namespace = schema
5197 .0
5198 .namespaces()
5199 .filter(|n| n.name == *"Foo")
5200 .last()
5201 .unwrap();
5202 assert!(default_namespace.name_loc.is_some());
5203 assert!(default_namespace.def_loc.is_some());
5204 }
5205}
5206
5207/// Given a schema and policy set, compute an entity manifest.
5208///
5209/// The policies must validate against the schema in strict mode,
5210/// otherwise an error is returned.
5211/// The manifest describes the data required to answer requests
5212/// for each action.
5213#[doc = include_str!("../experimental_warning.md")]
5214#[cfg(feature = "entity-manifest")]
5215pub fn compute_entity_manifest(
5216 schema: &Schema,
5217 pset: &PolicySet,
5218) -> Result<EntityManifest, EntityManifestError> {
5219 entity_manifest::compute_entity_manifest(&schema.0, &pset.ast).map_err(std::convert::Into::into)
5220}