cedar_policy_core/est/
scope_constraints.rs

1/*
2 * Copyright Cedar Contributors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17use super::{FromJsonError, LinkingError};
18use crate::ast;
19use crate::ast::EntityUID;
20use crate::entities::json::{
21    err::JsonDeserializationError, err::JsonDeserializationErrorContext, EntityUidJson,
22};
23use crate::parser::err::parse_errors;
24use serde::{Deserialize, Serialize};
25use smol_str::{SmolStr, ToSmolStr};
26use std::collections::{BTreeMap, HashMap};
27use std::sync::Arc;
28
29#[cfg(feature = "tolerant-ast")]
30static ERROR_CONSTRAINT_STR: &str = "ActionConstraint::ErrorConstraint";
31
32#[cfg(feature = "wasm")]
33extern crate tsify;
34
35/// Serde JSON structure for a principal scope constraint in the EST format
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(deny_unknown_fields)]
38#[serde(tag = "op")]
39#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
40#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
41pub enum PrincipalConstraint {
42    /// No constraint (e.g., `principal,`)
43    #[serde(alias = "all")]
44    All,
45    /// `==` constraint
46    #[serde(rename = "==")]
47    Eq(EqConstraint),
48    /// `in` constraint
49    #[serde(rename = "in")]
50    In(PrincipalOrResourceInConstraint),
51    /// `is` (and possibly `in`) constraint
52    #[serde(rename = "is")]
53    Is(PrincipalOrResourceIsConstraint),
54}
55
56/// Serde JSON structure for an action scope constraint in the EST format
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(deny_unknown_fields)]
59#[serde(tag = "op")]
60#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
61#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
62pub enum ActionConstraint {
63    /// No constraint (i.e., `action,`)
64    #[serde(alias = "all")]
65    All,
66    /// `==` constraint
67    #[serde(rename = "==")]
68    Eq(EqConstraint),
69    /// `in` constraint
70    #[serde(rename = "in")]
71    In(ActionInConstraint),
72    #[cfg(feature = "tolerant-ast")]
73    #[serde(alias = "error")]
74    /// Error node for a constraint that failed to parse
75    ErrorConstraint,
76}
77
78/// Serde JSON structure for a resource scope constraint in the EST format
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80#[serde(deny_unknown_fields)]
81#[serde(tag = "op")]
82#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
83#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
84pub enum ResourceConstraint {
85    /// No constraint (e.g., `resource,`)
86    #[serde(alias = "all")]
87    All,
88    /// `==` constraint
89    #[serde(rename = "==")]
90    Eq(EqConstraint),
91    /// `in` constraint
92    #[serde(rename = "in")]
93    In(PrincipalOrResourceInConstraint),
94    #[serde(rename = "is")]
95    /// `is` (and possibly `in`) constraint
96    Is(PrincipalOrResourceIsConstraint),
97}
98
99/// Serde JSON structure for a `==` scope constraint in the EST format
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(untagged)]
102#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
103#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
104pub enum EqConstraint {
105    /// `==` a literal entity
106    Entity {
107        /// Entity it must be `==` to
108        entity: EntityUidJson,
109    },
110    /// Template slot
111    Slot {
112        /// slot
113        #[cfg_attr(feature = "wasm", tsify(type = "string"))]
114        slot: ast::SlotId,
115    },
116}
117
118/// Serde JSON structure for an `in` scope constraint for principal/resource in
119/// the EST format
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
121#[serde(untagged)]
122#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
123#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
124pub enum PrincipalOrResourceInConstraint {
125    /// `in` a literal entity
126    Entity {
127        /// Entity it must be `in`
128        entity: EntityUidJson,
129    },
130    /// Template slot
131    Slot {
132        /// slot
133        #[cfg_attr(feature = "wasm", tsify(type = "string"))]
134        slot: ast::SlotId,
135    },
136}
137
138/// Serde JSON structure for an `is` scope constraint for principal/resource in
139/// the EST format
140#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
141#[serde(deny_unknown_fields)]
142#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
143#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
144pub struct PrincipalOrResourceIsConstraint {
145    #[cfg_attr(feature = "wasm", tsify(type = "string"))]
146    entity_type: SmolStr,
147    #[serde(skip_serializing_if = "Option::is_none")]
148    #[serde(rename = "in")]
149    in_entity: Option<PrincipalOrResourceInConstraint>,
150}
151
152/// Serde JSON structure for an `in` scope constraint for action in the EST
153/// format
154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
155#[serde(untagged)]
156#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
157#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
158pub enum ActionInConstraint {
159    /// Single entity
160    Single {
161        /// the single entity
162        entity: EntityUidJson,
163    },
164    /// Set of entities
165    Set {
166        /// the set of entities
167        entities: Vec<EntityUidJson>,
168    },
169}
170
171impl PrincipalConstraint {
172    /// Fill in any slots in the principal constraint using the values in
173    /// `vals`. Throws an error if `vals` doesn't contain a necessary mapping,
174    /// but does not throw an error if `vals` contains unused mappings.
175    pub fn link(self, vals: &HashMap<ast::SlotId, EntityUidJson>) -> Result<Self, LinkingError> {
176        match self {
177            PrincipalConstraint::All => Ok(PrincipalConstraint::All),
178            PrincipalConstraint::Eq(EqConstraint::Entity { entity }) => {
179                Ok(PrincipalConstraint::Eq(EqConstraint::Entity { entity }))
180            }
181            PrincipalConstraint::In(PrincipalOrResourceInConstraint::Entity { entity }) => Ok(
182                PrincipalConstraint::In(PrincipalOrResourceInConstraint::Entity { entity }),
183            ),
184            PrincipalConstraint::Eq(EqConstraint::Slot { slot }) => match vals.get(&slot) {
185                Some(val) => Ok(PrincipalConstraint::Eq(EqConstraint::Entity {
186                    entity: val.clone(),
187                })),
188                None => Err(LinkingError::MissedSlot { slot }),
189            },
190            PrincipalConstraint::In(PrincipalOrResourceInConstraint::Slot { slot }) => {
191                match vals.get(&slot) {
192                    Some(val) => Ok(PrincipalConstraint::In(
193                        PrincipalOrResourceInConstraint::Entity {
194                            entity: val.clone(),
195                        },
196                    )),
197                    None => Err(LinkingError::MissedSlot { slot }),
198                }
199            }
200            e @ PrincipalConstraint::Is(PrincipalOrResourceIsConstraint {
201                entity_type: _,
202                in_entity: None | Some(PrincipalOrResourceInConstraint::Entity { .. }),
203            }) => Ok(e),
204            PrincipalConstraint::Is(PrincipalOrResourceIsConstraint {
205                entity_type,
206                in_entity: Some(PrincipalOrResourceInConstraint::Slot { slot }),
207            }) => Ok(PrincipalConstraint::Is(PrincipalOrResourceIsConstraint {
208                entity_type,
209                in_entity: Some(PrincipalOrResourceInConstraint::Entity {
210                    entity: vals
211                        .get(&slot)
212                        .ok_or(LinkingError::MissedSlot { slot })?
213                        .clone(),
214                }),
215            })),
216        }
217    }
218
219    /// Substitute entity literals
220    pub fn sub_entity_literals(
221        self,
222        mapping: &BTreeMap<EntityUID, EntityUID>,
223    ) -> Result<Self, JsonDeserializationError> {
224        match self.clone() {
225            PrincipalConstraint::All
226            | PrincipalConstraint::Eq(EqConstraint::Slot { .. })
227            | PrincipalConstraint::In(PrincipalOrResourceInConstraint::Slot { .. })
228            | PrincipalConstraint::Is(PrincipalOrResourceIsConstraint {
229                in_entity: Some(PrincipalOrResourceInConstraint::Slot { .. }),
230                ..
231            })
232            | PrincipalConstraint::Is(PrincipalOrResourceIsConstraint {
233                entity_type: _,
234                in_entity: None,
235            }) => Ok(self),
236            PrincipalConstraint::Eq(EqConstraint::Entity { entity }) => {
237                let euid = entity.into_euid(|| JsonDeserializationErrorContext::EntityUid)?;
238                match mapping.get(&euid) {
239                    Some(new_euid) => Ok(PrincipalConstraint::Eq(EqConstraint::Entity {
240                        entity: new_euid.into(),
241                    })),
242                    None => Ok(self),
243                }
244            }
245            PrincipalConstraint::In(PrincipalOrResourceInConstraint::Entity { entity }) => {
246                let euid = entity.into_euid(|| JsonDeserializationErrorContext::EntityUid)?;
247                match mapping.get(&euid) {
248                    Some(new_euid) => Ok(PrincipalConstraint::In(
249                        PrincipalOrResourceInConstraint::Entity {
250                            entity: new_euid.into(),
251                        },
252                    )),
253                    None => Ok(self),
254                }
255            }
256            PrincipalConstraint::Is(PrincipalOrResourceIsConstraint {
257                entity_type: ety,
258                in_entity: Some(PrincipalOrResourceInConstraint::Entity { entity }),
259            }) => {
260                let euid = entity.into_euid(|| JsonDeserializationErrorContext::EntityUid)?;
261                match mapping.get(&euid) {
262                    Some(new_euid) => {
263                        Ok(PrincipalConstraint::Is(PrincipalOrResourceIsConstraint {
264                            entity_type: ety,
265                            in_entity: Some(PrincipalOrResourceInConstraint::Entity {
266                                entity: new_euid.into(),
267                            }),
268                        }))
269                    }
270                    None => Ok(self),
271                }
272            }
273        }
274    }
275
276    /// Returns true if this constraint has a slot.
277    pub fn has_slot(&self) -> bool {
278        match self {
279            PrincipalConstraint::All => false,
280            PrincipalConstraint::Eq(EqConstraint::Entity { .. }) => false,
281            PrincipalConstraint::Eq(EqConstraint::Slot { .. }) => true,
282            PrincipalConstraint::In(PrincipalOrResourceInConstraint::Entity { .. }) => false,
283            PrincipalConstraint::In(PrincipalOrResourceInConstraint::Slot { .. }) => true,
284            PrincipalConstraint::Is(PrincipalOrResourceIsConstraint {
285                in_entity: None | Some(PrincipalOrResourceInConstraint::Entity { .. }),
286                ..
287            }) => false,
288            PrincipalConstraint::Is(PrincipalOrResourceIsConstraint {
289                in_entity: Some(PrincipalOrResourceInConstraint::Slot { .. }),
290                ..
291            }) => true,
292        }
293    }
294}
295
296impl ResourceConstraint {
297    /// Fill in any slots in the resource constraint using the values in
298    /// `vals`. Throws an error if `vals` doesn't contain a necessary mapping,
299    /// but does not throw an error if `vals` contains unused mappings.
300    pub fn link(self, vals: &HashMap<ast::SlotId, EntityUidJson>) -> Result<Self, LinkingError> {
301        match self {
302            ResourceConstraint::All => Ok(ResourceConstraint::All),
303            ResourceConstraint::Eq(EqConstraint::Entity { entity }) => {
304                Ok(ResourceConstraint::Eq(EqConstraint::Entity { entity }))
305            }
306            ResourceConstraint::In(PrincipalOrResourceInConstraint::Entity { entity }) => Ok(
307                ResourceConstraint::In(PrincipalOrResourceInConstraint::Entity { entity }),
308            ),
309            ResourceConstraint::Eq(EqConstraint::Slot { slot }) => match vals.get(&slot) {
310                Some(val) => Ok(ResourceConstraint::Eq(EqConstraint::Entity {
311                    entity: val.clone(),
312                })),
313                None => Err(LinkingError::MissedSlot { slot }),
314            },
315            ResourceConstraint::In(PrincipalOrResourceInConstraint::Slot { slot }) => {
316                match vals.get(&slot) {
317                    Some(val) => Ok(ResourceConstraint::In(
318                        PrincipalOrResourceInConstraint::Entity {
319                            entity: val.clone(),
320                        },
321                    )),
322                    None => Err(LinkingError::MissedSlot { slot }),
323                }
324            }
325            e @ ResourceConstraint::Is(PrincipalOrResourceIsConstraint {
326                entity_type: _,
327                in_entity: None | Some(PrincipalOrResourceInConstraint::Entity { .. }),
328            }) => Ok(e),
329            ResourceConstraint::Is(PrincipalOrResourceIsConstraint {
330                entity_type,
331                in_entity: Some(PrincipalOrResourceInConstraint::Slot { slot }),
332            }) => Ok(ResourceConstraint::Is(PrincipalOrResourceIsConstraint {
333                entity_type,
334                in_entity: Some(PrincipalOrResourceInConstraint::Entity {
335                    entity: vals
336                        .get(&slot)
337                        .ok_or(LinkingError::MissedSlot { slot })?
338                        .clone(),
339                }),
340            })),
341        }
342    }
343
344    /// Substitute entity literals
345    pub fn sub_entity_literals(
346        self,
347        mapping: &BTreeMap<EntityUID, EntityUID>,
348    ) -> Result<Self, JsonDeserializationError> {
349        match self.clone() {
350            ResourceConstraint::All
351            | ResourceConstraint::Eq(EqConstraint::Slot { .. })
352            | ResourceConstraint::In(PrincipalOrResourceInConstraint::Slot { .. })
353            | ResourceConstraint::Is(PrincipalOrResourceIsConstraint {
354                in_entity: Some(PrincipalOrResourceInConstraint::Slot { .. }),
355                ..
356            })
357            | ResourceConstraint::Is(PrincipalOrResourceIsConstraint {
358                entity_type: _,
359                in_entity: None,
360            }) => Ok(self),
361            ResourceConstraint::Eq(EqConstraint::Entity { entity }) => {
362                let euid = entity.into_euid(|| JsonDeserializationErrorContext::EntityUid)?;
363                match mapping.get(&euid) {
364                    Some(new_euid) => Ok(ResourceConstraint::Eq(EqConstraint::Entity {
365                        entity: new_euid.into(),
366                    })),
367                    None => Ok(self),
368                }
369            }
370            ResourceConstraint::In(PrincipalOrResourceInConstraint::Entity { entity }) => {
371                let euid = entity.into_euid(|| JsonDeserializationErrorContext::EntityUid)?;
372                match mapping.get(&euid) {
373                    Some(new_euid) => Ok(ResourceConstraint::In(
374                        PrincipalOrResourceInConstraint::Entity {
375                            entity: new_euid.into(),
376                        },
377                    )),
378                    None => Ok(self),
379                }
380            }
381            ResourceConstraint::Is(PrincipalOrResourceIsConstraint {
382                entity_type: ety,
383                in_entity: Some(PrincipalOrResourceInConstraint::Entity { entity }),
384            }) => {
385                let euid = entity.into_euid(|| JsonDeserializationErrorContext::EntityUid)?;
386                match mapping.get(&euid) {
387                    Some(new_euid) => Ok(ResourceConstraint::Is(PrincipalOrResourceIsConstraint {
388                        entity_type: ety,
389                        in_entity: Some(PrincipalOrResourceInConstraint::Entity {
390                            entity: new_euid.into(),
391                        }),
392                    })),
393                    None => Ok(self),
394                }
395            }
396        }
397    }
398
399    /// Returns true if this constraint has a slot.
400    pub fn has_slot(&self) -> bool {
401        match self {
402            ResourceConstraint::All => false,
403            ResourceConstraint::Eq(EqConstraint::Entity { .. }) => false,
404            ResourceConstraint::In(PrincipalOrResourceInConstraint::Entity { .. }) => false,
405            ResourceConstraint::Eq(EqConstraint::Slot { .. }) => true,
406            ResourceConstraint::In(PrincipalOrResourceInConstraint::Slot { .. }) => true,
407            ResourceConstraint::Is(PrincipalOrResourceIsConstraint {
408                in_entity: None | Some(PrincipalOrResourceInConstraint::Entity { .. }),
409                ..
410            }) => false,
411            ResourceConstraint::Is(PrincipalOrResourceIsConstraint {
412                in_entity: Some(PrincipalOrResourceInConstraint::Slot { .. }),
413                ..
414            }) => true,
415        }
416    }
417}
418
419impl ActionConstraint {
420    /// Fill in any slots in the action constraint using the values in `vals`.
421    /// Throws an error if `vals` doesn't contain a necessary mapping, but does
422    /// not throw an error if `vals` contains unused mappings.
423    pub fn link(self, _vals: &HashMap<ast::SlotId, EntityUidJson>) -> Result<Self, LinkingError> {
424        // currently, slots are not allowed in action constraints
425        Ok(self)
426    }
427
428    /// Substitute entity literals
429    pub fn sub_entity_literals(
430        self,
431        mapping: &BTreeMap<EntityUID, EntityUID>,
432    ) -> Result<Self, JsonDeserializationError> {
433        match self.clone() {
434            ActionConstraint::Eq(EqConstraint::Entity { entity }) => {
435                let euid = entity.into_euid(|| JsonDeserializationErrorContext::EntityUid)?;
436                match mapping.get(&euid) {
437                    Some(new_euid) => Ok(ActionConstraint::Eq(EqConstraint::Entity {
438                        entity: new_euid.into(),
439                    })),
440                    None => Ok(self),
441                }
442            }
443            ActionConstraint::In(ActionInConstraint::Single { entity }) => {
444                let euid = entity.into_euid(|| JsonDeserializationErrorContext::EntityUid)?;
445                match mapping.get(&euid) {
446                    Some(new_euid) => Ok(ActionConstraint::In(ActionInConstraint::Single {
447                        entity: new_euid.into(),
448                    })),
449                    None => Ok(self),
450                }
451            }
452            ActionConstraint::In(ActionInConstraint::Set { entities }) => {
453                let mut new_entities: Vec<EntityUidJson> = vec![];
454                for entity in entities {
455                    let euid = entity
456                        .clone()
457                        .into_euid(|| JsonDeserializationErrorContext::EntityUid)?;
458                    match mapping.get(&euid) {
459                        Some(new_euid) => new_entities.push(new_euid.clone().into()),
460                        None => new_entities.push(entity),
461                    };
462                }
463                Ok(ActionConstraint::In(ActionInConstraint::Set {
464                    entities: new_entities,
465                }))
466            }
467            ActionConstraint::All | ActionConstraint::Eq(EqConstraint::Slot { .. }) => Ok(self),
468            #[cfg(feature = "tolerant-ast")]
469            ActionConstraint::ErrorConstraint => Ok(self),
470        }
471    }
472
473    /// Returns true if this constraint has a slot.
474    pub fn has_slot(&self) -> bool {
475        // currently, slots are not allowed in action constraints
476        false
477    }
478}
479
480impl std::fmt::Display for PrincipalConstraint {
481    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
482        match self {
483            Self::All => write!(f, "principal"),
484            Self::Eq(ec) => {
485                write!(f, "principal ")?;
486                std::fmt::Display::fmt(ec, f)?;
487                Ok(())
488            }
489            Self::In(ic) => {
490                write!(f, "principal ")?;
491                std::fmt::Display::fmt(ic, f)?;
492                Ok(())
493            }
494            Self::Is(isc) => {
495                write!(f, "principal ")?;
496                std::fmt::Display::fmt(isc, f)?;
497                Ok(())
498            }
499        }
500    }
501}
502
503impl std::fmt::Display for ActionConstraint {
504    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
505        match self {
506            Self::All => write!(f, "action"),
507            Self::Eq(ec) => {
508                write!(f, "action ")?;
509                std::fmt::Display::fmt(ec, f)?;
510                Ok(())
511            }
512            Self::In(aic) => {
513                write!(f, "action ")?;
514                std::fmt::Display::fmt(aic, f)?;
515                Ok(())
516            }
517            #[cfg(feature = "tolerant-ast")]
518            Self::ErrorConstraint => write!(f, "{ERROR_CONSTRAINT_STR}"),
519        }
520    }
521}
522
523impl std::fmt::Display for ResourceConstraint {
524    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
525        match self {
526            Self::All => write!(f, "resource"),
527            Self::Eq(ec) => {
528                write!(f, "resource ")?;
529                std::fmt::Display::fmt(ec, f)?;
530                Ok(())
531            }
532            Self::In(ic) => {
533                write!(f, "resource ")?;
534                std::fmt::Display::fmt(ic, f)?;
535                Ok(())
536            }
537            Self::Is(isc) => {
538                write!(f, "resource ")?;
539                std::fmt::Display::fmt(isc, f)?;
540                Ok(())
541            }
542        }
543    }
544}
545
546impl std::fmt::Display for EqConstraint {
547    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
548        match self {
549            Self::Entity { entity } => {
550                match entity
551                    .clone()
552                    .into_euid(|| JsonDeserializationErrorContext::EntityUid)
553                {
554                    Ok(euid) => write!(f, "== {euid}"),
555                    Err(e) => write!(f, "== (invalid entity uid: {e})"),
556                }
557            }
558            Self::Slot { slot } => write!(f, "== {slot}"),
559        }
560    }
561}
562
563impl std::fmt::Display for PrincipalOrResourceInConstraint {
564    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
565        match self {
566            Self::Entity { entity } => {
567                match entity
568                    .clone()
569                    .into_euid(|| JsonDeserializationErrorContext::EntityUid)
570                {
571                    Ok(euid) => write!(f, "in {euid}"),
572                    Err(e) => write!(f, "in (invalid entity uid: {e})"),
573                }
574            }
575            Self::Slot { slot } => write!(f, "in {slot}"),
576        }
577    }
578}
579
580impl std::fmt::Display for PrincipalOrResourceIsConstraint {
581    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
582        write!(f, "is {}", self.entity_type)?;
583        if let Some(in_entity) = &self.in_entity {
584            write!(f, " {in_entity}")?;
585        }
586        Ok(())
587    }
588}
589
590impl std::fmt::Display for ActionInConstraint {
591    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
592        match self {
593            Self::Single { entity } => {
594                match entity
595                    .clone()
596                    .into_euid(|| JsonDeserializationErrorContext::EntityUid)
597                {
598                    Ok(euid) => write!(f, "in {euid}"),
599                    Err(e) => write!(f, "in (invalid entity uid: {e})"),
600                }
601            }
602            Self::Set { entities } => {
603                write!(f, "in [")?;
604                for (i, entity) in entities.iter().enumerate() {
605                    match entity
606                        .clone()
607                        .into_euid(|| JsonDeserializationErrorContext::EntityUid)
608                    {
609                        Ok(euid) => write!(f, "{euid}"),
610                        Err(e) => write!(f, "(invalid entity uid: {e})"),
611                    }?;
612                    if i < (entities.len() - 1) {
613                        write!(f, ", ")?;
614                    }
615                }
616                write!(f, "]")?;
617                Ok(())
618            }
619        }
620    }
621}
622
623impl From<ast::PrincipalConstraint> for PrincipalConstraint {
624    fn from(constraint: ast::PrincipalConstraint) -> PrincipalConstraint {
625        constraint.constraint.into()
626    }
627}
628
629impl TryFrom<PrincipalConstraint> for ast::PrincipalConstraint {
630    type Error = FromJsonError;
631    fn try_from(constraint: PrincipalConstraint) -> Result<ast::PrincipalConstraint, Self::Error> {
632        constraint.try_into().map(ast::PrincipalConstraint::new)
633    }
634}
635
636impl From<ast::ResourceConstraint> for ResourceConstraint {
637    fn from(constraint: ast::ResourceConstraint) -> ResourceConstraint {
638        constraint.constraint.into()
639    }
640}
641
642impl TryFrom<ResourceConstraint> for ast::ResourceConstraint {
643    type Error = FromJsonError;
644    fn try_from(constraint: ResourceConstraint) -> Result<ast::ResourceConstraint, Self::Error> {
645        constraint.try_into().map(ast::ResourceConstraint::new)
646    }
647}
648
649impl From<ast::PrincipalOrResourceConstraint> for PrincipalConstraint {
650    fn from(constraint: ast::PrincipalOrResourceConstraint) -> PrincipalConstraint {
651        match constraint {
652            ast::PrincipalOrResourceConstraint::Any => PrincipalConstraint::All,
653            ast::PrincipalOrResourceConstraint::Eq(ast::EntityReference::EUID(e)) => {
654                PrincipalConstraint::Eq(EqConstraint::Entity {
655                    entity: EntityUidJson::ImplicitEntityEscape((&*e).into()),
656                })
657            }
658            ast::PrincipalOrResourceConstraint::Eq(ast::EntityReference::Slot(_)) => {
659                PrincipalConstraint::Eq(EqConstraint::Slot {
660                    slot: ast::SlotId::principal(),
661                })
662            }
663            ast::PrincipalOrResourceConstraint::In(ast::EntityReference::EUID(e)) => {
664                PrincipalConstraint::In(PrincipalOrResourceInConstraint::Entity {
665                    entity: EntityUidJson::ImplicitEntityEscape((&*e).into()),
666                })
667            }
668            ast::PrincipalOrResourceConstraint::In(ast::EntityReference::Slot(_)) => {
669                PrincipalConstraint::In(PrincipalOrResourceInConstraint::Slot {
670                    slot: ast::SlotId::principal(),
671                })
672            }
673            ast::PrincipalOrResourceConstraint::IsIn(entity_type, euid) => {
674                PrincipalConstraint::Is(PrincipalOrResourceIsConstraint {
675                    entity_type: entity_type.to_smolstr(),
676                    in_entity: Some(match euid {
677                        ast::EntityReference::EUID(e) => PrincipalOrResourceInConstraint::Entity {
678                            entity: EntityUidJson::ImplicitEntityEscape((&*e).into()),
679                        },
680                        ast::EntityReference::Slot(_) => PrincipalOrResourceInConstraint::Slot {
681                            slot: ast::SlotId::principal(),
682                        },
683                    }),
684                })
685            }
686            ast::PrincipalOrResourceConstraint::Is(entity_type) => {
687                PrincipalConstraint::Is(PrincipalOrResourceIsConstraint {
688                    entity_type: entity_type.to_smolstr(),
689                    in_entity: None,
690                })
691            }
692        }
693    }
694}
695
696impl From<ast::PrincipalOrResourceConstraint> for ResourceConstraint {
697    fn from(constraint: ast::PrincipalOrResourceConstraint) -> ResourceConstraint {
698        match constraint {
699            ast::PrincipalOrResourceConstraint::Any => ResourceConstraint::All,
700            ast::PrincipalOrResourceConstraint::Eq(ast::EntityReference::EUID(e)) => {
701                ResourceConstraint::Eq(EqConstraint::Entity {
702                    entity: EntityUidJson::ImplicitEntityEscape((&*e).into()),
703                })
704            }
705            ast::PrincipalOrResourceConstraint::Eq(ast::EntityReference::Slot(_)) => {
706                ResourceConstraint::Eq(EqConstraint::Slot {
707                    slot: ast::SlotId::resource(),
708                })
709            }
710            ast::PrincipalOrResourceConstraint::In(ast::EntityReference::EUID(e)) => {
711                ResourceConstraint::In(PrincipalOrResourceInConstraint::Entity {
712                    entity: EntityUidJson::ImplicitEntityEscape((&*e).into()),
713                })
714            }
715            ast::PrincipalOrResourceConstraint::In(ast::EntityReference::Slot(_)) => {
716                ResourceConstraint::In(PrincipalOrResourceInConstraint::Slot {
717                    slot: ast::SlotId::resource(),
718                })
719            }
720            ast::PrincipalOrResourceConstraint::IsIn(entity_type, euid) => {
721                ResourceConstraint::Is(PrincipalOrResourceIsConstraint {
722                    entity_type: entity_type.to_smolstr(),
723                    in_entity: Some(match euid {
724                        ast::EntityReference::EUID(e) => PrincipalOrResourceInConstraint::Entity {
725                            entity: EntityUidJson::ImplicitEntityEscape((&*e).into()),
726                        },
727                        ast::EntityReference::Slot(_) => PrincipalOrResourceInConstraint::Slot {
728                            slot: ast::SlotId::resource(),
729                        },
730                    }),
731                })
732            }
733            ast::PrincipalOrResourceConstraint::Is(entity_type) => {
734                ResourceConstraint::Is(PrincipalOrResourceIsConstraint {
735                    entity_type: entity_type.to_smolstr(),
736                    in_entity: None,
737                })
738            }
739        }
740    }
741}
742
743impl TryFrom<PrincipalConstraint> for ast::PrincipalOrResourceConstraint {
744    type Error = FromJsonError;
745    fn try_from(
746        constraint: PrincipalConstraint,
747    ) -> Result<ast::PrincipalOrResourceConstraint, Self::Error> {
748        match constraint {
749            PrincipalConstraint::All => Ok(ast::PrincipalOrResourceConstraint::Any),
750            PrincipalConstraint::Eq(EqConstraint::Entity { entity }) => Ok(
751                ast::PrincipalOrResourceConstraint::Eq(ast::EntityReference::EUID(Arc::new(
752                    entity.into_euid(|| JsonDeserializationErrorContext::EntityUid)?,
753                ))),
754            ),
755            PrincipalConstraint::Eq(EqConstraint::Slot { slot }) => {
756                if slot == ast::SlotId::principal() {
757                    Ok(ast::PrincipalOrResourceConstraint::Eq(
758                        ast::EntityReference::Slot(None),
759                    ))
760                } else {
761                    Err(Self::Error::InvalidSlotName)
762                }
763            }
764            PrincipalConstraint::In(PrincipalOrResourceInConstraint::Entity { entity }) => Ok(
765                ast::PrincipalOrResourceConstraint::In(ast::EntityReference::EUID(Arc::new(
766                    entity.into_euid(|| JsonDeserializationErrorContext::EntityUid)?,
767                ))),
768            ),
769            PrincipalConstraint::In(PrincipalOrResourceInConstraint::Slot { slot }) => {
770                if slot == ast::SlotId::principal() {
771                    Ok(ast::PrincipalOrResourceConstraint::In(
772                        ast::EntityReference::Slot(None),
773                    ))
774                } else {
775                    Err(Self::Error::InvalidSlotName)
776                }
777            }
778            PrincipalConstraint::Is(PrincipalOrResourceIsConstraint {
779                entity_type,
780                in_entity,
781            }) => ast::EntityType::from_normalized_str(entity_type.as_str())
782                .map_err(Self::Error::InvalidEntityType)
783                .and_then(|entity_type| {
784                    Ok(match in_entity {
785                        None => ast::PrincipalOrResourceConstraint::is_entity_type(Arc::new(
786                            entity_type,
787                        )),
788                        Some(PrincipalOrResourceInConstraint::Entity { entity }) => {
789                            ast::PrincipalOrResourceConstraint::is_entity_type_in(
790                                Arc::new(entity_type),
791                                Arc::new(
792                                    entity
793                                        .into_euid(|| JsonDeserializationErrorContext::EntityUid)?,
794                                ),
795                            )
796                        }
797                        Some(PrincipalOrResourceInConstraint::Slot { .. }) => {
798                            ast::PrincipalOrResourceConstraint::is_entity_type_in_slot(Arc::new(
799                                entity_type,
800                            ))
801                        }
802                    })
803                }),
804        }
805    }
806}
807
808impl TryFrom<ResourceConstraint> for ast::PrincipalOrResourceConstraint {
809    type Error = FromJsonError;
810    fn try_from(
811        constraint: ResourceConstraint,
812    ) -> Result<ast::PrincipalOrResourceConstraint, Self::Error> {
813        match constraint {
814            ResourceConstraint::All => Ok(ast::PrincipalOrResourceConstraint::Any),
815            ResourceConstraint::Eq(EqConstraint::Entity { entity }) => Ok(
816                ast::PrincipalOrResourceConstraint::Eq(ast::EntityReference::EUID(Arc::new(
817                    entity.into_euid(|| JsonDeserializationErrorContext::EntityUid)?,
818                ))),
819            ),
820            ResourceConstraint::Eq(EqConstraint::Slot { slot }) => {
821                if slot == ast::SlotId::resource() {
822                    Ok(ast::PrincipalOrResourceConstraint::Eq(
823                        ast::EntityReference::Slot(None),
824                    ))
825                } else {
826                    Err(Self::Error::InvalidSlotName)
827                }
828            }
829            ResourceConstraint::In(PrincipalOrResourceInConstraint::Entity { entity }) => Ok(
830                ast::PrincipalOrResourceConstraint::In(ast::EntityReference::EUID(Arc::new(
831                    entity.into_euid(|| JsonDeserializationErrorContext::EntityUid)?,
832                ))),
833            ),
834            ResourceConstraint::In(PrincipalOrResourceInConstraint::Slot { slot }) => {
835                if slot == ast::SlotId::resource() {
836                    Ok(ast::PrincipalOrResourceConstraint::In(
837                        ast::EntityReference::Slot(None),
838                    ))
839                } else {
840                    Err(Self::Error::InvalidSlotName)
841                }
842            }
843            ResourceConstraint::Is(PrincipalOrResourceIsConstraint {
844                entity_type,
845                in_entity,
846            }) => ast::EntityType::from_normalized_str(entity_type.as_str())
847                .map_err(Self::Error::InvalidEntityType)
848                .and_then(|entity_type| {
849                    Ok(match in_entity {
850                        None => ast::PrincipalOrResourceConstraint::is_entity_type(Arc::new(
851                            entity_type,
852                        )),
853                        Some(PrincipalOrResourceInConstraint::Entity { entity }) => {
854                            ast::PrincipalOrResourceConstraint::is_entity_type_in(
855                                Arc::new(entity_type),
856                                Arc::new(
857                                    entity
858                                        .into_euid(|| JsonDeserializationErrorContext::EntityUid)?,
859                                ),
860                            )
861                        }
862                        Some(PrincipalOrResourceInConstraint::Slot { .. }) => {
863                            ast::PrincipalOrResourceConstraint::is_entity_type_in_slot(Arc::new(
864                                entity_type,
865                            ))
866                        }
867                    })
868                }),
869        }
870    }
871}
872
873impl From<ast::ActionConstraint> for ActionConstraint {
874    fn from(constraint: ast::ActionConstraint) -> ActionConstraint {
875        match constraint {
876            ast::ActionConstraint::Any => ActionConstraint::All,
877            ast::ActionConstraint::Eq(e) => ActionConstraint::Eq(EqConstraint::Entity {
878                entity: EntityUidJson::ImplicitEntityEscape((&*e).into()),
879            }),
880            ast::ActionConstraint::In(es) => match &es[..] {
881                [e] => ActionConstraint::In(ActionInConstraint::Single {
882                    entity: EntityUidJson::ImplicitEntityEscape((&**e).into()),
883                }),
884                es => ActionConstraint::In(ActionInConstraint::Set {
885                    entities: es
886                        .iter()
887                        .map(|e| EntityUidJson::ImplicitEntityEscape((&**e).into()))
888                        .collect(),
889                }),
890            },
891            #[cfg(feature = "tolerant-ast")]
892            ast::ActionConstraint::ErrorConstraint => ActionConstraint::ErrorConstraint,
893        }
894    }
895}
896
897impl TryFrom<ActionConstraint> for ast::ActionConstraint {
898    type Error = FromJsonError;
899    fn try_from(constraint: ActionConstraint) -> Result<ast::ActionConstraint, Self::Error> {
900        let ast_action_constraint = match constraint {
901            ActionConstraint::All => Ok(ast::ActionConstraint::Any),
902            ActionConstraint::Eq(EqConstraint::Entity { entity }) => Ok(ast::ActionConstraint::Eq(
903                Arc::new(entity.into_euid(|| JsonDeserializationErrorContext::EntityUid)?),
904            )),
905            ActionConstraint::Eq(EqConstraint::Slot { .. }) => Err(Self::Error::ActionSlot),
906            ActionConstraint::In(ActionInConstraint::Single { entity }) => {
907                Ok(ast::ActionConstraint::In(vec![Arc::new(
908                    entity.into_euid(|| JsonDeserializationErrorContext::EntityUid)?,
909                )]))
910            }
911            ActionConstraint::In(ActionInConstraint::Set { entities }) => {
912                Ok(ast::ActionConstraint::In(
913                    entities
914                        .into_iter()
915                        .map(|e| {
916                            e.into_euid(|| JsonDeserializationErrorContext::EntityUid)
917                                .map(Arc::new)
918                        })
919                        .collect::<Result<Vec<_>, _>>()?,
920                ))
921            }
922            #[cfg(feature = "tolerant-ast")]
923            ActionConstraint::ErrorConstraint => Ok(ast::ActionConstraint::ErrorConstraint),
924        }?;
925
926        ast_action_constraint
927            .contains_only_action_types()
928            .map_err(|non_action_euids| {
929                parse_errors::InvalidActionType {
930                    euids: non_action_euids,
931                }
932                .into()
933            })
934    }
935}
936
937#[cfg(test)]
938mod test {
939    fn parse_policy(template: &str) -> crate::est::Policy {
940        let cst = crate::parser::text_to_cst::parse_policy(template)
941            .unwrap()
942            .node
943            .unwrap();
944        cst.try_into().unwrap()
945    }
946
947    fn principal_has_slot(principal_text: &str) -> bool {
948        let text = format!("permit({principal_text}, action, resource);");
949        parse_policy(&text).principal.has_slot()
950    }
951
952    fn resource_has_slot(resource_text: &str) -> bool {
953        let text = format!("permit(principal, action, {resource_text});");
954        parse_policy(&text).resource.has_slot()
955    }
956
957    #[test]
958    fn has_slot_principal_all() {
959        assert!(!principal_has_slot(r#"principal"#));
960    }
961
962    #[test]
963    fn has_slot_principal_eq_entity() {
964        assert!(!principal_has_slot(r#"principal == User::"alice""#));
965    }
966
967    #[test]
968    fn has_slot_principal_eq_slot() {
969        assert!(principal_has_slot(r#"principal == ?principal"#));
970    }
971
972    #[test]
973    fn has_slot_principal_in_entity() {
974        assert!(!principal_has_slot(r#"principal in Group::"friends""#));
975    }
976
977    #[test]
978    fn has_slot_principal_in_slot() {
979        assert!(principal_has_slot(r#"principal in ?principal"#));
980    }
981
982    #[test]
983    fn has_slot_principal_is_entity() {
984        assert!(!principal_has_slot(r#"principal is User"#));
985    }
986
987    #[test]
988    fn has_slot_principal_is_slot() {
989        assert!(principal_has_slot(r#"principal is User in ?principal"#));
990    }
991
992    #[test]
993    fn has_slot_resource_all() {
994        assert!(!resource_has_slot(r#"resource"#));
995    }
996
997    #[test]
998    fn has_slot_resource_eq_entity() {
999        assert!(!resource_has_slot(
1000            r#"resource == Photo::"VacationPhoto94.jpg""#
1001        ));
1002    }
1003
1004    #[test]
1005    fn has_slot_resource_eq_slot() {
1006        assert!(resource_has_slot(r#"resource == ?resource"#));
1007    }
1008
1009    #[test]
1010    fn has_slot_resource_in_entity() {
1011        assert!(!resource_has_slot(r#"resource in Group::"vacation""#));
1012    }
1013
1014    #[test]
1015    fn has_slot_resource_in_slot() {
1016        assert!(resource_has_slot(r#"resource in ?resource"#));
1017    }
1018
1019    #[test]
1020    fn has_slot_resource_is_entity() {
1021        assert!(!resource_has_slot(r#"resource is Photo"#));
1022    }
1023
1024    #[test]
1025    fn has_slot_resource_is_slot() {
1026        assert!(resource_has_slot(r#"resource is Photo in ?resource"#));
1027    }
1028}