cedar_policy/api.rs
1/*
2 * Copyright 2022-2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
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)]
23pub use ast::Effect;
24pub use authorizer::Decision;
25use cedar_policy_core::ast;
26use cedar_policy_core::ast::{
27 ContextCreationError, ExprConstructionError, RestrictedExprParseError,
28}; // `ContextCreationError` is unsuitable for `pub use` because it contains internal types like `RestrictedExpr`
29use cedar_policy_core::authorizer;
30pub use cedar_policy_core::authorizer::AuthorizationError;
31use cedar_policy_core::entities::{
32 self, ContextJsonDeserializationError, ContextSchema, Dereference, JsonDeserializationError,
33 JsonDeserializationErrorContext,
34};
35use cedar_policy_core::est;
36use cedar_policy_core::evaluator::Evaluator;
37pub use cedar_policy_core::evaluator::{EvaluationError, EvaluationErrorKind};
38pub use cedar_policy_core::extensions;
39use cedar_policy_core::extensions::Extensions;
40use cedar_policy_core::parser;
41pub use cedar_policy_core::parser::err::ParseErrors;
42use cedar_policy_core::parser::SourceInfo;
43use cedar_policy_core::FromNormalizedStr;
44use cedar_policy_validator::RequestValidationError; // this type is unsuitable for `pub use` because it contains internal types like `EntityUID` and `EntityType`
45pub use cedar_policy_validator::{
46 TypeErrorKind, UnsupportedFeature, ValidationErrorKind, ValidationWarningKind,
47};
48use itertools::Itertools;
49use ref_cast::RefCast;
50use serde::{Deserialize, Serialize};
51use smol_str::SmolStr;
52use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
53use std::convert::Infallible;
54use std::str::FromStr;
55use thiserror::Error;
56
57/// Identifier for a Template slot
58#[repr(transparent)]
59#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, Hash, RefCast)]
60pub struct SlotId(ast::SlotId);
61
62impl SlotId {
63 /// Get the slot for `principal`
64 pub fn principal() -> Self {
65 Self(ast::SlotId::principal())
66 }
67
68 /// Get the slot for `resource`
69 pub fn resource() -> Self {
70 Self(ast::SlotId::resource())
71 }
72}
73
74impl std::fmt::Display for SlotId {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 write!(f, "{}", self.0)
77 }
78}
79
80impl From<ast::SlotId> for SlotId {
81 fn from(a: ast::SlotId) -> Self {
82 Self(a)
83 }
84}
85
86impl From<SlotId> for ast::SlotId {
87 fn from(s: SlotId) -> Self {
88 s.0
89 }
90}
91
92/// Entity datatype
93// INVARIANT(UidOfEntityNotUnspecified): The `EntityUid` of an `Entity` cannot be unspecified
94#[repr(transparent)]
95#[derive(Debug, Clone, PartialEq, Eq, RefCast)]
96pub struct Entity(ast::Entity);
97
98impl Entity {
99 /// Create a new `Entity` with this Uid, attributes, and parents.
100 ///
101 /// Attribute values are specified here as "restricted expressions".
102 /// See docs on `RestrictedExpression`
103 /// ```
104 /// # use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid, RestrictedExpression};
105 /// # use std::collections::{HashMap, HashSet};
106 /// # use std::str::FromStr;
107 /// let eid = EntityId::from_str("alice").unwrap();
108 /// let type_name = EntityTypeName::from_str("User").unwrap();
109 /// let euid = EntityUid::from_type_name_and_id(type_name, eid);
110 /// let attrs = HashMap::from([
111 /// ("age".to_string(), RestrictedExpression::from_str("21").unwrap()),
112 /// ("department".to_string(), RestrictedExpression::from_str("\"CS\"").unwrap()),
113 /// ]);
114 /// let parent_eid = EntityId::from_str("admin").unwrap();
115 /// let parent_type_name = EntityTypeName::from_str("Group").unwrap();
116 /// let parent_euid = EntityUid::from_type_name_and_id(parent_type_name, parent_eid);
117 /// let parents = HashSet::from([parent_euid]);
118 /// let entity = Entity::new(euid, attrs, parents);
119 ///```
120 pub fn new(
121 uid: EntityUid,
122 attrs: HashMap<String, RestrictedExpression>,
123 parents: HashSet<EntityUid>,
124 ) -> Result<Self, EntityAttrEvaluationError> {
125 // note that we take a "parents" parameter here; we will compute TC when
126 // the `Entities` object is created
127 // INVARIANT(UidOfEntityNotUnspecified): by invariant on `EntityUid`
128 Ok(Self(ast::Entity::new(
129 uid.0,
130 attrs
131 .into_iter()
132 .map(|(k, v)| (SmolStr::from(k), v.0))
133 .collect(),
134 parents.into_iter().map(|uid| uid.0).collect(),
135 &Extensions::all_available(),
136 )?))
137 }
138
139 /// Create a new `Entity` with no attributes.
140 ///
141 /// Unlike [`Entity::new()`], this constructor cannot error.
142 /// (The only source of errors in `Entity::new()` are attributes.)
143 pub fn new_no_attrs(uid: EntityUid, parents: HashSet<EntityUid>) -> Self {
144 // note that we take a "parents" parameter here; we will compute TC when
145 // the `Entities` object is created
146 // INVARIANT(UidOfEntityNotUnspecified): by invariant on `EntityUid`
147 Self(ast::Entity::new_with_attr_partial_value(
148 uid.0,
149 HashMap::new(),
150 parents.into_iter().map(|uid| uid.0).collect(),
151 ))
152 }
153
154 /// Create a new `Entity` with this Uid, no attributes, and no parents.
155 /// ```
156 /// use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid};
157 /// # use std::str::FromStr;
158 /// let eid = EntityId::from_str("alice").unwrap();
159 /// let type_name = EntityTypeName::from_str("User").unwrap();
160 /// let euid = EntityUid::from_type_name_and_id(type_name, eid);
161 /// let alice = Entity::with_uid(euid);
162 /// # cool_asserts::assert_matches!(alice.attr("age"), None);
163 /// ```
164 pub fn with_uid(uid: EntityUid) -> Self {
165 // INVARIANT(UidOfEntityNotUnspecified): by invariant on `EntityUid`
166 Self(ast::Entity::with_uid(uid.0))
167 }
168
169 /// Get the Uid of this entity
170 /// ```
171 /// # use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid};
172 /// # use std::str::FromStr;
173 /// # let eid = EntityId::from_str("alice").unwrap();
174 /// let type_name = EntityTypeName::from_str("User").unwrap();
175 /// let euid = EntityUid::from_type_name_and_id(type_name, eid);
176 /// let alice = Entity::with_uid(euid.clone());
177 /// assert_eq!(alice.uid(), euid);
178 /// ```
179 pub fn uid(&self) -> EntityUid {
180 // INVARIANT: By invariant on self and `EntityUid`: Our Uid can't be unspecified
181 EntityUid(self.0.uid())
182 }
183
184 /// Get the value for the given attribute, or `None` if not present.
185 ///
186 /// This can also return Some(Err) if the attribute is not a value (i.e., is
187 /// unknown due to partial evaluation).
188 /// ```
189 /// use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid, EvalResult,
190 /// RestrictedExpression};
191 /// use std::collections::{HashMap, HashSet};
192 /// use std::str::FromStr;
193 /// let eid = EntityId::from_str("alice").unwrap();
194 /// let type_name = EntityTypeName::from_str("User").unwrap();
195 /// let euid = EntityUid::from_type_name_and_id(type_name, eid);
196 /// let attrs = HashMap::from([
197 /// ("age".to_string(), RestrictedExpression::from_str("21").unwrap()),
198 /// ("department".to_string(), RestrictedExpression::from_str("\"CS\"").unwrap()),
199 /// ]);
200 /// let entity = Entity::new(euid, attrs, HashSet::new()).unwrap();
201 /// assert_eq!(entity.attr("age").unwrap().unwrap(), EvalResult::Long(21));
202 /// assert_eq!(entity.attr("department").unwrap().unwrap(), EvalResult::String("CS".to_string()));
203 /// assert!(entity.attr("foo").is_none());
204 /// ```
205 pub fn attr(&self, attr: &str) -> Option<Result<EvalResult, impl std::error::Error>> {
206 let v = match ast::Value::try_from(self.0.get(attr)?.clone()) {
207 Ok(v) => v,
208 Err(e) => return Some(Err(e)),
209 };
210 Some(Ok(EvalResult::from(v)))
211 }
212}
213
214impl std::fmt::Display for Entity {
215 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216 write!(f, "{}", self.0)
217 }
218}
219
220/// Represents an entity hierarchy, and allows looking up `Entity` objects by
221/// Uid.
222#[repr(transparent)]
223#[derive(Debug, Clone, Default, PartialEq, Eq, RefCast)]
224pub struct Entities(pub(crate) entities::Entities);
225
226pub use entities::EntitiesError;
227
228impl Entities {
229 /// Create a fresh `Entities` with no entities
230 /// ```
231 /// use cedar_policy::Entities;
232 /// let entities = Entities::empty();
233 /// ```
234 pub fn empty() -> Self {
235 Self(entities::Entities::new())
236 }
237
238 /// Get the `Entity` with the given Uid, if any
239 pub fn get(&self, uid: &EntityUid) -> Option<&Entity> {
240 match self.0.entity(&uid.0) {
241 Dereference::Residual(_) | Dereference::NoSuchEntity => None,
242 Dereference::Data(e) => Some(Entity::ref_cast(e)),
243 }
244 }
245
246 /// Transform the store into a partial store, where
247 /// attempting to dereference a non-existent `EntityUID` results in
248 /// a residual instead of an error.
249 #[must_use]
250 #[cfg(feature = "partial-eval")]
251 pub fn partial(self) -> Self {
252 Self(self.0.partial())
253 }
254
255 /// Iterate over the `Entity`'s in the `Entities`
256 pub fn iter(&self) -> impl Iterator<Item = &Entity> {
257 self.0.iter().map(Entity::ref_cast)
258 }
259
260 /// Create an `Entities` object with the given entities.
261 ///
262 /// `schema` represents a source of `Action` entities, which will be added
263 /// to the entities provided.
264 /// (If any `Action` entities are present in the provided entities, and a
265 /// `schema` is also provided, each `Action` entity in the provided entities
266 /// must exactly match its definition in the schema or an error is
267 /// returned.)
268 ///
269 /// If a `schema` is present, this function will also ensure that the
270 /// produced entities fully conform to the `schema` -- for instance, it will
271 /// error if attributes have the wrong types (e.g., string instead of
272 /// integer), or if required attributes are missing or superfluous
273 /// attributes are provided.
274 pub fn from_entities(
275 entities: impl IntoIterator<Item = Entity>,
276 schema: Option<&Schema>,
277 ) -> Result<Self, entities::EntitiesError> {
278 entities::Entities::from_entities(
279 entities.into_iter().map(|e| e.0),
280 schema
281 .map(|s| cedar_policy_validator::CoreSchema::new(&s.0))
282 .as_ref(),
283 entities::TCComputation::ComputeNow,
284 Extensions::all_available(),
285 )
286 .map(Entities)
287 }
288
289 /// Add all of the [`Entity`]s in the collection to this [`Entities`]
290 /// structure, re-computing the transitive closure.
291 ///
292 /// If a `schema` is provided, this method will ensure that the added
293 /// entities fully conform to the schema -- for instance, it will error if
294 /// attributes have the wrong types (e.g., string instead of integer), or if
295 /// required attributes are missing or superfluous attributes are provided.
296 /// (This method will not add action entities from the `schema`.)
297 ///
298 /// Re-computing the transitive closure can be expensive, so it is advised
299 /// to not call this method in a loop.
300 pub fn add_entities(
301 self,
302 entities: impl IntoIterator<Item = Entity>,
303 schema: Option<&Schema>,
304 ) -> Result<Self, EntitiesError> {
305 Ok(Self(
306 self.0.add_entities(
307 entities.into_iter().map(|e| e.0),
308 schema
309 .map(|s| cedar_policy_validator::CoreSchema::new(&s.0))
310 .as_ref(),
311 entities::TCComputation::ComputeNow,
312 Extensions::all_available(),
313 )?,
314 ))
315 }
316
317 /// Parse an entities JSON file (in [&str] form) and add them into this
318 /// [`Entities`] structure, re-computing the transitive closure
319 ///
320 /// If a `schema` is provided, this will inform the parsing: for instance, it
321 /// will allow `__entity` and `__extn` escapes to be implicit.
322 /// This method will also ensure that the added entities fully conform to the
323 /// schema -- for instance, it will error if attributes have the wrong types
324 /// (e.g., string instead of integer), or if required attributes are missing
325 /// or superfluous attributes are provided.
326 /// (This method will not add action entities from the `schema`.)
327 ///
328 /// Re-computing the transitive closure can be expensive, so it is advised
329 /// to not call this method in a loop.
330 pub fn add_entities_from_json_str(
331 self,
332 json: &str,
333 schema: Option<&Schema>,
334 ) -> Result<Self, EntitiesError> {
335 let schema = schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0));
336 let eparser = entities::EntityJsonParser::new(
337 schema.as_ref(),
338 Extensions::all_available(),
339 entities::TCComputation::ComputeNow,
340 );
341 let new_entities = eparser.iter_from_json_str(json)?;
342 Ok(Self(self.0.add_entities(
343 new_entities,
344 schema.as_ref(),
345 entities::TCComputation::ComputeNow,
346 Extensions::all_available(),
347 )?))
348 }
349
350 /// Parse an entities JSON file (in [`serde_json::Value`] form) and add them
351 /// into this [`Entities`] structure, re-computing the transitive closure
352 ///
353 /// If a `schema` is provided, this will inform the parsing: for instance, it
354 /// will allow `__entity` and `__extn` escapes to be implicit.
355 /// This method will also ensure that the added entities fully conform to the
356 /// schema -- for instance, it will error if attributes have the wrong types
357 /// (e.g., string instead of integer), or if required attributes are missing
358 /// or superfluous attributes are provided.
359 /// (This method will not add action entities from the `schema`.)
360 ///
361 /// Re-computing the transitive closure can be expensive, so it is advised
362 /// to not call this method in a loop.
363 pub fn add_entities_from_json_value(
364 self,
365 json: serde_json::Value,
366 schema: Option<&Schema>,
367 ) -> Result<Self, EntitiesError> {
368 let schema = schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0));
369 let eparser = entities::EntityJsonParser::new(
370 schema.as_ref(),
371 Extensions::all_available(),
372 entities::TCComputation::ComputeNow,
373 );
374 let new_entities = eparser.iter_from_json_value(json)?;
375 Ok(Self(self.0.add_entities(
376 new_entities,
377 schema.as_ref(),
378 entities::TCComputation::ComputeNow,
379 Extensions::all_available(),
380 )?))
381 }
382
383 /// Parse an entities JSON file (in [`std::io::Read`] form) and add them
384 /// into this [`Entities`] structure, re-computing the transitive closure
385 ///
386 /// If a `schema` is provided, this will inform the parsing: for instance, it
387 /// will allow `__entity` and `__extn` escapes to be implicit.
388 /// This method will also ensure that the added entities fully conform to the
389 /// schema -- for instance, it will error if attributes have the wrong types
390 /// (e.g., string instead of integer), or if required attributes are missing
391 /// or superfluous attributes are provided.
392 /// (This method will not add action entities from the `schema`.)
393 ///
394 /// Re-computing the transitive closure can be expensive, so it is advised
395 /// to not call this method in a loop.
396 pub fn add_entities_from_json_file(
397 self,
398 json: impl std::io::Read,
399 schema: Option<&Schema>,
400 ) -> Result<Self, EntitiesError> {
401 let schema = schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0));
402 let eparser = entities::EntityJsonParser::new(
403 schema.as_ref(),
404 Extensions::all_available(),
405 entities::TCComputation::ComputeNow,
406 );
407 let new_entities = eparser.iter_from_json_file(json)?;
408 Ok(Self(self.0.add_entities(
409 new_entities,
410 schema.as_ref(),
411 entities::TCComputation::ComputeNow,
412 Extensions::all_available(),
413 )?))
414 }
415
416 /// Parse an entities JSON file (in `&str` form) into an `Entities` object
417 ///
418 /// `schema` represents a source of `Action` entities, which will be added
419 /// to the entities parsed from JSON.
420 /// (If any `Action` entities are present in the JSON, and a `schema` is
421 /// also provided, each `Action` entity in the JSON must exactly match its
422 /// definition in the schema or an error is returned.)
423 ///
424 /// If a `schema` is present, this will also inform the parsing: for
425 /// instance, it will allow `__entity` and `__extn` escapes to be implicit.
426 ///
427 /// Finally, if a `schema` is present, this function will ensure
428 /// that the produced entities fully conform to the `schema` -- for
429 /// instance, it will error if attributes have the wrong types (e.g., string
430 /// instead of integer), or if required attributes are missing or
431 /// superfluous attributes are provided.
432 ///
433 /// ```
434 /// use std::collections::HashMap;
435 /// use std::str::FromStr;
436 /// # use cedar_policy::{Entities, EntityId, EntityTypeName, EntityUid, EvalResult, Request,PolicySet};
437 /// let data =r#"
438 /// [
439 /// {
440 /// "uid": {"type":"User","id":"alice"},
441 /// "attrs": {
442 /// "age":19,
443 /// "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
444 /// },
445 /// "parents": [{"type":"Group","id":"admin"}]
446 /// },
447 /// {
448 /// "uid": {"type":"Groupd","id":"admin"},
449 /// "attrs": {},
450 /// "parents": []
451 /// }
452 /// ]
453 /// "#;
454 /// let entities = Entities::from_json_str(data, None).unwrap();
455 /// let eid = EntityId::from_str("alice").unwrap();
456 /// let type_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
457 /// let euid = EntityUid::from_type_name_and_id(type_name, eid);
458 /// let entity = entities.get(&euid).unwrap();
459 /// assert_eq!(entity.attr("age").unwrap().unwrap(), EvalResult::Long(19));
460 /// let ip = entity.attr("ip_addr").unwrap().unwrap();
461 /// assert_eq!(ip, EvalResult::ExtensionValue("10.0.1.101/32".to_string()));
462 /// ```
463 pub fn from_json_str(
464 json: &str,
465 schema: Option<&Schema>,
466 ) -> Result<Self, entities::EntitiesError> {
467 let schema = schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0));
468 let eparser = entities::EntityJsonParser::new(
469 schema.as_ref(),
470 Extensions::all_available(),
471 entities::TCComputation::ComputeNow,
472 );
473 eparser.from_json_str(json).map(Entities)
474 }
475
476 /// Parse an entities JSON file (in `serde_json::Value` form) into an
477 /// `Entities` object
478 ///
479 /// `schema` represents a source of `Action` entities, which will be added
480 /// to the entities parsed from JSON.
481 /// (If any `Action` entities are present in the JSON, and a `schema` is
482 /// also provided, each `Action` entity in the JSON must exactly match its
483 /// definition in the schema or an error is returned.)
484 ///
485 /// If a `schema` is present, this will also inform the parsing: for
486 /// instance, it will allow `__entity` and `__extn` escapes to be implicit.
487 ///
488 /// Finally, if a `schema` is present, this function will ensure
489 /// that the produced entities fully conform to the `schema` -- for
490 /// instance, it will error if attributes have the wrong types (e.g., string
491 /// instead of integer), or if required attributes are missing or
492 /// superfluous attributes are provided.
493 ///
494 /// ```
495 /// use std::collections::HashMap;
496 /// use std::str::FromStr;
497 /// # use cedar_policy::{Entities, EntityId, EntityTypeName, EntityUid, EvalResult, Request,PolicySet};
498 /// let data =serde_json::json!(
499 /// [
500 /// {
501 /// "uid": {"type":"User","id":"alice"},
502 /// "attrs": {
503 /// "age":19,
504 /// "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
505 /// },
506 /// "parents": [{"type":"Group","id":"admin"}]
507 /// },
508 /// {
509 /// "uid": {"type":"Groupd","id":"admin"},
510 /// "attrs": {},
511 /// "parents": []
512 /// }
513 /// ]
514 /// );
515 /// let entities = Entities::from_json_value(data, None).unwrap();
516 /// ```
517 pub fn from_json_value(
518 json: serde_json::Value,
519 schema: Option<&Schema>,
520 ) -> Result<Self, entities::EntitiesError> {
521 let schema = schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0));
522 let eparser = entities::EntityJsonParser::new(
523 schema.as_ref(),
524 Extensions::all_available(),
525 entities::TCComputation::ComputeNow,
526 );
527 eparser.from_json_value(json).map(Entities)
528 }
529
530 /// Parse an entities JSON file (in `std::io::Read` form) into an `Entities`
531 /// object
532 ///
533 /// `schema` represents a source of `Action` entities, which will be added
534 /// to the entities parsed from JSON.
535 /// (If any `Action` entities are present in the JSON, and a `schema` is
536 /// also provided, each `Action` entity in the JSON must exactly match its
537 /// definition in the schema or an error is returned.)
538 ///
539 /// If a `schema` is present, this will also inform the parsing: for
540 /// instance, it will allow `__entity` and `__extn` escapes to be implicit.
541 ///
542 /// Finally, if a `schema` is present, this function will ensure
543 /// that the produced entities fully conform to the `schema` -- for
544 /// instance, it will error if attributes have the wrong types (e.g., string
545 /// instead of integer), or if required attributes are missing or
546 /// superfluous attributes are provided.
547 pub fn from_json_file(
548 json: impl std::io::Read,
549 schema: Option<&Schema>,
550 ) -> Result<Self, entities::EntitiesError> {
551 let schema = schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0));
552 let eparser = entities::EntityJsonParser::new(
553 schema.as_ref(),
554 Extensions::all_available(),
555 entities::TCComputation::ComputeNow,
556 );
557 eparser.from_json_file(json).map(Entities)
558 }
559
560 /// Is entity `a` an ancestor of entity `b`?
561 /// Same semantics as `b in a` in the Cedar language
562 pub fn is_ancestor_of(&self, a: &EntityUid, b: &EntityUid) -> bool {
563 match self.0.entity(&b.0) {
564 Dereference::Data(b) => b.is_descendant_of(&a.0),
565 _ => a == b, // if b doesn't exist, `b in a` is only true if `b == a`
566 }
567 }
568
569 /// Get an iterator over the ancestors of the given Euid.
570 /// Returns `None` if the given `Euid` does not exist.
571 pub fn ancestors<'a>(
572 &'a self,
573 euid: &EntityUid,
574 ) -> Option<impl Iterator<Item = &'a EntityUid>> {
575 let entity = match self.0.entity(&euid.0) {
576 Dereference::Residual(_) | Dereference::NoSuchEntity => None,
577 Dereference::Data(e) => Some(e),
578 }?;
579 // Invariant: No way to write down the unspecified EntityUid, so no way to have ancestors that are unspecified
580 Some(entity.ancestors().map(EntityUid::ref_cast))
581 }
582
583 /// Dump an `Entities` object into an entities JSON file.
584 ///
585 /// The resulting JSON will be suitable for parsing in via
586 /// `from_json_*`, and will be parse-able even with no `Schema`.
587 ///
588 /// To read an `Entities` object from an entities JSON file, use
589 /// `from_json_file`.
590 pub fn write_to_json(
591 &self,
592 f: impl std::io::Write,
593 ) -> std::result::Result<(), entities::EntitiesError> {
594 self.0.write_to_json(f)
595 }
596}
597
598/// Authorizer object, which provides responses to authorization queries
599#[repr(transparent)]
600#[derive(Debug, RefCast)]
601pub struct Authorizer(authorizer::Authorizer);
602
603impl Default for Authorizer {
604 fn default() -> Self {
605 Self::new()
606 }
607}
608
609impl Authorizer {
610 /// Create a new `Authorizer`
611 ///
612 /// The authorizer uses the `stacker` crate to manage stack size and tries to use a sane default.
613 /// If the default is not right for you, you can try wrapping the authorizer or individual calls
614 /// to `is_authorized` in `stacker::grow`.
615 /// ```
616 /// # use cedar_policy::{Authorizer, Context, Entities, EntityId, EntityTypeName,
617 /// # EntityUid, Request,PolicySet};
618 /// # use std::str::FromStr;
619 /// # // create a request
620 /// # let p_eid = EntityId::from_str("alice").unwrap();
621 /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
622 /// # let p = EntityUid::from_type_name_and_id(p_name, p_eid);
623 /// #
624 /// # let a_eid = EntityId::from_str("view").unwrap();
625 /// # let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
626 /// # let a = EntityUid::from_type_name_and_id(a_name, a_eid);
627 /// #
628 /// # let r_eid = EntityId::from_str("trip").unwrap();
629 /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
630 /// # let r = EntityUid::from_type_name_and_id(r_name, r_eid);
631 /// #
632 /// # let c = Context::empty();
633 /// #
634 /// # let request: Request = Request::new(Some(p), Some(a), Some(r), c, None).unwrap();
635 /// #
636 /// # // create a policy
637 /// # let s = r#"permit(
638 /// # principal == User::"alice",
639 /// # action == Action::"view",
640 /// # resource == Album::"trip"
641 /// # )when{
642 /// # principal.ip_addr.isIpv4()
643 /// # };
644 /// # "#;
645 /// # let policy = PolicySet::from_str(s).expect("policy error");
646 /// # // create entities
647 /// # let e = r#"[
648 /// # {
649 /// # "uid": {"type":"User","id":"alice"},
650 /// # "attrs": {
651 /// # "age":19,
652 /// # "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
653 /// # },
654 /// # "parents": []
655 /// # }
656 /// # ]"#;
657 /// # let entities = Entities::from_json_str(e, None).expect("entity error");
658 /// let authorizer = Authorizer::new();
659 /// let r = authorizer.is_authorized(&request, &policy, &entities);
660 /// ```
661 pub fn new() -> Self {
662 Self(authorizer::Authorizer::new())
663 }
664
665 /// Returns an authorization response for `r` with respect to the given
666 /// `PolicySet` and `Entities`.
667 ///
668 /// The language spec and Dafny model give a precise definition of how this
669 /// is computed.
670 /// ```
671 /// use cedar_policy::{Authorizer,Context,Entities,EntityId,EntityTypeName,
672 /// EntityUid, Request,PolicySet};
673 /// use std::str::FromStr;
674 ///
675 /// // create a request
676 /// let p_eid = EntityId::from_str("alice").unwrap();
677 /// let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
678 /// let p = EntityUid::from_type_name_and_id(p_name, p_eid);
679 ///
680 /// let a_eid = EntityId::from_str("view").unwrap();
681 /// let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
682 /// let a = EntityUid::from_type_name_and_id(a_name, a_eid);
683 ///
684 /// let r_eid = EntityId::from_str("trip").unwrap();
685 /// let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
686 /// let r = EntityUid::from_type_name_and_id(r_name, r_eid);
687 ///
688 /// let c = Context::empty();
689 ///
690 /// let request: Request = Request::new(Some(p), Some(a), Some(r), c, None).unwrap();
691 ///
692 /// // create a policy
693 /// let s = r#"
694 /// permit (
695 /// principal == User::"alice",
696 /// action == Action::"view",
697 /// resource == Album::"trip"
698 /// )
699 /// when { principal.ip_addr.isIpv4() };
700 /// "#;
701 /// let policy = PolicySet::from_str(s).expect("policy error");
702
703 /// // create entities
704 /// let e = r#"[
705 /// {
706 /// "uid": {"type":"User","id":"alice"},
707 /// "attrs": {
708 /// "age":19,
709 /// "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
710 /// },
711 /// "parents": []
712 /// }
713 /// ]"#;
714 /// let entities = Entities::from_json_str(e, None).expect("entity error");
715 ///
716 /// let authorizer = Authorizer::new();
717 /// let r = authorizer.is_authorized(&request, &policy, &entities);
718 /// println!("{:?}", r);
719 /// ```
720 pub fn is_authorized(&self, r: &Request, p: &PolicySet, e: &Entities) -> Response {
721 self.0.is_authorized(r.0.clone(), &p.ast, &e.0).into()
722 }
723
724 /// A partially evaluated authorization request.
725 /// The Authorizer will attempt to make as much progress as possible in the presence of unknowns.
726 /// If the Authorizer can reach a response, it will return that response.
727 /// Otherwise, it will return a list of residual policies that still need to be evaluated.
728 #[cfg(feature = "partial-eval")]
729 pub fn is_authorized_partial(
730 &self,
731 query: &Request,
732 policy_set: &PolicySet,
733 entities: &Entities,
734 ) -> PartialResponse {
735 let response = self
736 .0
737 .is_authorized_core(query.0.clone(), &policy_set.ast, &entities.0);
738 match response {
739 authorizer::ResponseKind::FullyEvaluated(a) => PartialResponse::Concrete(a.into()),
740 authorizer::ResponseKind::Partial(p) => PartialResponse::Residual(p.into()),
741 }
742 }
743}
744
745/// Authorization response returned from the `Authorizer`
746#[derive(Debug, PartialEq, Eq, Clone)]
747pub struct Response {
748 /// Authorization decision
749 decision: Decision,
750 /// Diagnostics providing more information on how this decision was reached
751 diagnostics: Diagnostics,
752}
753
754/// Authorization response returned from `is_authorized_partial`.
755/// It can either be a full concrete response, or a residual response.
756#[cfg(feature = "partial-eval")]
757#[derive(Debug, PartialEq, Eq, Clone)]
758pub enum PartialResponse {
759 /// A full, concrete response.
760 Concrete(Response),
761 /// A residual response. Determining the concrete response requires further processing.
762 Residual(ResidualResponse),
763}
764
765/// A residual response obtained from `is_authorized_partial`.
766#[cfg(feature = "partial-eval")]
767#[derive(Debug, PartialEq, Eq, Clone)]
768pub struct ResidualResponse {
769 /// Residual policies
770 residuals: PolicySet,
771 /// Diagnostics
772 diagnostics: Diagnostics,
773}
774
775/// Diagnostics providing more information on how a `Decision` was reached
776#[derive(Debug, PartialEq, Eq, Clone)]
777pub struct Diagnostics {
778 /// `PolicyId`s of the policies that contributed to the decision.
779 /// If no policies applied to the request, this set will be empty.
780 reason: HashSet<PolicyId>,
781 /// Errors that occurred during authorization. The errors should be
782 /// treated as unordered, since policies may be evaluated in any order.
783 errors: Vec<AuthorizationError>,
784}
785
786impl From<authorizer::Diagnostics> for Diagnostics {
787 fn from(diagnostics: authorizer::Diagnostics) -> Self {
788 Self {
789 reason: diagnostics.reason.into_iter().map(PolicyId).collect(),
790 errors: diagnostics.errors,
791 }
792 }
793}
794
795impl Diagnostics {
796 /// Get the policies that contributed to the decision
797 /// ```
798 /// # use cedar_policy::{Authorizer, Context, Decision, Entities, EntityId, EntityTypeName,
799 /// # EntityUid, Request,PolicySet};
800 /// # use std::str::FromStr;
801 /// # // create a request
802 /// # let p_eid = EntityId::from_str("alice").unwrap();
803 /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
804 /// # let p = EntityUid::from_type_name_and_id(p_name, p_eid);
805 /// #
806 /// # let a_eid = EntityId::from_str("view").unwrap();
807 /// # let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
808 /// # let a = EntityUid::from_type_name_and_id(a_name, a_eid);
809 /// #
810 /// # let r_eid = EntityId::from_str("trip").unwrap();
811 /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
812 /// # let r = EntityUid::from_type_name_and_id(r_name, r_eid);
813 /// #
814 /// # let c = Context::empty();
815 /// #
816 /// # let request: Request = Request::new(Some(p), Some(a), Some(r), c, None).unwrap();
817 /// #
818 /// # // create a policy
819 /// # let s = r#"permit(
820 /// # principal == User::"alice",
821 /// # action == Action::"view",
822 /// # resource == Album::"trip"
823 /// # )when{
824 /// # principal.ip_addr.isIpv4()
825 /// # };
826 /// # "#;
827 /// # let policy = PolicySet::from_str(s).expect("policy error");
828 /// # // create entities
829 /// # let e = r#"[
830 /// # {
831 /// # "uid": {"type":"User","id":"alice"},
832 /// # "attrs": {
833 /// # "age":19,
834 /// # "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
835 /// # },
836 /// # "parents": []
837 /// # }
838 /// # ]"#;
839 /// # let entities = Entities::from_json_str(e, None).expect("entity error");
840 /// let authorizer = Authorizer::new();
841 /// let response = authorizer.is_authorized(&request, &policy, &entities);
842 /// match response.decision() {
843 /// Decision::Allow => println!("ALLOW"),
844 /// Decision::Deny => println!("DENY"),
845 /// }
846 /// println!("note: this decision was due to the following policies:");
847 /// for reason in response.diagnostics().reason() {
848 /// println!("{}", reason);
849 /// }
850 /// ```
851 pub fn reason(&self) -> impl Iterator<Item = &PolicyId> {
852 self.reason.iter()
853 }
854
855 /// Get the errors
856 /// ```
857 /// # use cedar_policy::{Authorizer, Context, Decision, Entities, EntityId, EntityTypeName,
858 /// # EntityUid, Request,PolicySet};
859 /// # use std::str::FromStr;
860 /// # // create a request
861 /// # let p_eid = EntityId::from_str("alice").unwrap();
862 /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
863 /// # let p = EntityUid::from_type_name_and_id(p_name, p_eid);
864 /// #
865 /// # let a_eid = EntityId::from_str("view").unwrap();
866 /// # let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
867 /// # let a = EntityUid::from_type_name_and_id(a_name, a_eid);
868 /// #
869 /// # let r_eid = EntityId::from_str("trip").unwrap();
870 /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
871 /// # let r = EntityUid::from_type_name_and_id(r_name, r_eid);
872 /// #
873 /// # let c = Context::empty();
874 /// #
875 /// # let request: Request = Request::new(Some(p), Some(a), Some(r), c, None).unwrap();
876 /// #
877 /// # // create a policy
878 /// # let s = r#"permit(
879 /// # principal == User::"alice",
880 /// # action == Action::"view",
881 /// # resource == Album::"trip"
882 /// # )when{
883 /// # principal.ip_addr.isIpv4()
884 /// # };
885 /// # "#;
886 /// # let policy = PolicySet::from_str(s).expect("policy error");
887 /// # // create entities
888 /// # let e = r#"[
889 /// # {
890 /// # "uid": {"type":"User","id":"alice"},
891 /// # "attrs": {
892 /// # "age":19,
893 /// # "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
894 /// # },
895 /// # "parents": []
896 /// # }
897 /// # ]"#;
898 /// # let entities = Entities::from_json_str(e, None).expect("entity error");
899 /// let authorizer = Authorizer::new();
900 /// let response = authorizer.is_authorized(&request, &policy, &entities);
901 /// match response.decision() {
902 /// Decision::Allow => println!("ALLOW"),
903 /// Decision::Deny => println!("DENY"),
904 /// }
905 /// for err in response.diagnostics().errors() {
906 /// println!("{}", err);
907 /// }
908 /// ```
909 pub fn errors(&self) -> impl Iterator<Item = &AuthorizationError> + '_ {
910 self.errors.iter()
911 }
912}
913
914impl Response {
915 /// Create a new `Response`
916 pub fn new(
917 decision: Decision,
918 reason: HashSet<PolicyId>,
919 errors: Vec<AuthorizationError>,
920 ) -> Self {
921 Self {
922 decision,
923 diagnostics: Diagnostics { reason, errors },
924 }
925 }
926
927 /// Get the authorization decision
928 pub fn decision(&self) -> Decision {
929 self.decision
930 }
931
932 /// Get the authorization diagnostics
933 pub fn diagnostics(&self) -> &Diagnostics {
934 &self.diagnostics
935 }
936}
937
938impl From<authorizer::Response> for Response {
939 fn from(a: authorizer::Response) -> Self {
940 Self {
941 decision: a.decision,
942 diagnostics: a.diagnostics.into(),
943 }
944 }
945}
946
947#[cfg(feature = "partial-eval")]
948impl ResidualResponse {
949 /// Create a new `ResidualResponse`
950 pub fn new(
951 residuals: PolicySet,
952 reason: HashSet<PolicyId>,
953 errors: Vec<AuthorizationError>,
954 ) -> Self {
955 Self {
956 residuals,
957 diagnostics: Diagnostics { reason, errors },
958 }
959 }
960
961 /// Get the residual policies needed to reach an authorization decision.
962 pub fn residuals(&self) -> &PolicySet {
963 &self.residuals
964 }
965
966 /// Get the authorization diagnostics
967 pub fn diagnostics(&self) -> &Diagnostics {
968 &self.diagnostics
969 }
970}
971
972#[cfg(feature = "partial-eval")]
973impl From<authorizer::PartialResponse> for ResidualResponse {
974 fn from(p: authorizer::PartialResponse) -> Self {
975 Self {
976 residuals: PolicySet::from_ast(p.residuals),
977 diagnostics: p.diagnostics.into(),
978 }
979 }
980}
981
982/// Used to select how a policy will be validated.
983#[derive(Default, Eq, PartialEq, Copy, Clone, Debug)]
984#[non_exhaustive]
985pub enum ValidationMode {
986 /// Validate that policies do not contain any type errors, and additionally
987 /// have a restricted form which is amenable for analysis.
988 #[default]
989 Strict,
990 #[cfg(feature = "permissive-validate")]
991 /// Validate that policies do not contain any type errors.
992 Permissive,
993 /// Validate using a partial schema. Policies may contain type errors.
994 #[cfg(feature = "partial-validate")]
995 Partial,
996}
997
998impl From<ValidationMode> for cedar_policy_validator::ValidationMode {
999 fn from(mode: ValidationMode) -> Self {
1000 match mode {
1001 ValidationMode::Strict => Self::Strict,
1002 #[cfg(feature = "permissive-validate")]
1003 ValidationMode::Permissive => Self::Permissive,
1004 #[cfg(feature = "partial-validate")]
1005 ValidationMode::Partial => Self::Partial,
1006 }
1007 }
1008}
1009
1010/// Validator object, which provides policy validation and typechecking.
1011#[repr(transparent)]
1012#[derive(Debug, RefCast)]
1013pub struct Validator(cedar_policy_validator::Validator);
1014
1015impl Validator {
1016 /// Construct a new `Validator` to validate policies using the given
1017 /// `Schema`.
1018 pub fn new(schema: Schema) -> Self {
1019 Self(cedar_policy_validator::Validator::new(schema.0))
1020 }
1021
1022 /// Validate all policies in a policy set, collecting all validation errors
1023 /// found into the returned `ValidationResult`. Each error is returned together with the
1024 /// policy id of the policy where the error was found. If a policy id
1025 /// included in the input policy set does not appear in the output iterator, then
1026 /// that policy passed the validator. If the function `validation_passed`
1027 /// returns true, then there were no validation errors found, so all
1028 /// policies in the policy set have passed the validator.
1029 pub fn validate<'a>(
1030 &'a self,
1031 pset: &'a PolicySet,
1032 mode: ValidationMode,
1033 ) -> ValidationResult<'a> {
1034 ValidationResult::from(self.0.validate(&pset.ast, mode.into()))
1035 }
1036}
1037
1038/// Contains all the type information used to construct a `Schema` that can be
1039/// used to validate a policy.
1040#[derive(Debug)]
1041pub struct SchemaFragment(cedar_policy_validator::ValidatorSchemaFragment);
1042
1043impl SchemaFragment {
1044 /// Extract namespaces defined in this `SchemaFragment`. Each namespace
1045 /// entry defines the name of the namespace and the entity types and actions
1046 /// that exist in the namespace.
1047 pub fn namespaces(&self) -> impl Iterator<Item = Option<EntityNamespace>> + '_ {
1048 self.0
1049 .namespaces()
1050 .map(|ns| ns.as_ref().map(|ns| EntityNamespace(ns.clone())))
1051 }
1052
1053 /// Create an `SchemaFragment` from a JSON value (which should be an
1054 /// object of the shape required for Cedar schemas).
1055 pub fn from_json_value(json: serde_json::Value) -> Result<Self, SchemaError> {
1056 Ok(Self(
1057 cedar_policy_validator::SchemaFragment::from_json_value(json)?.try_into()?,
1058 ))
1059 }
1060
1061 /// Create a `SchemaFragment` directly from a file.
1062 pub fn from_file(file: impl std::io::Read) -> Result<Self, SchemaError> {
1063 Ok(Self(
1064 cedar_policy_validator::SchemaFragment::from_file(file)?.try_into()?,
1065 ))
1066 }
1067}
1068
1069impl TryInto<Schema> for SchemaFragment {
1070 type Error = SchemaError;
1071
1072 /// Convert `SchemaFragment` into a `Schema`. To build the `Schema` we
1073 /// need to have all entity types defined, so an error will be returned if
1074 /// any undeclared entity types are referenced in the schema fragment.
1075 fn try_into(self) -> Result<Schema, Self::Error> {
1076 Ok(Schema(
1077 cedar_policy_validator::ValidatorSchema::from_schema_fragments([self.0])?,
1078 ))
1079 }
1080}
1081
1082impl FromStr for SchemaFragment {
1083 type Err = SchemaError;
1084 /// Construct `SchemaFragment` from a string containing a schema formatted
1085 /// in the cedar schema format. This can fail if the string is not valid
1086 /// JSON, or if the JSON structure does not form a valid schema. This
1087 /// function does not check for consistency in the schema (e.g., references
1088 /// to undefined entities) because this is not required until a `Schema` is
1089 /// constructed.
1090 fn from_str(src: &str) -> Result<Self, Self::Err> {
1091 Ok(Self(
1092 serde_json::from_str::<cedar_policy_validator::SchemaFragment>(src)
1093 .map_err(cedar_policy_validator::SchemaError::from)?
1094 .try_into()?,
1095 ))
1096 }
1097}
1098
1099/// Object containing schema information used by the validator.
1100#[repr(transparent)]
1101#[derive(Debug, Clone, RefCast)]
1102pub struct Schema(pub(crate) cedar_policy_validator::ValidatorSchema);
1103
1104impl FromStr for Schema {
1105 type Err = SchemaError;
1106
1107 /// Construct a schema from a string containing a schema formatted in the
1108 /// Cedar schema format. This can fail if it is not possible to parse a
1109 /// schema from the strings, or if errors in values in the schema are
1110 /// uncovered after parsing. For instance, when an entity attribute name is
1111 /// found to not be a valid attribute name according to the Cedar
1112 /// grammar.
1113 fn from_str(schema_src: &str) -> Result<Self, Self::Err> {
1114 Ok(Self(schema_src.parse()?))
1115 }
1116}
1117
1118impl Schema {
1119 /// Create a `Schema` from multiple `SchemaFragment`. The individual
1120 /// fragments may references entity types that are not declared in that
1121 /// fragment, but all referenced entity types must be declared in some
1122 /// fragment.
1123 pub fn from_schema_fragments(
1124 fragments: impl IntoIterator<Item = SchemaFragment>,
1125 ) -> Result<Self, SchemaError> {
1126 Ok(Self(
1127 cedar_policy_validator::ValidatorSchema::from_schema_fragments(
1128 fragments.into_iter().map(|f| f.0),
1129 )?,
1130 ))
1131 }
1132
1133 /// Create a `Schema` from a JSON value (which should be an object of the
1134 /// shape required for Cedar schemas).
1135 pub fn from_json_value(json: serde_json::Value) -> Result<Self, SchemaError> {
1136 Ok(Self(
1137 cedar_policy_validator::ValidatorSchema::from_json_value(
1138 json,
1139 Extensions::all_available(),
1140 )?,
1141 ))
1142 }
1143
1144 /// Create a `Schema` directly from a file.
1145 pub fn from_file(file: impl std::io::Read) -> Result<Self, SchemaError> {
1146 Ok(Self(cedar_policy_validator::ValidatorSchema::from_file(
1147 file,
1148 Extensions::all_available(),
1149 )?))
1150 }
1151
1152 /// Extract from the schema an `Entities` containing the action entities
1153 /// declared in the schema.
1154 pub fn action_entities(&self) -> Result<Entities, EntitiesError> {
1155 Ok(Entities(self.0.action_entities()?))
1156 }
1157}
1158
1159/// Errors encountered during construction of a Validation Schema
1160#[derive(Debug, Error)]
1161pub enum SchemaError {
1162 /// Error thrown by the `serde_json` crate during deserialization
1163 #[error("failed to parse schema: {0}")]
1164 Serde(#[from] serde_json::Error),
1165 /// Errors occurring while computing or enforcing transitive closure on
1166 /// action hierarchy.
1167 #[error("transitive closure computation/enforcement error on action hierarchy: {0}")]
1168 ActionTransitiveClosure(String),
1169 /// Errors occurring while computing or enforcing transitive closure on
1170 /// entity type hierarchy.
1171 #[error("transitive closure computation/enforcement error on entity type hierarchy: {0}")]
1172 EntityTypeTransitiveClosure(String),
1173 /// Error generated when processing a schema file that uses unsupported features
1174 #[error("unsupported feature used in schema: {0}")]
1175 UnsupportedFeature(String),
1176 /// Undeclared entity type(s) used in the `memberOf` field of an entity
1177 /// type, the `appliesTo` fields of an action, or an attribute type in a
1178 /// context or entity attribute record. Entity types in the error message
1179 /// are fully qualified, including any implicit or explicit namespaces.
1180 #[error("undeclared entity type(s): {0:?}")]
1181 UndeclaredEntityTypes(HashSet<String>),
1182 /// Undeclared action(s) used in the `memberOf` field of an action.
1183 #[error("undeclared action(s): {0:?}")]
1184 UndeclaredActions(HashSet<String>),
1185 /// Undeclared common type(s) used in entity or context attributes.
1186 #[error("undeclared common type(s): {0:?}")]
1187 UndeclaredCommonTypes(HashSet<String>),
1188 /// Duplicate specifications for an entity type. Argument is the name of
1189 /// the duplicate entity type.
1190 #[error("duplicate entity type `{0}`")]
1191 DuplicateEntityType(String),
1192 /// Duplicate specifications for an action. Argument is the name of the
1193 /// duplicate action.
1194 #[error("duplicate action `{0}`")]
1195 DuplicateAction(String),
1196 /// Duplicate specification for a reusable type declaration.
1197 #[error("duplicate common type `{0}`")]
1198 DuplicateCommonType(String),
1199 /// Cycle in the schema's action hierarchy.
1200 #[error("cycle in action hierarchy containing `{0}`")]
1201 CycleInActionHierarchy(EntityUid),
1202 /// Parse errors occurring while parsing an entity type.
1203 #[error("parse error in entity type: {0}")]
1204 ParseEntityType(ParseErrors),
1205 /// Parse errors occurring while parsing a namespace identifier.
1206 #[error("parse error in namespace identifier: {0}")]
1207 ParseNamespace(ParseErrors),
1208 /// Parse errors occurring while parsing an extension type.
1209 #[error("parse error in extension type: {0}")]
1210 ParseExtensionType(ParseErrors),
1211 /// Parse errors occurring while parsing the name of a reusable
1212 /// declared type.
1213 #[error("parse error in common type identifier: {0}")]
1214 ParseCommonType(ParseErrors),
1215 /// The schema file included an entity type `Action` in the entity type
1216 /// list. The `Action` entity type is always implicitly declared, and it
1217 /// cannot currently have attributes or be in any groups, so there is no
1218 /// purposes in adding an explicit entry.
1219 #[error("entity type `Action` declared in `entityTypes` list")]
1220 ActionEntityTypeDeclared,
1221 /// `context` or `shape` fields are not records
1222 #[error("{0} is declared with a type other than `Record`")]
1223 ContextOrShapeNotRecord(ContextOrShape),
1224 /// An action entity (transitively) has an attribute that is an empty set.
1225 /// The validator cannot assign a type to an empty set.
1226 /// This error variant should only be used when `PermitAttributes` is enabled.
1227 #[error("action `{0}` has an attribute that is an empty set")]
1228 ActionAttributesContainEmptySet(EntityUid),
1229 /// An action entity (transitively) has an attribute of unsupported type (`ExprEscape`, `EntityEscape` or `ExtnEscape`).
1230 /// This error variant should only be used when `PermitAttributes` is enabled.
1231 #[error("action `{0}` has an attribute with unsupported JSON representation: {1}")]
1232 UnsupportedActionAttribute(EntityUid, String),
1233 /// Error when evaluating an action attribute
1234 #[error(transparent)]
1235 ActionAttrEval(EntityAttrEvaluationError),
1236 /// Error thrown when the schema contains the `__expr` escape.
1237 /// Support for this escape form has been dropped.
1238 #[error("schema contained the non-supported `__expr` escape.")]
1239 ExprEscapeUsed,
1240}
1241
1242/// Error when evaluating an entity attribute
1243#[derive(Debug, Error)]
1244#[error("in attribute `{attr}` of `{uid}`: {err}")]
1245pub struct EntityAttrEvaluationError {
1246 /// Action that had the attribute with the error
1247 pub uid: EntityUid,
1248 /// Attribute that had the error
1249 pub attr: SmolStr,
1250 /// Underlying evaluation error
1251 pub err: EvaluationError,
1252}
1253
1254impl From<ast::EntityAttrEvaluationError> for EntityAttrEvaluationError {
1255 fn from(err: ast::EntityAttrEvaluationError) -> Self {
1256 Self {
1257 uid: EntityUid(err.uid),
1258 attr: err.attr,
1259 err: err.err,
1260 }
1261 }
1262}
1263
1264/// Describes in what action context or entity type shape a schema parsing error
1265/// occurred.
1266#[derive(Debug)]
1267pub enum ContextOrShape {
1268 /// An error occurred when parsing the context for the action with this
1269 /// `EntityUid`.
1270 ActionContext(EntityUid),
1271 /// An error occurred when parsing the shape for the entity type with this
1272 /// `EntityTypeName`.
1273 EntityTypeShape(EntityTypeName),
1274}
1275
1276impl std::fmt::Display for ContextOrShape {
1277 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1278 match self {
1279 Self::ActionContext(action) => write!(f, "Context for action {action}"),
1280 Self::EntityTypeShape(entity_type) => {
1281 write!(f, "Shape for entity type {entity_type}")
1282 }
1283 }
1284 }
1285}
1286
1287impl From<cedar_policy_validator::ContextOrShape> for ContextOrShape {
1288 fn from(value: cedar_policy_validator::ContextOrShape) -> Self {
1289 match value {
1290 cedar_policy_validator::ContextOrShape::ActionContext(euid) => {
1291 Self::ActionContext(EntityUid(euid))
1292 }
1293 cedar_policy_validator::ContextOrShape::EntityTypeShape(name) => {
1294 Self::EntityTypeShape(EntityTypeName(name))
1295 }
1296 }
1297 }
1298}
1299
1300#[doc(hidden)]
1301impl From<cedar_policy_validator::SchemaError> for SchemaError {
1302 fn from(value: cedar_policy_validator::SchemaError) -> Self {
1303 match value {
1304 cedar_policy_validator::SchemaError::Serde(e) => Self::Serde(e),
1305 cedar_policy_validator::SchemaError::ActionTransitiveClosure(e) => {
1306 Self::ActionTransitiveClosure(e.to_string())
1307 }
1308 cedar_policy_validator::SchemaError::EntityTypeTransitiveClosure(e) => {
1309 Self::EntityTypeTransitiveClosure(e.to_string())
1310 }
1311 cedar_policy_validator::SchemaError::UnsupportedFeature(e) => {
1312 Self::UnsupportedFeature(e.to_string())
1313 }
1314 cedar_policy_validator::SchemaError::UndeclaredEntityTypes(e) => {
1315 Self::UndeclaredEntityTypes(e)
1316 }
1317 cedar_policy_validator::SchemaError::UndeclaredActions(e) => Self::UndeclaredActions(e),
1318 cedar_policy_validator::SchemaError::UndeclaredCommonTypes(c) => {
1319 Self::UndeclaredCommonTypes(c)
1320 }
1321 cedar_policy_validator::SchemaError::DuplicateEntityType(e) => {
1322 Self::DuplicateEntityType(e)
1323 }
1324 cedar_policy_validator::SchemaError::DuplicateAction(e) => Self::DuplicateAction(e),
1325 cedar_policy_validator::SchemaError::DuplicateCommonType(c) => {
1326 Self::DuplicateCommonType(c)
1327 }
1328 cedar_policy_validator::SchemaError::CycleInActionHierarchy(e) => {
1329 Self::CycleInActionHierarchy(EntityUid(e))
1330 }
1331 cedar_policy_validator::SchemaError::ParseEntityType(e) => Self::ParseEntityType(e),
1332 cedar_policy_validator::SchemaError::ParseNamespace(e) => Self::ParseNamespace(e),
1333 cedar_policy_validator::SchemaError::ParseCommonType(e) => Self::ParseCommonType(e),
1334 cedar_policy_validator::SchemaError::ParseExtensionType(e) => {
1335 Self::ParseExtensionType(e)
1336 }
1337 cedar_policy_validator::SchemaError::ActionEntityTypeDeclared => {
1338 Self::ActionEntityTypeDeclared
1339 }
1340 cedar_policy_validator::SchemaError::ContextOrShapeNotRecord(context_or_shape) => {
1341 Self::ContextOrShapeNotRecord(context_or_shape.into())
1342 }
1343 cedar_policy_validator::SchemaError::ActionAttributesContainEmptySet(uid) => {
1344 Self::ActionAttributesContainEmptySet(EntityUid(uid))
1345 }
1346 cedar_policy_validator::SchemaError::UnsupportedActionAttribute(uid, escape_type) => {
1347 Self::UnsupportedActionAttribute(EntityUid(uid), escape_type)
1348 }
1349 cedar_policy_validator::SchemaError::ActionAttrEval(err) => {
1350 Self::ActionAttrEval(err.into())
1351 }
1352 cedar_policy_validator::SchemaError::ExprEscapeUsed => Self::ExprEscapeUsed,
1353 }
1354 }
1355}
1356
1357/// Contains the result of policy validation. The result includes the list of
1358/// issues found by validation and whether validation succeeds or fails.
1359/// Validation succeeds if there are no fatal errors. There may still be
1360/// non-fatal warnings present when validation passes.
1361#[derive(Debug)]
1362pub struct ValidationResult<'a> {
1363 validation_errors: Vec<ValidationError<'a>>,
1364 validation_warnings: Vec<ValidationWarning<'a>>,
1365}
1366
1367impl<'a> ValidationResult<'a> {
1368 /// True when validation passes. There are no errors, but there may be
1369 /// non-fatal warnings. Use [`ValidationResult::validation_passed_without_warnings`]
1370 /// to check that there are also no warnings.
1371 pub fn validation_passed(&self) -> bool {
1372 self.validation_errors.is_empty()
1373 }
1374
1375 /// True when validation passes (i.e., there are no errors) and there are
1376 /// additionally no non-fatal warnings.
1377 pub fn validation_passed_without_warnings(&self) -> bool {
1378 self.validation_errors.is_empty() && self.validation_warnings.is_empty()
1379 }
1380
1381 /// Get an iterator over the errors found by the validator.
1382 pub fn validation_errors(&self) -> impl Iterator<Item = &ValidationError<'a>> {
1383 self.validation_errors.iter()
1384 }
1385
1386 /// Get an iterator over the warnings found by the validator.
1387 pub fn validation_warnings(&self) -> impl Iterator<Item = &ValidationWarning<'a>> {
1388 self.validation_warnings.iter()
1389 }
1390}
1391
1392impl<'a> From<cedar_policy_validator::ValidationResult<'a>> for ValidationResult<'a> {
1393 fn from(r: cedar_policy_validator::ValidationResult<'a>) -> Self {
1394 let (errors, warnings) = r.into_errors_and_warnings();
1395 Self {
1396 validation_errors: errors.map(ValidationError::from).collect(),
1397 validation_warnings: warnings.map(ValidationWarning::from).collect(),
1398 }
1399 }
1400}
1401
1402/// An error generated by the validator when it finds a potential problem in a
1403/// policy. The error contains a enumeration that specifies the kind of problem,
1404/// and provides details specific to that kind of problem. The error also records
1405/// where the problem was encountered.
1406#[derive(Debug, Error)]
1407#[error("validation error on `{}`: {}", self.location, self.error_kind())]
1408pub struct ValidationError<'a> {
1409 location: SourceLocation<'a>,
1410 error_kind: ValidationErrorKind,
1411}
1412
1413impl<'a> ValidationError<'a> {
1414 /// Extract details about the exact issue detected by the validator.
1415 pub fn error_kind(&self) -> &ValidationErrorKind {
1416 &self.error_kind
1417 }
1418
1419 /// Extract the location where the validator found the issue.
1420 pub fn location(&self) -> &SourceLocation<'a> {
1421 &self.location
1422 }
1423}
1424
1425impl<'a> From<cedar_policy_validator::ValidationError<'a>> for ValidationError<'a> {
1426 fn from(err: cedar_policy_validator::ValidationError<'a>) -> Self {
1427 let (location, error_kind) = err.into_location_and_error_kind();
1428 Self {
1429 location: SourceLocation::from(location),
1430 error_kind,
1431 }
1432 }
1433}
1434
1435/// Represents a location in Cedar policy source.
1436#[derive(Debug, Clone, Eq, PartialEq)]
1437pub struct SourceLocation<'a> {
1438 policy_id: &'a PolicyId,
1439 source_range: Option<SourceInfo>,
1440}
1441
1442impl<'a> SourceLocation<'a> {
1443 /// Get the `PolicyId` for the policy at this source location.
1444 pub fn policy_id(&self) -> &'a PolicyId {
1445 self.policy_id
1446 }
1447
1448 /// Get the start of the location. Returns `None` if this location does not
1449 /// have a range.
1450 pub fn range_start(&self) -> Option<usize> {
1451 self.source_range.as_ref().map(SourceInfo::range_start)
1452 }
1453
1454 /// Get the end of the location. Returns `None` if this location does not
1455 /// have a range.
1456 pub fn range_end(&self) -> Option<usize> {
1457 self.source_range.as_ref().map(SourceInfo::range_end)
1458 }
1459}
1460
1461impl<'a> std::fmt::Display for SourceLocation<'a> {
1462 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1463 write!(f, "policy `{}`", self.policy_id)?;
1464 if let Some(source_range) = &self.source_range {
1465 write!(
1466 f,
1467 " at offset {}-{}",
1468 source_range.range_start(),
1469 source_range.range_end()
1470 )?;
1471 }
1472 Ok(())
1473 }
1474}
1475
1476impl<'a> From<cedar_policy_validator::SourceLocation<'a>> for SourceLocation<'a> {
1477 fn from(loc: cedar_policy_validator::SourceLocation<'a>) -> SourceLocation<'a> {
1478 let policy_id: &'a PolicyId = PolicyId::ref_cast(loc.policy_id());
1479 let source_range = loc.into_source_info();
1480 Self {
1481 policy_id,
1482 source_range,
1483 }
1484 }
1485}
1486
1487/// Scan a set of policies for potentially confusing/obfuscating text. These
1488/// checks are also provided through [`Validator::validate`] which provides more
1489/// comprehensive error detection, but this function can be used to check for
1490/// confusable strings without defining a schema.
1491pub fn confusable_string_checker<'a>(
1492 templates: impl Iterator<Item = &'a Template>,
1493) -> impl Iterator<Item = ValidationWarning<'a>> {
1494 cedar_policy_validator::confusable_string_checks(templates.map(|t| &t.ast))
1495 .map(std::convert::Into::into)
1496}
1497
1498#[derive(Debug, Error)]
1499#[error("validation warning on `{}`: {}", .location, .kind)]
1500/// Warnings found in Cedar policies
1501pub struct ValidationWarning<'a> {
1502 location: SourceLocation<'a>,
1503 kind: ValidationWarningKind,
1504}
1505
1506impl<'a> ValidationWarning<'a> {
1507 /// Extract details about the exact issue detected by the validator.
1508 pub fn warning_kind(&self) -> &ValidationWarningKind {
1509 &self.kind
1510 }
1511
1512 /// Extract the location where the validator found the issue.
1513 pub fn location(&self) -> &SourceLocation<'a> {
1514 &self.location
1515 }
1516}
1517
1518#[doc(hidden)]
1519impl<'a> From<cedar_policy_validator::ValidationWarning<'a>> for ValidationWarning<'a> {
1520 fn from(w: cedar_policy_validator::ValidationWarning<'a>) -> Self {
1521 let (loc, kind) = w.to_kind_and_location();
1522 ValidationWarning {
1523 location: loc.into(),
1524 kind,
1525 }
1526 }
1527}
1528
1529/// unique identifier portion of the `EntityUid` type
1530#[repr(transparent)]
1531#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, RefCast)]
1532pub struct EntityId(ast::Eid);
1533
1534impl FromStr for EntityId {
1535 type Err = Infallible;
1536 fn from_str(eid_str: &str) -> Result<Self, Self::Err> {
1537 Ok(Self(ast::Eid::new(eid_str)))
1538 }
1539}
1540
1541impl AsRef<str> for EntityId {
1542 fn as_ref(&self) -> &str {
1543 self.0.as_ref()
1544 }
1545}
1546
1547// Note that this Display formatter will format the EntityId as it would be expected
1548// in the EntityUid string form. For instance, the `"alice"` in `User::"alice"`.
1549// This means it adds quotes and potentially performs some escaping.
1550impl std::fmt::Display for EntityId {
1551 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1552 write!(f, "{}", self.0)
1553 }
1554}
1555
1556/// Represents a concatenation of Namespaces and `TypeName`
1557#[repr(transparent)]
1558#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, RefCast)]
1559pub struct EntityTypeName(ast::Name);
1560
1561impl EntityTypeName {
1562 /// Get the basename of the `EntityTypeName` (ie, with namespaces stripped).
1563 /// ```
1564 /// use cedar_policy::EntityTypeName;
1565 /// use std::str::FromStr;
1566 /// let type_name = EntityTypeName::from_str("MySpace::User").unwrap();
1567 /// assert_eq!(type_name.basename(), "User");
1568 /// ```
1569 pub fn basename(&self) -> &str {
1570 self.0.basename().as_ref()
1571 }
1572
1573 /// Get the namespace of the `EntityTypeName`, as components
1574 /// ```
1575 /// use cedar_policy::EntityTypeName;
1576 /// use std::str::FromStr;
1577 /// let type_name = EntityTypeName::from_str("Namespace::MySpace::User").unwrap();
1578 /// let mut components = type_name.namespace_components();
1579 /// assert_eq!(components.next(), Some("Namespace"));
1580 /// assert_eq!(components.next(), Some("MySpace"));
1581 /// assert_eq!(components.next(), None);
1582 /// ```
1583 pub fn namespace_components(&self) -> impl Iterator<Item = &str> {
1584 self.0.namespace_components().map(AsRef::as_ref)
1585 }
1586
1587 /// Get the full namespace of the `EntityTypeName`, as a single string.
1588 /// ```
1589 /// use cedar_policy::EntityTypeName;
1590 /// use std::str::FromStr;
1591 /// let type_name = EntityTypeName::from_str("Namespace::MySpace::User").unwrap();
1592 /// let components = type_name.namespace();
1593 /// assert_eq!(components,"Namespace::MySpace");
1594 /// ```
1595 pub fn namespace(&self) -> String {
1596 self.0.namespace()
1597 }
1598}
1599
1600// This FromStr implementation requires the _normalized_ representation of the
1601// type name. See https://github.com/cedar-policy/rfcs/pull/9/.
1602impl FromStr for EntityTypeName {
1603 type Err = ParseErrors;
1604
1605 fn from_str(namespace_type_str: &str) -> Result<Self, Self::Err> {
1606 ast::Name::from_normalized_str(namespace_type_str).map(EntityTypeName)
1607 }
1608}
1609
1610impl std::fmt::Display for EntityTypeName {
1611 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1612 write!(f, "{}", self.0)
1613 }
1614}
1615
1616/// Represents a namespace
1617#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
1618pub struct EntityNamespace(ast::Name);
1619
1620// This FromStr implementation requires the _normalized_ representation of the
1621// namespace. See https://github.com/cedar-policy/rfcs/pull/9/.
1622impl FromStr for EntityNamespace {
1623 type Err = ParseErrors;
1624
1625 fn from_str(namespace_str: &str) -> Result<Self, Self::Err> {
1626 ast::Name::from_normalized_str(namespace_str).map(EntityNamespace)
1627 }
1628}
1629
1630impl std::fmt::Display for EntityNamespace {
1631 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1632 write!(f, "{}", self.0)
1633 }
1634}
1635
1636/// Unique Id for an entity, such as `User::"alice"`
1637// INVARIANT: this can never be an `ast::EntityType::Unspecified`
1638#[repr(transparent)]
1639#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, RefCast)]
1640pub struct EntityUid(ast::EntityUID);
1641
1642impl EntityUid {
1643 /// Returns the portion of the Euid that represents namespace and entity type
1644 /// ```
1645 /// use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid};
1646 /// use std::str::FromStr;
1647 /// let json_data = serde_json::json!({ "__entity": { "type": "User", "id": "alice" } });
1648 /// let euid = EntityUid::from_json(json_data).unwrap();
1649 /// assert_eq!(euid.type_name(), &EntityTypeName::from_str("User").unwrap());
1650 /// ```
1651 pub fn type_name(&self) -> &EntityTypeName {
1652 // PANIC SAFETY by invariant on struct
1653 #[allow(clippy::panic)]
1654 match self.0.entity_type() {
1655 ast::EntityType::Unspecified => panic!("Impossible to have an unspecified entity"),
1656 ast::EntityType::Specified(name) => EntityTypeName::ref_cast(name),
1657 }
1658 }
1659
1660 /// Returns the id portion of the Euid
1661 /// ```
1662 /// use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid};
1663 /// use std::str::FromStr;
1664 /// let json_data = serde_json::json!({ "__entity": { "type": "User", "id": "alice" } });
1665 /// let euid = EntityUid::from_json(json_data).unwrap();
1666 /// assert_eq!(euid.id(), &EntityId::from_str("alice").unwrap());
1667 /// ```
1668 pub fn id(&self) -> &EntityId {
1669 EntityId::ref_cast(self.0.eid())
1670 }
1671
1672 /// Creates `EntityUid` from `EntityTypeName` and `EntityId`
1673 ///```
1674 /// use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid};
1675 /// use std::str::FromStr;
1676 /// let eid = EntityId::from_str("alice").unwrap();
1677 /// let type_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
1678 /// let euid = EntityUid::from_type_name_and_id(type_name, eid);
1679 /// assert_eq!(euid.type_name(), &EntityTypeName::from_str("User").unwrap());
1680 /// assert_eq!(euid.id(), &EntityId::from_str("alice").unwrap());
1681 ///
1682 /// ```
1683 pub fn from_type_name_and_id(name: EntityTypeName, id: EntityId) -> Self {
1684 // INVARIANT: `from_components` always constructs a Concrete id
1685 Self(ast::EntityUID::from_components(name.0, id.0))
1686 }
1687
1688 /// Creates `EntityUid` from a JSON value, which should have
1689 /// either the implicit or explicit `__entity` form.
1690 /// ```
1691 /// # use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid};
1692 /// # use std::str::FromStr;
1693 /// let json_data = serde_json::json!({ "__entity": { "type": "User", "id": "123abc" } });
1694 /// let euid = EntityUid::from_json(json_data).unwrap();
1695 /// assert_eq!(euid.type_name(), &EntityTypeName::from_str("User").unwrap());
1696 /// ```
1697 pub fn from_json(json: serde_json::Value) -> Result<Self, impl std::error::Error> {
1698 let parsed: entities::EntityUidJson = serde_json::from_value(json)?;
1699 // INVARIANT: There is no way to write down the unspecified entityuid
1700 Ok::<Self, entities::JsonDeserializationError>(Self(
1701 parsed.into_euid(|| JsonDeserializationErrorContext::EntityUid)?,
1702 ))
1703 }
1704
1705 /// Testing utility for creating `EntityUids` a bit easier
1706 #[cfg(test)]
1707 pub(crate) fn from_strs(typename: &str, id: &str) -> Self {
1708 Self::from_type_name_and_id(
1709 EntityTypeName::from_str(typename).unwrap(),
1710 EntityId::from_str(id).unwrap(),
1711 )
1712 }
1713}
1714
1715impl FromStr for EntityUid {
1716 type Err = ParseErrors;
1717
1718 /// Parse an [`EntityUid`].
1719 ///
1720 /// An [`EntityUid`] consists of an [`EntityTypeName`] followed by a quoted [`EntityId`].
1721 /// The two are joined by a `::`.
1722 /// For the formal grammar, see <https://docs.cedarpolicy.com/policies/syntax-grammar.html#entity>
1723 ///
1724 /// Examples:
1725 /// ```
1726 /// use cedar_policy::EntityUid;
1727 /// let euid: EntityUid = r#"Foo::Bar::"george""#.parse().unwrap();
1728 /// // Get the type of this euid (`Foo::Bar`)
1729 /// euid.type_name();
1730 /// // Or the id
1731 /// euid.id();
1732 /// ```
1733 ///
1734 /// This [`FromStr`] implementation requires the _normalized_ representation of the
1735 /// UID. See <https://github.com/cedar-policy/rfcs/pull/9/>.
1736 ///
1737 /// A note on safety:
1738 ///
1739 /// __DO NOT__ create [`EntityUid`]'s via string concatenation.
1740 /// If you have separate components of an [`EntityUid`], use [`EntityUid::from_type_name_and_id`]
1741 fn from_str(uid_str: &str) -> Result<Self, Self::Err> {
1742 // INVARIANT there is no way to write down the unspecified entity
1743 ast::EntityUID::from_normalized_str(uid_str).map(EntityUid)
1744 }
1745}
1746
1747impl std::fmt::Display for EntityUid {
1748 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1749 write!(f, "{}", self.0)
1750 }
1751}
1752
1753/// Potential errors when adding to a `PolicySet`.
1754#[derive(Error, Debug)]
1755#[non_exhaustive]
1756pub enum PolicySetError {
1757 /// There was a duplicate [`PolicyId`] encountered in either the set of
1758 /// templates or the set of policies.
1759 #[error("duplicate template or policy id `{id}`")]
1760 AlreadyDefined {
1761 /// [`PolicyId`] that was duplicate
1762 id: PolicyId,
1763 },
1764 /// Error when linking a template
1765 #[error("unable to link template: {0}")]
1766 LinkingError(#[from] ast::LinkingError),
1767 /// Expected a static policy, but a template-linked policy was provided
1768 #[error("expected a static policy, but a template-linked policy was provided")]
1769 ExpectedStatic,
1770 /// Expected a template, but a static policy was provided.
1771 #[error("expected a template, but a static policy was provided")]
1772 ExpectedTemplate,
1773 /// Error when removing a static policy
1774 #[error("unable to remove static policy `{0}` because it does not exist")]
1775 PolicyNonexistentError(PolicyId),
1776 /// Error when removing a template that doesn't exist
1777 #[error("unable to remove policy template `{0}` because it does not exist")]
1778 TemplateNonexistentError(PolicyId),
1779 /// Error when removing a template with active links
1780 #[error("unable to remove policy template `{0}` because it has active links")]
1781 RemoveTemplateWithActiveLinksError(PolicyId),
1782 /// Error when removing a template that is not a template
1783 #[error("unable to remove policy template `{0}` because it is not a template")]
1784 RemoveTemplateNotTemplateError(PolicyId),
1785 /// Error when unlinking a template
1786 #[error("unable to unlink policy template `{0}` because it does not exist")]
1787 LinkNonexistentError(PolicyId),
1788 /// Error when removing a link that is not a link
1789 #[error("unable to unlink `{0}` because it is not a link")]
1790 UnlinkLinkNotLinkError(PolicyId),
1791}
1792
1793impl From<ast::PolicySetError> for PolicySetError {
1794 fn from(e: ast::PolicySetError) -> Self {
1795 match e {
1796 ast::PolicySetError::Occupied { id } => Self::AlreadyDefined { id: PolicyId(id) },
1797 }
1798 }
1799}
1800
1801impl From<ast::UnexpectedSlotError> for PolicySetError {
1802 fn from(_: ast::UnexpectedSlotError) -> Self {
1803 Self::ExpectedStatic
1804 }
1805}
1806
1807/// Represents a set of `Policy`s
1808#[derive(Debug, Clone, Default)]
1809pub struct PolicySet {
1810 /// AST representation. Technically partially redundant with the other fields.
1811 /// Internally, we ensure that the duplicated information remains consistent.
1812 pub(crate) ast: ast::PolicySet,
1813 /// Policies in the set (this includes both static policies and template linked-policies)
1814 policies: HashMap<PolicyId, Policy>,
1815 /// Templates in the set
1816 templates: HashMap<PolicyId, Template>,
1817}
1818
1819impl PartialEq for PolicySet {
1820 fn eq(&self, other: &Self) -> bool {
1821 // eq is based on just the `ast`
1822 self.ast.eq(&other.ast)
1823 }
1824}
1825impl Eq for PolicySet {}
1826
1827impl FromStr for PolicySet {
1828 type Err = ParseErrors;
1829
1830 /// Create a policy set from multiple statements.
1831 ///
1832 /// Policy ids will default to "policy*" with numbers from 0.
1833 /// If you load more policies, do not use the default id, or there will be conflicts.
1834 ///
1835 /// See [`Policy`] for more.
1836 fn from_str(policies: &str) -> Result<Self, Self::Err> {
1837 let (texts, pset) = parser::parse_policyset_and_also_return_policy_text(policies)?;
1838 // 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`.
1839 #[allow(clippy::expect_used)]
1840 let policies = pset.policies().map(|p|
1841 (
1842 PolicyId(p.id().clone()),
1843 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() }
1844 )
1845 ).collect();
1846 // PANIC SAFETY: By the same invariant, every `PolicyId` in `pset.templates()` also occurs as a key in `text`.
1847 #[allow(clippy::expect_used)]
1848 let templates = pset.templates().map(|t|
1849 (
1850 PolicyId(t.id().clone()),
1851 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() }
1852 )
1853 ).collect();
1854 Ok(Self {
1855 ast: pset,
1856 policies,
1857 templates,
1858 })
1859 }
1860}
1861
1862impl PolicySet {
1863 /// Create a fresh empty `PolicySet`
1864 pub fn new() -> Self {
1865 Self {
1866 ast: ast::PolicySet::new(),
1867 policies: HashMap::new(),
1868 templates: HashMap::new(),
1869 }
1870 }
1871
1872 /// Create a `PolicySet` from the given policies
1873 pub fn from_policies(
1874 policies: impl IntoIterator<Item = Policy>,
1875 ) -> Result<Self, PolicySetError> {
1876 let mut set = Self::new();
1877 for policy in policies {
1878 set.add(policy)?;
1879 }
1880 Ok(set)
1881 }
1882
1883 /// Add an static policy to the `PolicySet`. To add a template instance, use
1884 /// `link` instead. This function will return an error (and not modify
1885 /// the `PolicySet`) if a template-linked policy is passed in.
1886 pub fn add(&mut self, policy: Policy) -> Result<(), PolicySetError> {
1887 if policy.is_static() {
1888 let id = PolicyId(policy.ast.id().clone());
1889 self.ast.add(policy.ast.clone())?;
1890 self.policies.insert(id, policy);
1891 Ok(())
1892 } else {
1893 Err(PolicySetError::ExpectedStatic)
1894 }
1895 }
1896
1897 /// Remove a static `Policy` from the `PolicySet`.
1898 ///
1899 /// This will error if the policy is not a static policy.
1900 pub fn remove_static(&mut self, policy_id: PolicyId) -> Result<Policy, PolicySetError> {
1901 let Some(policy) = self.policies.remove(&policy_id) else {
1902 return Err(PolicySetError::PolicyNonexistentError(policy_id));
1903 };
1904 match self
1905 .ast
1906 .remove_static(&ast::PolicyID::from_string(policy_id.to_string()))
1907 {
1908 Ok(_) => Ok(policy),
1909 Err(_) => {
1910 //Restore self.policies
1911 self.policies.insert(policy_id.clone(), policy);
1912 Err(PolicySetError::PolicyNonexistentError(policy_id.clone()))
1913 }
1914 }
1915 }
1916
1917 /// Add a `Template` to the `PolicySet`
1918 pub fn add_template(&mut self, template: Template) -> Result<(), PolicySetError> {
1919 let id = PolicyId(template.ast.id().clone());
1920 self.ast.add_template(template.ast.clone())?;
1921 self.templates.insert(id, template);
1922 Ok(())
1923 }
1924
1925 /// Remove a `Template` from the `PolicySet`.
1926 ///
1927 /// This will error if any policy is linked to the template.
1928 /// This will error if `policy_id` is not a template.
1929 pub fn remove_template(&mut self, template_id: PolicyId) -> Result<Template, PolicySetError> {
1930 let Some(template) = self.templates.remove(&template_id) else {
1931 return Err(PolicySetError::TemplateNonexistentError(template_id));
1932 };
1933 // If self.templates and self.ast disagree, authorization cannot be trusted.
1934 // PANIC SAFETY: We just found the policy in self.templates.
1935 #[allow(clippy::panic)]
1936 match self
1937 .ast
1938 .remove_template(&ast::PolicyID::from_string(template_id.to_string()))
1939 {
1940 Ok(_) => Ok(template),
1941 Err(ast::PolicySetTemplateRemovalError::RemoveTemplateWithLinksError(_)) => {
1942 self.templates.insert(template_id.clone(), template);
1943 Err(PolicySetError::RemoveTemplateWithActiveLinksError(
1944 template_id,
1945 ))
1946 }
1947 Err(ast::PolicySetTemplateRemovalError::NotTemplateError(_)) => {
1948 self.templates.insert(template_id.clone(), template);
1949 Err(PolicySetError::RemoveTemplateNotTemplateError(template_id))
1950 }
1951 Err(ast::PolicySetTemplateRemovalError::RemovePolicyNoTemplateError(_)) => {
1952 panic!("Found template policy in self.templates but not in self.ast");
1953 }
1954 }
1955 }
1956
1957 /// Get policies linked to a `Template` in the `PolicySet`.
1958 /// If any policy is linked to the template, this will error
1959 pub fn get_linked_policies(
1960 &self,
1961 template_id: PolicyId,
1962 ) -> Result<impl Iterator<Item = &PolicyId>, PolicySetError> {
1963 match self
1964 .ast
1965 .get_linked_policies(&ast::PolicyID::from_string(template_id.to_string()))
1966 {
1967 Ok(v) => Ok(v.map(PolicyId::ref_cast)),
1968 Err(_) => Err(PolicySetError::TemplateNonexistentError(template_id)),
1969 }
1970 }
1971
1972 /// Iterate over all the `Policy`s in the `PolicySet`.
1973 ///
1974 /// This will include both static and template-linked policies.
1975 pub fn policies(&self) -> impl Iterator<Item = &Policy> {
1976 self.policies.values()
1977 }
1978
1979 /// Iterate over the `Template`'s in the `PolicySet`.
1980 pub fn templates(&self) -> impl Iterator<Item = &Template> {
1981 self.templates.values()
1982 }
1983
1984 /// Get a `Template` by its `PolicyId`
1985 pub fn template(&self, id: &PolicyId) -> Option<&Template> {
1986 self.templates.get(id)
1987 }
1988
1989 /// Get a `Policy` by its `PolicyId`
1990 pub fn policy(&self, id: &PolicyId) -> Option<&Policy> {
1991 self.policies.get(id)
1992 }
1993
1994 /// Extract annotation data from a `Policy` by its `PolicyId` and annotation key
1995 pub fn annotation<'a>(&'a self, id: &PolicyId, key: impl AsRef<str>) -> Option<&'a str> {
1996 self.ast
1997 .get(&id.0)?
1998 .annotation(&key.as_ref().parse().ok()?)
1999 .map(smol_str::SmolStr::as_str)
2000 }
2001
2002 /// Extract annotation data from a `Template` by its `PolicyId` and annotation key.
2003 pub fn template_annotation(&self, id: &PolicyId, key: impl AsRef<str>) -> Option<String> {
2004 self.ast
2005 .get_template(&id.0)?
2006 .annotation(&key.as_ref().parse().ok()?)
2007 .map(smol_str::SmolStr::to_string)
2008 }
2009
2010 /// Returns true iff the `PolicySet` is empty
2011 pub fn is_empty(&self) -> bool {
2012 debug_assert_eq!(
2013 self.ast.is_empty(),
2014 self.policies.is_empty() && self.templates.is_empty()
2015 );
2016 self.ast.is_empty()
2017 }
2018
2019 /// Attempt to link a template and add the new template-linked policy to the policy set.
2020 /// If link fails, the `PolicySet` is not modified.
2021 /// Failure can happen for three reasons
2022 /// 1) The map passed in `vals` may not match the slots in the template
2023 /// 2) The `new_id` may conflict w/ a policy that already exists in the set
2024 /// 3) `template_id` does not correspond to a template. Either the id is
2025 /// not in the policy set, or it is in the policy set but is either a
2026 /// linked or static policy rather than a template
2027 #[allow(clippy::needless_pass_by_value)]
2028 pub fn link(
2029 &mut self,
2030 template_id: PolicyId,
2031 new_id: PolicyId,
2032 vals: HashMap<SlotId, EntityUid>,
2033 ) -> Result<(), PolicySetError> {
2034 let unwrapped_vals: HashMap<ast::SlotId, ast::EntityUID> = vals
2035 .into_iter()
2036 .map(|(key, value)| (key.into(), value.0))
2037 .collect();
2038
2039 // Try to get the template with the id we're linking from. We do this
2040 // _before_ calling `self.ast.link` because `link` mutates the policy
2041 // set by creating a new link entry in a hashmap. This happens even when
2042 // trying to link a static policy, which we want to error on here.
2043 let Some(template) = self.templates.get(&template_id) else {
2044 return Err(if self.policies.contains_key(&template_id) {
2045 PolicySetError::ExpectedTemplate
2046 } else {
2047 PolicySetError::LinkingError(ast::LinkingError::NoSuchTemplate {
2048 id: template_id.0,
2049 })
2050 });
2051 };
2052
2053 let linked_ast = self
2054 .ast
2055 .link(
2056 template_id.0.clone(),
2057 new_id.0.clone(),
2058 unwrapped_vals.clone(),
2059 )
2060 .map_err(PolicySetError::LinkingError)?;
2061
2062 // PANIC SAFETY: `lossless.link()` will not fail after `ast.link()` succeeds
2063 #[allow(clippy::expect_used)]
2064 let linked_lossless = template
2065 .lossless
2066 .clone()
2067 .link(unwrapped_vals.iter().map(|(k, v)| (*k, v)))
2068 // The only error case for `lossless.link()` is a template with
2069 // slots which are not filled by the provided values. `ast.link()`
2070 // will have already errored if there are any unfilled slots in the
2071 // template.
2072 .expect("ast.link() didn't fail above, so this shouldn't fail");
2073 self.policies.insert(
2074 new_id,
2075 Policy {
2076 ast: linked_ast.clone(),
2077 lossless: linked_lossless,
2078 },
2079 );
2080 Ok(())
2081 }
2082
2083 /// Get all the unknown entities from the policy set
2084 #[cfg(feature = "partial-eval")]
2085 pub fn unknown_entities(&self) -> HashSet<EntityUid> {
2086 let mut entity_uids = HashSet::new();
2087 for policy in self.policies.values() {
2088 let ids: Vec<EntityUid> = policy
2089 .ast
2090 .condition()
2091 .unknowns()
2092 .filter_map(
2093 |ast::Unknown {
2094 name,
2095 type_annotation,
2096 }| {
2097 if matches!(type_annotation, Some(ast::Type::Entity { .. })) {
2098 EntityUid::from_str(name.as_str()).ok()
2099 } else {
2100 None
2101 }
2102 },
2103 )
2104 .collect();
2105 entity_uids.extend(ids);
2106 }
2107 entity_uids
2108 }
2109
2110 /// Unlink a template link from the policy set.
2111 /// Returns the policy that was unlinked.
2112 pub fn unlink(&mut self, policy_id: PolicyId) -> Result<Policy, PolicySetError> {
2113 let Some(policy) = self.policies.remove(&policy_id) else {
2114 return Err(PolicySetError::LinkNonexistentError(policy_id));
2115 };
2116 // If self.policies and self.ast disagree, authorization cannot be trusted.
2117 // PANIC SAFETY: We just found the policy in self.policies.
2118 #[allow(clippy::panic)]
2119 match self
2120 .ast
2121 .unlink(&ast::PolicyID::from_string(policy_id.to_string()))
2122 {
2123 Ok(_) => Ok(policy),
2124 Err(ast::PolicySetUnlinkError::NotLinkError(_)) => {
2125 //Restore self.policies
2126 self.policies.insert(policy_id.clone(), policy);
2127 Err(PolicySetError::UnlinkLinkNotLinkError(policy_id))
2128 }
2129 Err(ast::PolicySetUnlinkError::UnlinkingError(_)) => {
2130 panic!("Found linked policy in self.policies but not in self.ast")
2131 }
2132 }
2133 }
2134
2135 /// Create a `PolicySet` from its AST representation only. The EST will
2136 /// reflect the AST structure. When possible, don't use this method and
2137 /// create the ESTs from the policy text or CST instead, as the conversion
2138 /// to AST is lossy. ESTs generated by this method will reflect the AST and
2139 /// not the original policy syntax.
2140 #[cfg_attr(not(feature = "partial-eval"), allow(unused))]
2141 fn from_ast(ast: ast::PolicySet) -> Self {
2142 let policies = ast
2143 .policies()
2144 .map(|p| (PolicyId(p.id().clone()), Policy::from_ast(p.clone())))
2145 .collect();
2146 let templates = ast
2147 .templates()
2148 .map(|t| (PolicyId(t.id().clone()), Template::from_ast(t.clone())))
2149 .collect();
2150 Self {
2151 ast,
2152 policies,
2153 templates,
2154 }
2155 }
2156}
2157
2158impl std::fmt::Display for PolicySet {
2159 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2160 // prefer to display the lossless format
2161 write!(f, "{}", self.policies().map(|p| &p.lossless).join("\n"))
2162 }
2163}
2164
2165/// Policy template datatype
2166#[derive(Debug, Clone)]
2167pub struct Template {
2168 /// AST representation of the template, used for most operations.
2169 /// In particular, the `ast` contains the authoritative `PolicyId` for the template.
2170 ast: ast::Template,
2171
2172 /// Some "lossless" representation of the template, whichever is most
2173 /// convenient to provide (and can be provided with the least overhead).
2174 /// This is used just for `to_json()`.
2175 /// We can't just derive this on-demand from `ast`, because the AST is lossy:
2176 /// we can't reconstruct an accurate CST/EST/policy-text from the AST, but
2177 /// we can from the EST (modulo whitespace and a few other things like the
2178 /// order of annotations).
2179 ///
2180 /// This is a `LosslessPolicy` (rather than something like `LosslessTemplate`)
2181 /// because the EST doesn't distinguish between static policies and templates.
2182 lossless: LosslessPolicy,
2183}
2184
2185impl PartialEq for Template {
2186 fn eq(&self, other: &Self) -> bool {
2187 // eq is based on just the `ast`
2188 self.ast.eq(&other.ast)
2189 }
2190}
2191impl Eq for Template {}
2192
2193impl Template {
2194 /// Attempt to parse a `Template` from source.
2195 /// If `id` is Some, then the resulting template will have that `id`.
2196 /// If the `id` is None, the parser will use the default "policy0".
2197 /// The behavior around None may change in the future.
2198 pub fn parse(id: Option<String>, src: impl AsRef<str>) -> Result<Self, ParseErrors> {
2199 let ast = parser::parse_policy_template(id, src.as_ref())?;
2200 Ok(Self {
2201 ast,
2202 lossless: LosslessPolicy::policy_or_template_text(src.as_ref()),
2203 })
2204 }
2205
2206 /// Get the `PolicyId` of this `Template`
2207 pub fn id(&self) -> &PolicyId {
2208 PolicyId::ref_cast(self.ast.id())
2209 }
2210
2211 /// Clone this `Template` with a new `PolicyId`
2212 #[must_use]
2213 pub fn new_id(&self, id: PolicyId) -> Self {
2214 Self {
2215 ast: self.ast.new_id(id.0),
2216 lossless: self.lossless.clone(), // Lossless representation doesn't include the `PolicyId`
2217 }
2218 }
2219
2220 /// Get the `Effect` (`Forbid` or `Permit`) of this `Template`
2221 pub fn effect(&self) -> Effect {
2222 self.ast.effect()
2223 }
2224
2225 /// Get an annotation value of this `Template`
2226 pub fn annotation(&self, key: impl AsRef<str>) -> Option<&str> {
2227 self.ast
2228 .annotation(&key.as_ref().parse().ok()?)
2229 .map(smol_str::SmolStr::as_str)
2230 }
2231
2232 /// Iterate through annotation data of this `Template` as key-value pairs
2233 pub fn annotations(&self) -> impl Iterator<Item = (&str, &str)> {
2234 self.ast
2235 .annotations()
2236 .map(|(k, v)| (k.as_ref(), v.as_str()))
2237 }
2238
2239 /// Iterate over the open slots in this `Template`
2240 pub fn slots(&self) -> impl Iterator<Item = &SlotId> {
2241 self.ast.slots().map(SlotId::ref_cast)
2242 }
2243
2244 /// Get the head constraint on this policy's principal
2245 pub fn principal_constraint(&self) -> TemplatePrincipalConstraint {
2246 match self.ast.principal_constraint().as_inner() {
2247 ast::PrincipalOrResourceConstraint::Any => TemplatePrincipalConstraint::Any,
2248 ast::PrincipalOrResourceConstraint::In(eref) => {
2249 TemplatePrincipalConstraint::In(match eref {
2250 ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
2251 ast::EntityReference::Slot => None,
2252 })
2253 }
2254 ast::PrincipalOrResourceConstraint::Eq(eref) => {
2255 TemplatePrincipalConstraint::Eq(match eref {
2256 ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
2257 ast::EntityReference::Slot => None,
2258 })
2259 }
2260 ast::PrincipalOrResourceConstraint::Is(entity_type) => {
2261 TemplatePrincipalConstraint::Is(EntityTypeName(entity_type.clone()))
2262 }
2263 ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
2264 TemplatePrincipalConstraint::IsIn(
2265 EntityTypeName(entity_type.clone()),
2266 match eref {
2267 ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
2268 ast::EntityReference::Slot => None,
2269 },
2270 )
2271 }
2272 }
2273 }
2274
2275 /// Get the head constraint on this policy's action
2276 pub fn action_constraint(&self) -> ActionConstraint {
2277 // Clone the data from Core to be consistent with the other constraints
2278 match self.ast.action_constraint() {
2279 ast::ActionConstraint::Any => ActionConstraint::Any,
2280 ast::ActionConstraint::In(ids) => ActionConstraint::In(
2281 ids.iter()
2282 .map(|id| EntityUid(id.as_ref().clone()))
2283 .collect(),
2284 ),
2285 ast::ActionConstraint::Eq(id) => ActionConstraint::Eq(EntityUid(id.as_ref().clone())),
2286 }
2287 }
2288
2289 /// Get the head constraint on this policy's resource
2290 pub fn resource_constraint(&self) -> TemplateResourceConstraint {
2291 match self.ast.resource_constraint().as_inner() {
2292 ast::PrincipalOrResourceConstraint::Any => TemplateResourceConstraint::Any,
2293 ast::PrincipalOrResourceConstraint::In(eref) => {
2294 TemplateResourceConstraint::In(match eref {
2295 ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
2296 ast::EntityReference::Slot => None,
2297 })
2298 }
2299 ast::PrincipalOrResourceConstraint::Eq(eref) => {
2300 TemplateResourceConstraint::Eq(match eref {
2301 ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
2302 ast::EntityReference::Slot => None,
2303 })
2304 }
2305 ast::PrincipalOrResourceConstraint::Is(entity_type) => {
2306 TemplateResourceConstraint::Is(EntityTypeName(entity_type.clone()))
2307 }
2308 ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
2309 TemplateResourceConstraint::IsIn(
2310 EntityTypeName(entity_type.clone()),
2311 match eref {
2312 ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
2313 ast::EntityReference::Slot => None,
2314 },
2315 )
2316 }
2317 }
2318 }
2319
2320 /// Create a `Template` from its JSON representation.
2321 /// If `id` is Some, the policy will be given that Policy Id.
2322 /// If `id` is None, then "JSON policy" will be used.
2323 /// The behavior around None may change in the future.
2324 pub fn from_json(
2325 id: Option<PolicyId>,
2326 json: serde_json::Value,
2327 ) -> Result<Self, cedar_policy_core::est::FromJsonError> {
2328 let est: est::Policy =
2329 serde_json::from_value(json).map_err(JsonDeserializationError::Serde)?;
2330 Ok(Self {
2331 ast: est.clone().try_into_ast_template(id.map(|id| id.0))?,
2332 lossless: LosslessPolicy::Est(est),
2333 })
2334 }
2335
2336 /// Get the JSON representation of this `Template`.
2337 pub fn to_json(&self) -> Result<serde_json::Value, impl std::error::Error> {
2338 let est = self.lossless.est()?;
2339 let json = serde_json::to_value(est)?;
2340 Ok::<_, PolicyToJsonError>(json)
2341 }
2342
2343 /// Create a `Template` from its AST representation only. The EST will
2344 /// reflect the AST structure. When possible, don't use this method and
2345 /// create the EST from the policy text or CST instead, as the conversion
2346 /// to AST is lossy. ESTs generated by this method will reflect the AST and
2347 /// not the original policy syntax.
2348 #[cfg_attr(not(feature = "partial-eval"), allow(unused))]
2349 fn from_ast(ast: ast::Template) -> Self {
2350 let text = ast.to_string(); // assume that pretty-printing is faster than `est::Policy::from(ast.clone())`; is that true?
2351 Self {
2352 ast,
2353 lossless: LosslessPolicy::policy_or_template_text(text),
2354 }
2355 }
2356}
2357
2358impl std::fmt::Display for Template {
2359 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2360 // prefer to display the lossless format
2361 self.lossless.fmt(f)
2362 }
2363}
2364
2365impl FromStr for Template {
2366 type Err = ParseErrors;
2367
2368 fn from_str(src: &str) -> Result<Self, Self::Err> {
2369 Self::parse(None, src)
2370 }
2371}
2372
2373/// Head constraint on policy principals.
2374#[derive(Debug, Clone, PartialEq, Eq)]
2375pub enum PrincipalConstraint {
2376 /// Un-constrained
2377 Any,
2378 /// Must be In the given EntityUid
2379 In(EntityUid),
2380 /// Must be equal to the given EntityUid
2381 Eq(EntityUid),
2382 /// Must be the given EntityTypeName
2383 Is(EntityTypeName),
2384 /// Must be the given EntityTypeName, and `in` the EntityUID
2385 IsIn(EntityTypeName, EntityUid),
2386}
2387
2388/// Head constraint on policy principals for templates.
2389#[derive(Debug, Clone, PartialEq, Eq)]
2390pub enum TemplatePrincipalConstraint {
2391 /// Un-constrained
2392 Any,
2393 /// Must be In the given EntityUid.
2394 /// If [`None`], then it is a template slot.
2395 In(Option<EntityUid>),
2396 /// Must be equal to the given EntityUid.
2397 /// If [`None`], then it is a template slot.
2398 Eq(Option<EntityUid>),
2399 /// Must be the given EntityTypeName.
2400 Is(EntityTypeName),
2401 /// Must be the given EntityTypeName, and `in` the EntityUID.
2402 /// If the EntityUID is [`None`], then it is a template slot.
2403 IsIn(EntityTypeName, Option<EntityUid>),
2404}
2405
2406impl TemplatePrincipalConstraint {
2407 /// Does this constraint contain a slot?
2408 pub fn has_slot(&self) -> bool {
2409 match self {
2410 Self::Any | Self::Is(_) => false,
2411 Self::In(o) | Self::Eq(o) | Self::IsIn(_, o) => o.is_none(),
2412 }
2413 }
2414}
2415
2416/// Head constraint on policy actions.
2417#[derive(Debug, Clone, PartialEq, Eq)]
2418pub enum ActionConstraint {
2419 /// Un-constrained
2420 Any,
2421 /// Must be In the given EntityUid
2422 In(Vec<EntityUid>),
2423 /// Must be equal to the given EntityUid
2424 Eq(EntityUid),
2425}
2426
2427/// Head constraint on policy resources.
2428#[derive(Debug, Clone, PartialEq, Eq)]
2429pub enum ResourceConstraint {
2430 /// Un-constrained
2431 Any,
2432 /// Must be In the given EntityUid
2433 In(EntityUid),
2434 /// Must be equal to the given EntityUid
2435 Eq(EntityUid),
2436 /// Must be the given EntityTypeName
2437 Is(EntityTypeName),
2438 /// Must be the given EntityTypeName, and `in` the EntityUID
2439 IsIn(EntityTypeName, EntityUid),
2440}
2441
2442/// Head constraint on policy resources for templates.
2443#[derive(Debug, Clone, PartialEq, Eq)]
2444pub enum TemplateResourceConstraint {
2445 /// Un-constrained
2446 Any,
2447 /// Must be In the given EntityUid.
2448 /// If [`None`], then it is a template slot.
2449 In(Option<EntityUid>),
2450 /// Must be equal to the given EntityUid.
2451 /// If [`None`], then it is a template slot.
2452 Eq(Option<EntityUid>),
2453 /// Must be the given EntityTypeName.
2454 Is(EntityTypeName),
2455 /// Must be the given EntityTypeName, and `in` the EntityUID.
2456 /// If the EntityUID is [`None`], then it is a template slot.
2457 IsIn(EntityTypeName, Option<EntityUid>),
2458}
2459
2460impl TemplateResourceConstraint {
2461 /// Does this constraint contain a slot?
2462 pub fn has_slot(&self) -> bool {
2463 match self {
2464 Self::Any | Self::Is(_) => false,
2465 Self::In(o) | Self::Eq(o) | Self::IsIn(_, o) => o.is_none(),
2466 }
2467 }
2468}
2469
2470/// Unique Ids assigned to policies and templates
2471#[repr(transparent)]
2472#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize, RefCast)]
2473pub struct PolicyId(ast::PolicyID);
2474
2475impl FromStr for PolicyId {
2476 type Err = ParseErrors;
2477
2478 /// Create a `PolicyId` from a string. Currently always returns Ok().
2479 fn from_str(id: &str) -> Result<Self, Self::Err> {
2480 Ok(Self(ast::PolicyID::from_string(id)))
2481 }
2482}
2483
2484impl std::fmt::Display for PolicyId {
2485 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2486 write!(f, "{}", self.0)
2487 }
2488}
2489
2490/// Structure for a `Policy`. Includes both static policies and template-linked policies.
2491#[derive(Debug, Clone)]
2492pub struct Policy {
2493 /// AST representation of the policy, used for most operations.
2494 /// In particular, the `ast` contains the authoritative `PolicyId` for the policy.
2495 ast: ast::Policy,
2496 /// Some "lossless" representation of the policy, whichever is most
2497 /// convenient to provide (and can be provided with the least overhead).
2498 /// This is used just for `to_json()`.
2499 /// We can't just derive this on-demand from `ast`, because the AST is lossy:
2500 /// we can't reconstruct an accurate CST/EST/policy-text from the AST, but
2501 /// we can from the EST (modulo whitespace and a few other things like the
2502 /// order of annotations).
2503 lossless: LosslessPolicy,
2504}
2505
2506impl PartialEq for Policy {
2507 fn eq(&self, other: &Self) -> bool {
2508 // eq is based on just the `ast`
2509 self.ast.eq(&other.ast)
2510 }
2511}
2512impl Eq for Policy {}
2513
2514impl Policy {
2515 /// Get the `PolicyId` of the `Template` this is linked to.
2516 /// If this is a static policy, this will return `None`.
2517 pub fn template_id(&self) -> Option<&PolicyId> {
2518 if self.is_static() {
2519 None
2520 } else {
2521 Some(PolicyId::ref_cast(self.ast.template().id()))
2522 }
2523 }
2524
2525 /// Get the `Effect` (`Permit` or `Forbid`) for this instance
2526 pub fn effect(&self) -> Effect {
2527 self.ast.effect()
2528 }
2529
2530 /// Get an annotation value of this template-linked or static policy
2531 pub fn annotation(&self, key: impl AsRef<str>) -> Option<&str> {
2532 self.ast
2533 .annotation(&key.as_ref().parse().ok()?)
2534 .map(smol_str::SmolStr::as_str)
2535 }
2536
2537 /// Iterate through annotation data of this template-linked or static policy
2538 pub fn annotations(&self) -> impl Iterator<Item = (&str, &str)> {
2539 self.ast
2540 .annotations()
2541 .map(|(k, v)| (k.as_ref(), v.as_str()))
2542 }
2543
2544 /// Get the `PolicyId` for this template-linked or static policy
2545 pub fn id(&self) -> &PolicyId {
2546 PolicyId::ref_cast(self.ast.id())
2547 }
2548
2549 /// Clone this `Policy` with a new `PolicyId`
2550 #[must_use]
2551 pub fn new_id(&self, id: PolicyId) -> Self {
2552 Self {
2553 ast: self.ast.new_id(id.0),
2554 lossless: self.lossless.clone(), // Lossless representation doesn't include the `PolicyId`
2555 }
2556 }
2557
2558 /// Returns `true` if this is a static policy, `false` otherwise.
2559 pub fn is_static(&self) -> bool {
2560 self.ast.is_static()
2561 }
2562
2563 /// Get the head constraint on this policy's principal
2564 pub fn principal_constraint(&self) -> PrincipalConstraint {
2565 let slot_id = ast::SlotId::principal();
2566 match self.ast.template().principal_constraint().as_inner() {
2567 ast::PrincipalOrResourceConstraint::Any => PrincipalConstraint::Any,
2568 ast::PrincipalOrResourceConstraint::In(eref) => {
2569 PrincipalConstraint::In(self.convert_entity_reference(eref, slot_id).clone())
2570 }
2571 ast::PrincipalOrResourceConstraint::Eq(eref) => {
2572 PrincipalConstraint::Eq(self.convert_entity_reference(eref, slot_id).clone())
2573 }
2574 ast::PrincipalOrResourceConstraint::Is(entity_type) => {
2575 PrincipalConstraint::Is(EntityTypeName(entity_type.clone()))
2576 }
2577 ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
2578 PrincipalConstraint::IsIn(
2579 EntityTypeName(entity_type.clone()),
2580 self.convert_entity_reference(eref, slot_id).clone(),
2581 )
2582 }
2583 }
2584 }
2585
2586 /// Get the head constraint on this policy's action
2587 pub fn action_constraint(&self) -> ActionConstraint {
2588 // Clone the data from Core to be consistant with the other constraints
2589 // INVARIANT: all of the EntityUids come from a policy, which must have Concrete EntityUids
2590 match self.ast.template().action_constraint() {
2591 ast::ActionConstraint::Any => ActionConstraint::Any,
2592 ast::ActionConstraint::In(ids) => ActionConstraint::In(
2593 ids.iter()
2594 .map(|euid| EntityUid::ref_cast(euid.as_ref()))
2595 .cloned()
2596 .collect(),
2597 ),
2598 ast::ActionConstraint::Eq(id) => ActionConstraint::Eq(EntityUid::ref_cast(id).clone()),
2599 }
2600 }
2601
2602 /// Get the head constraint on this policy's resource
2603 pub fn resource_constraint(&self) -> ResourceConstraint {
2604 let slot_id = ast::SlotId::resource();
2605 match self.ast.template().resource_constraint().as_inner() {
2606 ast::PrincipalOrResourceConstraint::Any => ResourceConstraint::Any,
2607 ast::PrincipalOrResourceConstraint::In(eref) => {
2608 ResourceConstraint::In(self.convert_entity_reference(eref, slot_id).clone())
2609 }
2610 ast::PrincipalOrResourceConstraint::Eq(eref) => {
2611 ResourceConstraint::Eq(self.convert_entity_reference(eref, slot_id).clone())
2612 }
2613 ast::PrincipalOrResourceConstraint::Is(entity_type) => {
2614 ResourceConstraint::Is(EntityTypeName(entity_type.clone()))
2615 }
2616 ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
2617 ResourceConstraint::IsIn(
2618 EntityTypeName(entity_type.clone()),
2619 self.convert_entity_reference(eref, slot_id).clone(),
2620 )
2621 }
2622 }
2623 }
2624
2625 /// To avoid panicking, this function may only be called when `slot` is the
2626 /// `SlotId` corresponding to the scope constraint from which the entity
2627 /// reference `r` was extracted. I.e., If `r` is taken from the principal
2628 /// scope constraint, `slot` must be `?principal`. This ensures that the
2629 /// `SlotId` exists in the policy (and therefore the slot environment map)
2630 /// whenever the `EntityReference` `r` is the Slot variant.
2631 fn convert_entity_reference<'a>(
2632 &'a self,
2633 r: &'a ast::EntityReference,
2634 slot: ast::SlotId,
2635 ) -> &'a EntityUid {
2636 match r {
2637 // INVARIANT: this comes from policy source, so must be concrete
2638 ast::EntityReference::EUID(euid) => EntityUid::ref_cast(euid),
2639 // PANIC SAFETY: This `unwrap` here is safe due the invariant (values total map) on policies.
2640 #[allow(clippy::unwrap_used)]
2641 ast::EntityReference::Slot => EntityUid::ref_cast(self.ast.env().get(&slot).unwrap()),
2642 }
2643 }
2644
2645 /// Parse a single policy.
2646 /// If `id` is Some, the policy will be given that Policy Id.
2647 /// If `id` is None, then "policy0" will be used.
2648 /// The behavior around None may change in the future.
2649 ///
2650 /// This can fail if the policy fails to parse.
2651 /// It can also fail if a template was passed in, as this function only accepts static
2652 /// policies
2653 pub fn parse(id: Option<String>, policy_src: impl AsRef<str>) -> Result<Self, ParseErrors> {
2654 let inline_ast = parser::parse_policy(id, policy_src.as_ref())?;
2655 let (_, ast) = ast::Template::link_static_policy(inline_ast);
2656 Ok(Self {
2657 ast,
2658 lossless: LosslessPolicy::policy_or_template_text(policy_src.as_ref()),
2659 })
2660 }
2661
2662 /// Create a `Policy` from its JSON representation.
2663 /// If `id` is Some, the policy will be given that Policy Id.
2664 /// If `id` is None, then "JSON policy" will be used.
2665 /// The behavior around None may change in the future.
2666 ///
2667 /// ```
2668 /// use cedar_policy::{Policy, PolicyId};
2669 /// use std::str::FromStr;
2670 ///
2671 /// let json: serde_json::Value = serde_json::json!(
2672 /// {
2673 /// "effect":"permit",
2674 /// "principal":{
2675 /// "op":"==",
2676 /// "entity":{
2677 /// "type":"User",
2678 /// "id":"bob"
2679 /// }
2680 /// },
2681 /// "action":{
2682 /// "op":"==",
2683 /// "entity":{
2684 /// "type":"Action",
2685 /// "id":"view"
2686 /// }
2687 /// },
2688 /// "resource":{
2689 /// "op":"==",
2690 /// "entity":{
2691 /// "type":"Album",
2692 /// "id":"trip"
2693 /// }
2694 /// },
2695 /// "conditions":[
2696 /// {
2697 /// "kind":"when",
2698 /// "body":{
2699 /// ">":{
2700 /// "left":{
2701 /// ".":{
2702 /// "left":{
2703 /// "Var":"principal"
2704 /// },
2705 /// "attr":"age"
2706 /// }
2707 /// },
2708 /// "right":{
2709 /// "Value":18
2710 /// }
2711 /// }
2712 /// }
2713 /// }
2714 /// ]
2715 /// }
2716 /// );
2717 /// let json_policy = Policy::from_json(None, json).unwrap();
2718 /// let src = r#"
2719 /// permit(
2720 /// principal == User::"bob",
2721 /// action == Action::"view",
2722 /// resource == Album::"trip"
2723 /// )
2724 /// when { principal.age > 18 };"#;
2725 /// let text_policy = Policy::parse(None, src).unwrap();
2726 /// assert_eq!(json_policy.to_json().unwrap(), text_policy.to_json().unwrap());
2727 /// ```
2728 pub fn from_json(
2729 id: Option<PolicyId>,
2730 json: serde_json::Value,
2731 ) -> Result<Self, cedar_policy_core::est::FromJsonError> {
2732 let est: est::Policy =
2733 serde_json::from_value(json).map_err(JsonDeserializationError::Serde)?;
2734 Ok(Self {
2735 ast: est.clone().try_into_ast_policy(id.map(|id| id.0))?,
2736 lossless: LosslessPolicy::Est(est),
2737 })
2738 }
2739
2740 /// Get the JSON representation of this `Policy`.
2741 /// ```
2742 /// # use cedar_policy::Policy;
2743 /// let src = r#"
2744 /// permit(
2745 /// principal == User::"bob",
2746 /// action == Action::"view",
2747 /// resource == Album::"trip"
2748 /// )
2749 /// when { principal.age > 18 };"#;
2750 ///
2751 /// let policy = Policy::parse(None, src).unwrap();
2752 /// println!("{}", policy);
2753 /// // convert the policy to JSON
2754 /// let json = policy.to_json().unwrap();
2755 /// println!("{}", json);
2756 /// assert_eq!(json, Policy::from_json(None, json.clone()).unwrap().to_json().unwrap());
2757 /// ```
2758 pub fn to_json(&self) -> Result<serde_json::Value, impl std::error::Error> {
2759 let est = self.lossless.est()?;
2760 let json = serde_json::to_value(est)?;
2761 Ok::<_, PolicyToJsonError>(json)
2762 }
2763
2764 /// Create a `Policy` from its AST representation only. The `LosslessPolicy`
2765 /// will reflect the AST structure. When possible, don't use this method and
2766 /// create the `Policy` from the policy text, CST, or EST instead, as the
2767 /// conversion to AST is lossy. ESTs for policies generated by this method
2768 /// will reflect the AST and not the original policy syntax.
2769 #[cfg_attr(not(feature = "partial-eval"), allow(unused))]
2770 pub(crate) fn from_ast(ast: ast::Policy) -> Self {
2771 let text = ast.to_string(); // assume that pretty-printing is faster than `est::Policy::from(ast.clone())`; is that true?
2772 Self {
2773 ast,
2774 lossless: LosslessPolicy::policy_or_template_text(text),
2775 }
2776 }
2777}
2778
2779impl std::fmt::Display for Policy {
2780 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2781 // prefer to display the lossless format
2782 self.lossless.fmt(f)
2783 }
2784}
2785
2786impl FromStr for Policy {
2787 type Err = ParseErrors;
2788 /// Create a policy
2789 ///
2790 /// Important note: Policies have ids, but this interface does not
2791 /// allow them to be set. It will use the default "policy0", which
2792 /// may cause id conflicts if not handled. Use `Policy::parse` to set
2793 /// the id when parsing, or `Policy::new_id` to clone a policy with
2794 /// a new id.
2795 fn from_str(policy: &str) -> Result<Self, Self::Err> {
2796 Self::parse(None, policy)
2797 }
2798}
2799
2800/// See comments on `Policy` and `Template`.
2801///
2802/// This structure can be used for static policies, linked policies, and templates.
2803#[derive(Debug, Clone)]
2804enum LosslessPolicy {
2805 /// EST representation
2806 Est(est::Policy),
2807 /// Text representation
2808 Text {
2809 /// actual policy text, of the policy or template
2810 text: String,
2811 /// For linked policies, map of slot to UID. Only linked policies have
2812 /// this; static policies and (unlinked) templates have an empty map
2813 /// here
2814 slots: HashMap<ast::SlotId, ast::EntityUID>,
2815 },
2816}
2817
2818impl LosslessPolicy {
2819 /// Create a new `LosslessPolicy` from the text of a policy or template.
2820 fn policy_or_template_text(text: impl Into<String>) -> Self {
2821 Self::Text {
2822 text: text.into(),
2823 slots: HashMap::new(),
2824 }
2825 }
2826
2827 /// Get the EST representation of this static policy, linked policy, or template
2828 fn est(&self) -> Result<est::Policy, PolicyToJsonError> {
2829 match self {
2830 Self::Est(est) => Ok(est.clone()),
2831 Self::Text { text, slots } => {
2832 let est = parser::parse_policy_or_template_to_est(text)?;
2833 if slots.is_empty() {
2834 Ok(est)
2835 } else {
2836 let unwrapped_vals = slots.iter().map(|(k, v)| (*k, v.into())).collect();
2837 Ok(est.link(&unwrapped_vals)?)
2838 }
2839 }
2840 }
2841 }
2842
2843 fn link<'a>(
2844 self,
2845 vals: impl IntoIterator<Item = (ast::SlotId, &'a ast::EntityUID)>,
2846 ) -> Result<Self, est::InstantiationError> {
2847 match self {
2848 Self::Est(est) => {
2849 let unwrapped_est_vals: HashMap<ast::SlotId, entities::EntityUidJson> =
2850 vals.into_iter().map(|(k, v)| (k, v.into())).collect();
2851 Ok(Self::Est(est.link(&unwrapped_est_vals)?))
2852 }
2853 Self::Text { text, slots } => {
2854 debug_assert!(
2855 slots.is_empty(),
2856 "shouldn't call link() on an already-linked policy"
2857 );
2858 let slots = vals.into_iter().map(|(k, v)| (k, v.clone())).collect();
2859 Ok(Self::Text { text, slots })
2860 }
2861 }
2862 }
2863}
2864
2865impl std::fmt::Display for LosslessPolicy {
2866 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2867 match self {
2868 Self::Est(est) => write!(f, "{est}"),
2869 Self::Text { text, slots } => {
2870 if slots.is_empty() {
2871 write!(f, "{text}")
2872 } else {
2873 // need to replace placeholders according to `slots`.
2874 // just find-and-replace wouldn't be safe/perfect, we
2875 // want to use the actual parser; right now we reuse
2876 // another implementation by just converting to EST and
2877 // printing that
2878 match self.est() {
2879 Ok(est) => write!(f, "{est}"),
2880 Err(e) => write!(f, "<invalid linked policy: {e}>"),
2881 }
2882 }
2883 }
2884 }
2885 }
2886}
2887
2888/// Errors that can happen when getting the JSON representation of a policy
2889#[derive(Debug, Error)]
2890pub enum PolicyToJsonError {
2891 /// Parse error in the policy text
2892 #[error(transparent)]
2893 Parse(#[from] ParseErrors),
2894 /// For linked policies, error linking the JSON representation
2895 #[error(transparent)]
2896 Link(#[from] est::InstantiationError),
2897 /// Error in the JSON serialization
2898 #[error(transparent)]
2899 Serde(#[from] serde_json::Error),
2900}
2901
2902/// Expressions to be evaluated
2903#[repr(transparent)]
2904#[derive(Debug, Clone, RefCast)]
2905pub struct Expression(ast::Expr);
2906
2907impl Expression {
2908 /// Create an expression representing a literal string.
2909 pub fn new_string(value: String) -> Self {
2910 Self(ast::Expr::val(value))
2911 }
2912
2913 /// Create an expression representing a literal bool.
2914 pub fn new_bool(value: bool) -> Self {
2915 Self(ast::Expr::val(value))
2916 }
2917
2918 /// Create an expression representing a literal long.
2919 pub fn new_long(value: i64) -> Self {
2920 Self(ast::Expr::val(value))
2921 }
2922
2923 /// Create an expression representing a record.
2924 ///
2925 /// Error if any key appears two or more times in `fields`.
2926 pub fn new_record(
2927 fields: impl IntoIterator<Item = (String, Self)>,
2928 ) -> Result<Self, ExprConstructionError> {
2929 Ok(Self(ast::Expr::record(
2930 fields.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
2931 )?))
2932 }
2933
2934 /// Create an expression representing a Set.
2935 pub fn new_set(values: impl IntoIterator<Item = Self>) -> Self {
2936 Self(ast::Expr::set(values.into_iter().map(|v| v.0)))
2937 }
2938}
2939
2940impl FromStr for Expression {
2941 type Err = ParseErrors;
2942
2943 /// create an Expression using Cedar syntax
2944 fn from_str(expression: &str) -> Result<Self, Self::Err> {
2945 ast::Expr::from_str(expression).map(Expression)
2946 }
2947}
2948
2949/// "Restricted" expressions are used for attribute values and `context`.
2950///
2951/// Restricted expressions can contain only the following:
2952/// - bool, int, and string literals
2953/// - literal `EntityUid`s such as `User::"alice"`
2954/// - extension function calls, where the arguments must be other things
2955/// on this list
2956/// - set and record literals, where the values must be other things on
2957/// this list
2958///
2959/// That means the following are not allowed in restricted expressions:
2960/// - `principal`, `action`, `resource`, `context`
2961/// - builtin operators and functions, including `.`, `in`, `has`, `like`,
2962/// `.contains()`
2963/// - if-then-else expressions
2964#[repr(transparent)]
2965#[derive(Debug, Clone, RefCast)]
2966pub struct RestrictedExpression(ast::RestrictedExpr);
2967
2968impl RestrictedExpression {
2969 /// Create an expression representing a literal string.
2970 pub fn new_string(value: String) -> Self {
2971 Self(ast::RestrictedExpr::val(value))
2972 }
2973
2974 /// Create an expression representing a literal bool.
2975 pub fn new_bool(value: bool) -> Self {
2976 Self(ast::RestrictedExpr::val(value))
2977 }
2978
2979 /// Create an expression representing a literal long.
2980 pub fn new_long(value: i64) -> Self {
2981 Self(ast::RestrictedExpr::val(value))
2982 }
2983
2984 /// Create an expression representing a literal `EntityUid`.
2985 pub fn new_entity_uid(value: EntityUid) -> Self {
2986 Self(ast::RestrictedExpr::val(value.0))
2987 }
2988
2989 /// Create an expression representing a record.
2990 ///
2991 /// Error if any key appears two or more times in `fields`.
2992 pub fn new_record(
2993 fields: impl IntoIterator<Item = (String, Self)>,
2994 ) -> Result<Self, ExprConstructionError> {
2995 Ok(Self(ast::RestrictedExpr::record(
2996 fields.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
2997 )?))
2998 }
2999
3000 /// Create an expression representing a Set.
3001 pub fn new_set(values: impl IntoIterator<Item = Self>) -> Self {
3002 Self(ast::RestrictedExpr::set(values.into_iter().map(|v| v.0)))
3003 }
3004}
3005
3006impl FromStr for RestrictedExpression {
3007 type Err = RestrictedExprParseError;
3008
3009 /// create a `RestrictedExpression` using Cedar syntax
3010 fn from_str(expression: &str) -> Result<Self, Self::Err> {
3011 ast::RestrictedExpr::from_str(expression).map(RestrictedExpression)
3012 }
3013}
3014
3015/// Builder for a [`Request`]
3016///
3017/// The default for principal, action, resource, and context fields is Unknown
3018/// for partial evaluation.
3019#[cfg(feature = "partial-eval")]
3020#[derive(Debug)]
3021pub struct RequestBuilder<'a> {
3022 principal: ast::EntityUIDEntry,
3023 action: ast::EntityUIDEntry,
3024 resource: ast::EntityUIDEntry,
3025 /// Here, `None` means unknown
3026 context: Option<ast::Context>,
3027 /// Here, `None` means no request validation is performed
3028 schema: Option<&'a Schema>,
3029}
3030
3031#[cfg(feature = "partial-eval")]
3032impl<'a> Default for RequestBuilder<'a> {
3033 fn default() -> Self {
3034 Self {
3035 principal: ast::EntityUIDEntry::Unknown,
3036 action: ast::EntityUIDEntry::Unknown,
3037 resource: ast::EntityUIDEntry::Unknown,
3038 context: None,
3039 schema: None,
3040 }
3041 }
3042}
3043
3044#[cfg(feature = "partial-eval")]
3045impl<'a> RequestBuilder<'a> {
3046 /// Set the principal.
3047 ///
3048 /// Note that you can create the `EntityUid` using `.parse()` on any
3049 /// string (via the `FromStr` implementation for `EntityUid`).
3050 ///
3051 /// Here, passing `None` for `principal` indicates that `principal` does
3052 /// not contribute to authorization decisions (e.g., because it is not
3053 /// used in your policies).
3054 /// This is different than Unknown for partial-evaluation purposes.
3055 pub fn principal(self, principal: Option<EntityUid>) -> Self {
3056 Self {
3057 principal: match principal {
3058 Some(p) => ast::EntityUIDEntry::concrete(p.0),
3059 None => ast::EntityUIDEntry::concrete(ast::EntityUID::unspecified_from_eid(
3060 ast::Eid::new("principal"),
3061 )),
3062 },
3063 ..self
3064 }
3065 }
3066
3067 /// Set the action.
3068 ///
3069 /// Note that you can create the `EntityUid` using `.parse()` on any
3070 /// string (via the `FromStr` implementation for `EntityUid`).
3071 ///
3072 /// Here, passing `None` for `action` indicates that `action` does
3073 /// not contribute to authorization decisions (e.g., because it is not
3074 /// used in your policies).
3075 /// This is different than Unknown for partial-evaluation purposes.
3076 pub fn action(self, action: Option<EntityUid>) -> Self {
3077 Self {
3078 action: match action {
3079 Some(a) => ast::EntityUIDEntry::concrete(a.0),
3080 None => ast::EntityUIDEntry::concrete(ast::EntityUID::unspecified_from_eid(
3081 ast::Eid::new("action"),
3082 )),
3083 },
3084 ..self
3085 }
3086 }
3087
3088 /// Set the resource.
3089 ///
3090 /// Note that you can create the `EntityUid` using `.parse()` on any
3091 /// string (via the `FromStr` implementation for `EntityUid`).
3092 ///
3093 /// Here, passing `None` for `resource` indicates that `resource` does
3094 /// not contribute to authorization decisions (e.g., because it is not
3095 /// used in your policies).
3096 /// This is different than Unknown for partial-evaluation purposes.
3097 pub fn resource(self, resource: Option<EntityUid>) -> Self {
3098 Self {
3099 resource: match resource {
3100 Some(r) => ast::EntityUIDEntry::concrete(r.0),
3101 None => ast::EntityUIDEntry::concrete(ast::EntityUID::unspecified_from_eid(
3102 ast::Eid::new("resource"),
3103 )),
3104 },
3105 ..self
3106 }
3107 }
3108
3109 /// Set the context.
3110 pub fn context(self, context: Context) -> Self {
3111 Self {
3112 context: Some(context.0),
3113 ..self
3114 }
3115 }
3116
3117 /// Set the schema. If present, this will be used for request validation.
3118 pub fn schema(self, schema: &'a Schema) -> Self {
3119 Self {
3120 schema: Some(schema),
3121 ..self
3122 }
3123 }
3124
3125 /// Create the [`Request`]
3126 pub fn build(self) -> Result<Request, RequestValidationError> {
3127 Ok(Request(ast::Request::new_with_unknowns(
3128 self.principal,
3129 self.action,
3130 self.resource,
3131 self.context,
3132 self.schema.map(|schema| &schema.0),
3133 Extensions::all_available(),
3134 )?))
3135 }
3136}
3137
3138/// Represents the request tuple <P, A, R, C> (see the Cedar design doc).
3139#[repr(transparent)]
3140#[derive(Debug, RefCast)]
3141pub struct Request(pub(crate) ast::Request);
3142
3143impl Request {
3144 /// Create a [`RequestBuilder`]
3145 #[cfg(feature = "partial-eval")]
3146 pub fn builder<'a>() -> RequestBuilder<'a> {
3147 RequestBuilder::default()
3148 }
3149
3150 /// Create a Request.
3151 ///
3152 /// Note that you can create the `EntityUid`s using `.parse()` on any
3153 /// string (via the `FromStr` implementation for `EntityUid`).
3154 /// The principal, action, and resource fields are optional to support
3155 /// the case where these fields do not contribute to authorization
3156 /// decisions (e.g., because they are not used in your policies).
3157 /// If any of the fields are `None`, we will automatically generate
3158 /// a unique entity UID that is not equal to any UID in the store.
3159 ///
3160 /// If `schema` is present, this constructor will validate that the
3161 /// `Request` complies with the given `schema`.
3162 pub fn new(
3163 principal: Option<EntityUid>,
3164 action: Option<EntityUid>,
3165 resource: Option<EntityUid>,
3166 context: Context,
3167 schema: Option<&Schema>,
3168 ) -> Result<Self, RequestValidationError> {
3169 let p = match principal {
3170 Some(p) => p.0,
3171 None => ast::EntityUID::unspecified_from_eid(ast::Eid::new("principal")),
3172 };
3173 let a = match action {
3174 Some(a) => a.0,
3175 None => ast::EntityUID::unspecified_from_eid(ast::Eid::new("action")),
3176 };
3177 let r = match resource {
3178 Some(r) => r.0,
3179 None => ast::EntityUID::unspecified_from_eid(ast::Eid::new("resource")),
3180 };
3181 Ok(Self(ast::Request::new(
3182 p,
3183 a,
3184 r,
3185 context.0,
3186 schema.map(|schema| &schema.0),
3187 Extensions::all_available(),
3188 )?))
3189 }
3190
3191 /// Get the principal component of the request. Returns `None` if the principal is
3192 /// "unspecified" (i.e., constructed by passing `None` into the constructor) or
3193 /// "unknown" (i.e., constructed using the partial evaluation APIs).
3194 pub fn principal(&self) -> Option<&EntityUid> {
3195 match self.0.principal() {
3196 ast::EntityUIDEntry::Known(euid) => match euid.entity_type() {
3197 // INVARIANT: we ensure Concrete-ness here
3198 ast::EntityType::Specified(_) => Some(EntityUid::ref_cast(euid.as_ref())),
3199 ast::EntityType::Unspecified => None,
3200 },
3201 ast::EntityUIDEntry::Unknown => None,
3202 }
3203 }
3204
3205 /// Get the action component of the request. Returns `None` if the action is
3206 /// "unspecified" (i.e., constructed by passing `None` into the constructor) or
3207 /// "unknown" (i.e., constructed using the partial evaluation APIs).
3208 pub fn action(&self) -> Option<&EntityUid> {
3209 match self.0.action() {
3210 ast::EntityUIDEntry::Known(euid) => match euid.entity_type() {
3211 // INVARIANT: we ensure Concrete-ness here
3212 ast::EntityType::Specified(_) => Some(EntityUid::ref_cast(euid.as_ref())),
3213 ast::EntityType::Unspecified => None,
3214 },
3215 ast::EntityUIDEntry::Unknown => None,
3216 }
3217 }
3218
3219 /// Get the resource component of the request. Returns `None` if the resource is
3220 /// "unspecified" (i.e., constructed by passing `None` into the constructor) or
3221 /// "unknown" (i.e., constructed using the partial evaluation APIs).
3222 pub fn resource(&self) -> Option<&EntityUid> {
3223 match self.0.resource() {
3224 ast::EntityUIDEntry::Known(euid) => match euid.entity_type() {
3225 // INVARIANT: we ensure Concrete-ness here
3226 ast::EntityType::Specified(_) => Some(EntityUid::ref_cast(euid.as_ref())),
3227 ast::EntityType::Unspecified => None,
3228 },
3229 ast::EntityUIDEntry::Unknown => None,
3230 }
3231 }
3232}
3233
3234/// the Context object for an authorization request
3235#[repr(transparent)]
3236#[derive(Debug, Clone, RefCast)]
3237pub struct Context(ast::Context);
3238
3239impl Context {
3240 /// Create an empty `Context`
3241 /// ```
3242 /// use cedar_policy::Context;
3243 /// let context = Context::empty();
3244 /// ```
3245 pub fn empty() -> Self {
3246 Self(ast::Context::empty())
3247 }
3248
3249 /// Create a `Context` from a map of key to "restricted expression",
3250 /// or a Vec of `(key, restricted expression)` pairs, or any other iterator
3251 /// of `(key, restricted expression)` pairs.
3252 /// ```
3253 /// use cedar_policy::{Context, RestrictedExpression};
3254 /// # use std::collections::HashMap;
3255 /// use std::str::FromStr;
3256 /// # use cedar_policy::{Entities, EntityId, EntityTypeName, EntityUid, Request,PolicySet};
3257 /// let data : serde_json::Value = serde_json::json!({
3258 /// "sub": "1234",
3259 /// "groups": {
3260 /// "1234": {
3261 /// "group_id": "abcd",
3262 /// "group_name": "test-group"
3263 /// }
3264 /// }
3265 /// });
3266 /// let mut groups: HashMap<String, RestrictedExpression> = HashMap::new();
3267 /// groups.insert("key".to_string(), RestrictedExpression::from_str(&data.to_string()).unwrap());
3268 /// groups.insert("age".to_string(), RestrictedExpression::from_str("18").unwrap());
3269 /// let context = Context::from_pairs(groups).unwrap();
3270 /// # // create a request
3271 /// # let p_eid = EntityId::from_str("alice").unwrap();
3272 /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
3273 /// # let p = EntityUid::from_type_name_and_id(p_name, p_eid);
3274 /// #
3275 /// # let a_eid = EntityId::from_str("view").unwrap();
3276 /// # let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
3277 /// # let a = EntityUid::from_type_name_and_id(a_name, a_eid);
3278 /// # let r_eid = EntityId::from_str("trip").unwrap();
3279 /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
3280 /// # let r = EntityUid::from_type_name_and_id(r_name, r_eid);
3281 /// # let request: Request = Request::new(Some(p), Some(a), Some(r), context, None).unwrap();
3282 /// ```
3283 pub fn from_pairs(
3284 pairs: impl IntoIterator<Item = (String, RestrictedExpression)>,
3285 ) -> Result<Self, ContextCreationError> {
3286 Ok(Self(ast::Context::from_pairs(
3287 pairs.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
3288 Extensions::all_available(),
3289 )?))
3290 }
3291
3292 /// Create a `Context` from a string containing JSON (which must be a JSON
3293 /// object, not any other JSON type, or you will get an error here).
3294 /// JSON here must use the `__entity` and `__extn` escapes for entity
3295 /// references, extension values, etc.
3296 ///
3297 /// If a `schema` is provided, this will inform the parsing: for instance, it
3298 /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
3299 /// if attributes have the wrong types (e.g., string instead of integer).
3300 /// Since different Actions have different schemas for `Context`, you also
3301 /// must specify the `Action` for schema-based parsing.
3302 /// ```
3303 /// use cedar_policy::{Context, RestrictedExpression};
3304 /// # use std::collections::HashMap;
3305 /// use std::str::FromStr;
3306 /// # use cedar_policy::{Entities, EntityId, EntityTypeName, EntityUid, Request,PolicySet};
3307 /// let data =r#"{
3308 /// "sub": "1234",
3309 /// "groups": {
3310 /// "1234": {
3311 /// "group_id": "abcd",
3312 /// "group_name": "test-group"
3313 /// }
3314 /// }
3315 /// }"#;
3316 /// let context = Context::from_json_str(data, None).unwrap();
3317 /// # // create a request
3318 /// # let p_eid = EntityId::from_str("alice").unwrap();
3319 /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
3320 /// # let p = EntityUid::from_type_name_and_id(p_name, p_eid);
3321 /// #
3322 /// # let a_eid = EntityId::from_str("view").unwrap();
3323 /// # let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
3324 /// # let a = EntityUid::from_type_name_and_id(a_name, a_eid);
3325 /// # let r_eid = EntityId::from_str("trip").unwrap();
3326 /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
3327 /// # let r = EntityUid::from_type_name_and_id(r_name, r_eid);
3328 /// # let request: Request = Request::new(Some(p), Some(a), Some(r), context, None).unwrap();
3329 /// ```
3330 pub fn from_json_str(
3331 json: &str,
3332 schema: Option<(&Schema, &EntityUid)>,
3333 ) -> Result<Self, ContextJsonError> {
3334 let schema = schema
3335 .map(|(s, uid)| Self::get_context_schema(s, uid))
3336 .transpose()?;
3337 let context =
3338 entities::ContextJsonParser::new(schema.as_ref(), Extensions::all_available())
3339 .from_json_str(json)?;
3340 Ok(Self(context))
3341 }
3342
3343 /// Create a `Context` from a `serde_json::Value` (which must be a JSON object,
3344 /// not any other JSON type, or you will get an error here).
3345 /// JSON here must use the `__entity` and `__extn` escapes for entity
3346 /// references, extension values, etc.
3347 ///
3348 /// If a `schema` is provided, this will inform the parsing: for instance, it
3349 /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
3350 /// if attributes have the wrong types (e.g., string instead of integer).
3351 /// Since different Actions have different schemas for `Context`, you also
3352 /// must specify the `Action` for schema-based parsing.
3353 /// ```
3354 /// use cedar_policy::{Context, RestrictedExpression, Schema};
3355 /// # use std::collections::HashMap;
3356 /// use std::str::FromStr;
3357 /// # use cedar_policy::{Entities, EntityId, EntityTypeName, EntityUid, Request,PolicySet};
3358 /// let data = serde_json::json!(
3359 /// {
3360 /// "sub": 1234
3361 /// });
3362 /// let schema_json = serde_json::json!(
3363 /// {
3364 /// "": {
3365 /// "entityTypes": {
3366 /// "User": {},
3367 /// "Album": {},
3368 /// },
3369 /// "actions": {
3370 /// "view": {
3371 /// "appliesTo": {
3372 /// "principalTypes": ["User"],
3373 /// "resourceTypes": ["Album"],
3374 /// "context": {
3375 /// "type": "Record",
3376 /// "attributes": {
3377 /// "sub": { "type": "Long" }
3378 /// }
3379 /// }
3380 /// }
3381 /// }
3382 /// }
3383 /// }
3384 /// });
3385 /// # // create a request
3386 /// # let p_eid = EntityId::from_str("alice").unwrap();
3387 /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
3388 /// # let principal = EntityUid::from_type_name_and_id(p_name, p_eid);
3389 /// let a_eid = EntityId::from_str("view").unwrap();
3390 /// let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
3391 /// let action = EntityUid::from_type_name_and_id(a_name, a_eid);
3392 /// # let r_eid = EntityId::from_str("trip").unwrap();
3393 /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
3394 /// # let resource = EntityUid::from_type_name_and_id(r_name, r_eid);
3395 /// let schema = Schema::from_json_value(schema_json).unwrap();
3396 /// let context = Context::from_json_value(data, Some((&schema, &action))).unwrap();
3397 /// # let request: Request = Request::new(Some(principal), Some(action), Some(resource), context, Some(&schema)).unwrap();
3398 /// ```
3399 pub fn from_json_value(
3400 json: serde_json::Value,
3401 schema: Option<(&Schema, &EntityUid)>,
3402 ) -> Result<Self, ContextJsonError> {
3403 let schema = schema
3404 .map(|(s, uid)| Self::get_context_schema(s, uid))
3405 .transpose()?;
3406 let context =
3407 entities::ContextJsonParser::new(schema.as_ref(), Extensions::all_available())
3408 .from_json_value(json)?;
3409 Ok(Self(context))
3410 }
3411
3412 /// Create a `Context` from a JSON file. The JSON file must contain a JSON
3413 /// object, not any other JSON type, or you will get an error here.
3414 /// JSON here must use the `__entity` and `__extn` escapes for entity
3415 /// references, extension values, etc.
3416 ///
3417 /// If a `schema` is provided, this will inform the parsing: for instance, it
3418 /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
3419 /// if attributes have the wrong types (e.g., string instead of integer).
3420 /// Since different Actions have different schemas for `Context`, you also
3421 /// must specify the `Action` for schema-based parsing.
3422 /// ```no_run
3423 /// # use cedar_policy::{Context, RestrictedExpression};
3424 /// # use cedar_policy::{Entities, EntityId, EntityTypeName, EntityUid, Request,PolicySet};
3425 /// # use std::collections::HashMap;
3426 /// # use std::str::FromStr;
3427 /// # use std::fs::File;
3428 /// let mut json = File::open("json_file.txt").unwrap();
3429 /// let context = Context::from_json_file(&json, None).unwrap();
3430 /// # // create a request
3431 /// # let p_eid = EntityId::from_str("alice").unwrap();
3432 /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
3433 /// # let p = EntityUid::from_type_name_and_id(p_name, p_eid);
3434 /// #
3435 /// # let a_eid = EntityId::from_str("view").unwrap();
3436 /// # let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
3437 /// # let a = EntityUid::from_type_name_and_id(a_name, a_eid);
3438 /// # let r_eid = EntityId::from_str("trip").unwrap();
3439 /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
3440 /// # let r = EntityUid::from_type_name_and_id(r_name, r_eid);
3441 /// # let request: Request = Request::new(Some(p), Some(a), Some(r), context, None).unwrap();
3442 /// ```
3443 pub fn from_json_file(
3444 json: impl std::io::Read,
3445 schema: Option<(&Schema, &EntityUid)>,
3446 ) -> Result<Self, ContextJsonError> {
3447 let schema = schema
3448 .map(|(s, uid)| Self::get_context_schema(s, uid))
3449 .transpose()?;
3450 let context =
3451 entities::ContextJsonParser::new(schema.as_ref(), Extensions::all_available())
3452 .from_json_file(json)?;
3453 Ok(Self(context))
3454 }
3455
3456 /// Internal helper function to convert `(&Schema, &EntityUid)` to `impl ContextSchema`
3457 fn get_context_schema(
3458 schema: &Schema,
3459 action: &EntityUid,
3460 ) -> Result<impl ContextSchema, ContextJsonError> {
3461 cedar_policy_validator::context_schema_for_action(&schema.0, &action.0).ok_or_else(|| {
3462 ContextJsonError::MissingAction {
3463 action: action.clone(),
3464 }
3465 })
3466 }
3467}
3468
3469/// Error type for parsing `Context` from JSON
3470#[derive(Debug, Error)]
3471pub enum ContextJsonError {
3472 /// Error deserializing the JSON into a Context
3473 #[error(transparent)]
3474 JsonDeserialization(#[from] ContextJsonDeserializationError),
3475 /// The supplied action doesn't exist in the supplied schema
3476 #[error("action `{action}` does not exist in the supplied schema")]
3477 MissingAction {
3478 /// UID of the action which doesn't exist
3479 action: EntityUid,
3480 },
3481}
3482
3483impl std::fmt::Display for Request {
3484 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3485 write!(f, "{}", self.0)
3486 }
3487}
3488
3489/// Result of Evaluation
3490#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
3491pub enum EvalResult {
3492 /// Boolean value
3493 Bool(bool),
3494 /// Signed integer value
3495 Long(i64),
3496 /// String value
3497 String(String),
3498 /// Entity Uid
3499 EntityUid(EntityUid),
3500 /// A first-class set
3501 Set(Set),
3502 /// A first-class anonymous record
3503 Record(Record),
3504 /// An extension value, currently limited to String results
3505 ExtensionValue(String),
3506 // ExtensionValue(std::sync::Arc<dyn InternalExtensionValue>),
3507}
3508
3509/// Sets of Cedar values
3510#[derive(Debug, Eq, PartialEq, PartialOrd, Ord)]
3511pub struct Set(BTreeSet<EvalResult>);
3512
3513impl Set {
3514 /// Iterate over the members of the set
3515 pub fn iter(&self) -> impl Iterator<Item = &EvalResult> {
3516 self.0.iter()
3517 }
3518
3519 /// Is a given element in the set
3520 pub fn contains(&self, elem: &EvalResult) -> bool {
3521 self.0.contains(elem)
3522 }
3523
3524 /// Get the number of members of the set
3525 pub fn len(&self) -> usize {
3526 self.0.len()
3527 }
3528
3529 /// Test if the set is empty
3530 pub fn is_empty(&self) -> bool {
3531 self.0.is_empty()
3532 }
3533}
3534
3535/// A record of Cedar values
3536#[derive(Debug, Eq, PartialEq, PartialOrd, Ord)]
3537pub struct Record(BTreeMap<String, EvalResult>);
3538
3539impl Record {
3540 /// Iterate over the attribute/value pairs in the record
3541 pub fn iter(&self) -> impl Iterator<Item = (&String, &EvalResult)> {
3542 self.0.iter()
3543 }
3544
3545 /// Check if a given attribute is in the record
3546 pub fn contains_attribute(&self, key: impl AsRef<str>) -> bool {
3547 self.0.contains_key(key.as_ref())
3548 }
3549
3550 /// Get a given attribute from the record
3551 pub fn get(&self, key: impl AsRef<str>) -> Option<&EvalResult> {
3552 self.0.get(key.as_ref())
3553 }
3554
3555 /// Get the number of attributes in the record
3556 pub fn len(&self) -> usize {
3557 self.0.len()
3558 }
3559
3560 /// Test if the record is empty
3561 pub fn is_empty(&self) -> bool {
3562 self.0.is_empty()
3563 }
3564}
3565
3566#[doc(hidden)]
3567impl From<ast::Value> for EvalResult {
3568 fn from(v: ast::Value) -> Self {
3569 match v {
3570 ast::Value::Lit(ast::Literal::Bool(b)) => Self::Bool(b),
3571 ast::Value::Lit(ast::Literal::Long(i)) => Self::Long(i),
3572 ast::Value::Lit(ast::Literal::String(s)) => Self::String(s.to_string()),
3573 ast::Value::Lit(ast::Literal::EntityUID(e)) => {
3574 Self::EntityUid(EntityUid(ast::EntityUID::clone(&e)))
3575 }
3576 ast::Value::Set(s) => Self::Set(Set(s
3577 .authoritative
3578 .iter()
3579 .map(|v| v.clone().into())
3580 .collect())),
3581 ast::Value::Record(r) => Self::Record(Record(
3582 r.iter()
3583 .map(|(k, v)| (k.to_string(), v.clone().into()))
3584 .collect(),
3585 )),
3586 ast::Value::ExtensionValue(v) => Self::ExtensionValue(v.to_string()),
3587 }
3588 }
3589}
3590impl std::fmt::Display for EvalResult {
3591 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3592 match self {
3593 Self::Bool(b) => write!(f, "{b}"),
3594 Self::Long(l) => write!(f, "{l}"),
3595 Self::String(s) => write!(f, "\"{}\"", s.escape_debug()),
3596 Self::EntityUid(uid) => write!(f, "{uid}"),
3597 Self::Set(s) => {
3598 write!(f, "[")?;
3599 for (i, ev) in s.iter().enumerate() {
3600 write!(f, "{ev}")?;
3601 if (i + 1) < s.len() {
3602 write!(f, ", ")?;
3603 }
3604 }
3605 write!(f, "]")?;
3606 Ok(())
3607 }
3608 Self::Record(r) => {
3609 write!(f, "{{")?;
3610 for (i, (k, v)) in r.iter().enumerate() {
3611 write!(f, "\"{}\": {v}", k.escape_debug())?;
3612 if (i + 1) < r.len() {
3613 write!(f, ", ")?;
3614 }
3615 }
3616 write!(f, "}}")?;
3617 Ok(())
3618 }
3619 Self::ExtensionValue(s) => write!(f, "{s}"),
3620 }
3621 }
3622}
3623
3624/// Evaluates an expression.
3625/// If evaluation results in an error (e.g., attempting to access a non-existent Entity or Record,
3626/// passing the wrong number of arguments to a function etc.), that error is returned as a String
3627pub fn eval_expression(
3628 request: &Request,
3629 entities: &Entities,
3630 expr: &Expression,
3631) -> Result<EvalResult, EvaluationError> {
3632 let all_ext = Extensions::all_available();
3633 let eval = Evaluator::new(request.0.clone(), &entities.0, &all_ext);
3634 Ok(EvalResult::from(
3635 // Evaluate under the empty slot map, as an expression should not have slots
3636 eval.interpret(&expr.0, &ast::SlotEnv::new())?,
3637 ))
3638}
3639
3640#[cfg(test)]
3641#[cfg(feature = "partial-eval")]
3642mod partial_eval_test {
3643 use std::collections::HashSet;
3644
3645 use crate::{AuthorizationError, PolicyId, PolicySet, ResidualResponse};
3646
3647 #[test]
3648 fn test_pe_response_constructor() {
3649 let p: PolicySet = "permit(principal, action, resource);".parse().unwrap();
3650 let reason: HashSet<PolicyId> = std::iter::once("id1".parse().unwrap()).collect();
3651 let errors: Vec<AuthorizationError> = std::iter::empty().collect();
3652 let a = ResidualResponse::new(p.clone(), reason.clone(), errors.clone());
3653 assert_eq!(a.diagnostics().errors, errors);
3654 assert_eq!(a.diagnostics().reason, reason);
3655 assert_eq!(a.residuals(), &p);
3656 }
3657}