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