Skip to main content

cedar_policy_symcc/symcc/
env.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//! This module defines the ADTs that represent the symbolic environment.
17//! The environment consists of a symbolic request, entity store, and action store.
18//!
19//! A symbolic environment is _literal_ when it consists of literal terms and
20//! interpreted functions (UDFs).
21
22use super::function::{
23    self,
24    UnaryFunction::{self, Udf, Uuf},
25};
26use super::op;
27use super::result::CompileError;
28use super::tags::SymTags;
29use super::term::{Term, TermPrim, TermVar};
30use super::term_type::TermType;
31use super::type_abbrevs::*;
32use cedar_policy::{RequestEnv, Schema};
33use cedar_policy_core::validator::ValidatorSchema;
34use cedar_policy_core::validator::{
35    types::{Attributes, OpenTag, Type},
36    ValidatorActionId,
37};
38use cedar_policy_core::validator::{ValidatorEntityType, ValidatorEntityTypeKind};
39use smol_str::{format_smolstr, SmolStr};
40use std::collections::{BTreeMap, BTreeSet};
41use std::ops::Deref;
42use std::sync::Arc;
43
44/// A symbolic request is analogous to a concrete request. It binds
45/// request variables to Terms.
46#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)]
47pub struct SymRequest {
48    pub principal: Term,
49    pub action: Term,
50    pub resource: Term,
51    pub context: Term,
52}
53
54impl SymRequest {
55    #[cfg(test)]
56    pub fn empty_sym_req() -> Self {
57        SymRequest {
58            principal: Term::Var(TermVar {
59                id: "principal".into(),
60                ty: TermType::Bool,
61            }),
62            action: Term::Var(TermVar {
63                id: "action".into(),
64                ty: TermType::Bool,
65            }),
66            resource: Term::Var(TermVar {
67                id: "resource".into(),
68                ty: TermType::Bool,
69            }),
70            context: Term::Record(Arc::new(BTreeMap::new())),
71        }
72    }
73
74    pub fn is_literal(&self) -> bool {
75        self.principal.is_literal()
76            && self.action.is_literal()
77            && self.resource.is_literal()
78            && self.context.is_literal()
79    }
80}
81
82// A symbolic entity store is analogous to the concrete entity store. The concrete
83// entity store is a map from EntityUIDs to Record values. The symbolic entity
84// store partitions this map into multiple maps:  one for each entity type. These
85// maps are represented as unary functions from entities to records. The functions
86// can be uninterpreted or defined. The type-based partition is required for
87// reduction to SMT. The symbolic store also carries a representation of the
88// `entityIn` relation on entities. The symbolic store partitions this relation
89// into multiple maps, one for each pair of an entity type and its ancestor type.
90
91/// Represents the entity data for _all entities_ of a _single type_
92#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)]
93pub struct SymEntityData {
94    pub attrs: UnaryFunction,
95    /// Let `t1` be the entity type which this `SymEntityData` represents the data for.
96    /// Let `t2` be an entity type in this map's keys.
97    /// The `UnaryFunction` here maps entities of type `t1` to the set of
98    /// entities of type `t2` which are ancestors of that entity.
99    /// (Of course, the `UnaryFunction` is on SMT-level values, so the domain is
100    /// essentially `t1`-typed `Term`s, and the output is essentially a
101    /// `Set<t2>`-typed `Term`.)
102    pub ancestors: BTreeMap<EntityType, UnaryFunction>,
103    /// Specifies EIDs of enum members for this entity type, if applicable
104    pub members: Option<BTreeSet<SmolStr>>,
105    /// `None` means this entity type has no tags
106    pub tags: Option<SymTags>,
107}
108
109impl SymEntityData {
110    pub fn is_literal(&self) -> bool {
111        self.attrs.is_literal()
112            && self.ancestors.values().all(|uf| uf.is_literal())
113            && self.tags.as_ref().is_none_or(|t| t.is_literal())
114    }
115}
116
117#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)]
118pub struct SymEntities(pub BTreeMap<EntityType, SymEntityData>);
119
120impl Deref for SymEntities {
121    type Target = BTreeMap<EntityType, SymEntityData>;
122
123    fn deref(&self) -> &Self::Target {
124        &self.0
125    }
126}
127
128impl SymEntities {
129    pub fn attrs(&self, ety: &EntityType) -> Option<&UnaryFunction> {
130        Some(&self.get(ety)?.attrs)
131    }
132
133    pub fn ancestors(&self, ety: &EntityType) -> Option<&BTreeMap<EntityType, UnaryFunction>> {
134        Some(&self.get(ety)?.ancestors)
135    }
136
137    pub fn ancestors_of_type(
138        &self,
139        ety: &EntityType,
140        anc_ty: &EntityType,
141    ) -> Option<&UnaryFunction> {
142        self.ancestors(ety)?.get(anc_ty)
143    }
144
145    pub fn is_valid_entity_type(&self, ety: &EntityType) -> bool {
146        self.contains_key(ety)
147    }
148
149    pub fn is_valid_entity_uid(&self, uid: &EntityUID) -> bool {
150        match self.get(uid.type_name()) {
151            Some(d) => {
152                if let Some(eids) = &d.members {
153                    eids.contains(AsRef::<str>::as_ref(uid.id()))
154                } else {
155                    true
156                }
157            }
158            None => false,
159        }
160    }
161
162    pub fn tags(&self, ety: &EntityType) -> Option<&Option<SymTags>> {
163        self.get(ety).map(|sed| &sed.tags)
164    }
165
166    pub fn is_literal(&self) -> bool {
167        self.values().all(|d| d.is_literal())
168    }
169}
170
171/// Symbolic representation of a request environment.
172#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)]
173pub struct SymEnv {
174    /// Symbolic request
175    pub request: SymRequest,
176    /// Symbolic entities
177    pub entities: Arc<SymEntities>,
178}
179
180impl SymEnv {
181    /// Checks if the symbolic environment only contains literal terms.
182    pub fn is_literal(&self) -> bool {
183        self.request.is_literal() && self.entities.is_literal()
184    }
185}
186
187// ----- Functions for constructing symbolic input from a schema -----
188impl SymEntityData {
189    pub fn of_entity_type(
190        ety: &EntityType,
191        validator_ety: &ValidatorEntityType,
192        schema: &ValidatorSchema,
193    ) -> Result<Self, CompileError> {
194        match EntitySchemaEntry::of_schema(ety, validator_ety, schema) {
195            // Corresponds to `SymEntityData.ofStandardEntityType` in Lean
196            EntitySchemaEntry::Standard(sch) => {
197                let attrs_uuf = Uuf(Arc::new(op::Uuf {
198                    id: format_smolstr!("attrs[{ety}]"),
199                    arg: entity(ety.clone()), // more efficient than the Lean: avoids `TermType::of_type()` and constructs the `TermType` directly
200                    out: TermType::of_type(&record(sch.attrs))?,
201                }));
202                let ancs_uuf = |anc_ty: &EntityType| {
203                    Uuf(Arc::new(op::Uuf {
204                        id: format_smolstr!("ancs[{ety}, {anc_ty}]"),
205                        arg: entity(ety.clone()), // more efficient than the Lean: avoids `TermType::of_type()` and constructs the `TermType` directly
206                        out: TermType::set_of(entity(anc_ty.clone())), // more efficient than the Lean: avoids `TermType::of_type()` and constructs the `TermType` directly
207                    }))
208                };
209                let sym_tags = |tag_ty: Type| -> Result<SymTags, CompileError> {
210                    Ok(SymTags {
211                        keys: Uuf(Arc::new(op::Uuf {
212                            id: format_smolstr!("tagKeys[{ety}]"),
213                            arg: entity(ety.clone()), // more efficient than the Lean: avoids `TermType::of_type()` and constructs the `TermType` directly
214                            out: TermType::set_of(TermType::String),
215                        })),
216                        vals: Uuf(Arc::new(op::Uuf {
217                            id: format_smolstr!("tagVals[{ety}]"),
218                            arg: TermType::tag_for(ety.clone()), // record representing the pair type (ety, .string)
219                            out: TermType::of_type(&tag_ty)?,
220                        })),
221                    })
222                };
223
224                Ok(SymEntityData {
225                    attrs: attrs_uuf,
226                    ancestors: sch
227                        .ancestors
228                        .into_iter()
229                        .map(|anc_ty| {
230                            let uuf = ancs_uuf(&anc_ty);
231                            (anc_ty, uuf)
232                        })
233                        .collect(),
234                    members: None,
235                    tags: sch.tags.map(sym_tags).transpose()?,
236                })
237            }
238
239            // Corresponds to `SymEntityData.ofEnumEntityType` in Lean
240            EntitySchemaEntry::Enum(eids) => {
241                let attrs_udf = Udf(Arc::new(function::Udf {
242                    arg: entity(ety.clone()),
243                    out: TermType::Record {
244                        rty: Arc::new(BTreeMap::new()),
245                    },
246                    table: Arc::new(BTreeMap::new()),
247                    default: Term::Record(Arc::new(BTreeMap::new())),
248                }));
249                Ok(SymEntityData {
250                    attrs: attrs_udf,
251                    ancestors: BTreeMap::new(),
252                    members: Some(eids),
253                    tags: None,
254                })
255            }
256        }
257    }
258
259    pub(super) fn of_action_type<'a>(
260        act_ty: &EntityType,
261        act_tys: impl IntoIterator<Item = &'a EntityType>,
262        actions: &ActionSchemaEntries,
263    ) -> Self {
264        let attrs_udf = Udf(Arc::new(function::Udf {
265            arg: entity(act_ty.clone()),
266            out: TermType::Record {
267                rty: Arc::new(BTreeMap::new()),
268            },
269            table: Arc::new(BTreeMap::new()),
270            default: Term::Record(Arc::new(BTreeMap::new())),
271        }));
272        let term_of_type = |ety: EntityType, uid: EntityUID| -> Option<Term> {
273            if uid.type_name() == &ety {
274                Some(Term::Prim(TermPrim::Entity(uid)))
275            } else {
276                None
277            }
278        };
279        let ancs_term = |anc_ty: &EntityType, ancs: &BTreeSet<EntityUID>| -> Term {
280            Term::Set {
281                elts: Arc::new(
282                    ancs.iter()
283                        .filter_map(|anc| term_of_type(anc_ty.clone(), anc.clone()))
284                        .collect(),
285                ),
286                elts_ty: entity(anc_ty.clone()),
287            }
288        };
289        let ancs_udf = |anc_ty: &EntityType| -> UnaryFunction {
290            Udf(Arc::new(function::Udf {
291                arg: entity(act_ty.clone()),
292                out: TermType::set_of(entity(anc_ty.clone())),
293                table: Arc::new(
294                    actions
295                        .iter()
296                        .filter_map(|(uid, entry)| {
297                            Some((
298                                term_of_type(act_ty.clone(), uid.clone())?,
299                                ancs_term(anc_ty, &entry.ancestors),
300                            ))
301                        })
302                        .collect(),
303                ),
304                default: Term::Set {
305                    elts: Arc::new(BTreeSet::new()),
306                    elts_ty: entity(anc_ty.clone()),
307                },
308            }))
309        };
310        let acts = actions
311            .keys()
312            .filter(|uid| uid.type_name() == act_ty)
313            .map(|uid| SmolStr::new(uid.id()))
314            .collect();
315        SymEntityData {
316            attrs: attrs_udf,
317            ancestors: act_tys
318                .into_iter()
319                .map(|anc_ty| {
320                    let udf = ancs_udf(anc_ty);
321                    (anc_ty.clone(), udf)
322                })
323                .collect(),
324            members: Some(acts),
325            tags: None,
326        }
327    }
328}
329
330impl SymEntities {
331    /// Creates symbolic entities for the given schema.
332    ///
333    /// This function assumes that the schemas are well-formed in the following
334    /// sense:
335    /// * All entity types that appear in the attributes or ancestors fields of `ets` are
336    ///   declared either in `ets` or `acts`.
337    /// * All entity types that appear in the ancestors fields of `acts` are declared in `acts`.
338    ///
339    /// An entity type is declared in `ets` if it's a key in the underlying map; it's
340    /// declared in `acts` if it's the type of a key in the underlying map. This
341    /// function also assumes `ets` and `ats` declare disjoint sets of types.
342    ///
343    /// This function assumes that no entity types have tags, and that action types
344    /// have no attributes.
345    fn of_schema(schema: &ValidatorSchema) -> Result<Self, CompileError> {
346        let e_data = schema.entity_types().map(|vdtr_ety| {
347            let ety = core_entity_type_into_entity_type(vdtr_ety.name());
348            match SymEntityData::of_entity_type(ety, vdtr_ety, schema) {
349                Ok(sym_edata) => Ok((ety.clone(), sym_edata)),
350                Err(e) => Err(e),
351            }
352        });
353        #[expect(
354            clippy::expect_used,
355            reason = "ValidatorSchema::action_entities should not error"
356        )]
357        let acts = schema
358            .action_entities()
359            .expect("Schema should have action entities");
360        let act_tys: BTreeSet<&EntityType> = acts
361            .iter()
362            .map(|act| core_entity_type_into_entity_type(act.uid().entity_type()))
363            .collect();
364        let action_schema = ActionSchemaEntries::of_schema(schema);
365        let a_data = act_tys.iter().map(|&act_ty| {
366            Ok((
367                act_ty.clone(),
368                SymEntityData::of_action_type(act_ty, act_tys.iter().copied(), &action_schema),
369            ))
370        });
371        Ok(SymEntities(
372            e_data.into_iter().chain(a_data).collect::<Result<_, _>>()?,
373        ))
374    }
375}
376
377impl SymRequest {
378    /// Creates a symbolic request for the given request type.
379    fn of_request_type(req_ty: &RequestType<'_>) -> Result<Self, CompileError> {
380        Ok(Self {
381            principal: Term::Var(TermVar {
382                id: "principal".into(),
383                ty: TermType::Entity {
384                    ety: req_ty.principal.clone(),
385                },
386            }),
387            action: Term::Prim(TermPrim::Entity(req_ty.action.clone())),
388            resource: Term::Var(TermVar {
389                id: "resource".into(),
390                ty: TermType::Entity {
391                    ety: req_ty.resource.clone(),
392                },
393            }),
394            context: Term::Var(TermVar {
395                id: "context".into(),
396                ty: TermType::of_type(&record(req_ty.context.clone()))?,
397            }),
398        })
399    }
400}
401
402impl SymEnv {
403    /// Returns a symbolic environment that conforms to the given
404    /// type Environment.
405    pub fn of_env(ty_env: &Environment<'_>) -> Result<Self, CompileError> {
406        Ok(SymEnv {
407            request: SymRequest::of_request_type(&ty_env.req_ty)?,
408            entities: Arc::new(SymEntities::of_schema(ty_env.schema)?),
409        })
410    }
411}
412
413// --------------Code not present in Lean--------------
414//
415// Starting from this point, this file differs from the Lean.
416//
417// The Lean representation of the schema doesn't match the Rust, so we define
418// structs that match the Lean and convert the cedar-policy-validator schema
419// representation to these structs.
420// `cedar-policy-validator` doesn't have an equivalent of several types like
421// `RequestType` that are only used in `Cedar/SymCC/Env.lean`, so we define
422// them here.
423
424/// A [`Schema`] paired with its precomputed [`SymEntities`].
425///
426/// Building [`SymEntities`] from a schema is expensive and depends only on the
427/// schema, not on any particular request environment. [`CompiledSchema`] computes it
428/// once so it can be reused across many request environments via
429/// [`CompiledSchema::sym_env`], which is significantly cheaper than rebuilding the
430/// symbolic entities per environment (as [`SymEnv::new`] does).
431#[derive(Debug, Clone)]
432pub struct CompiledSchema {
433    schema: Schema,
434    entities: Arc<SymEntities>,
435}
436
437impl CompiledSchema {
438    /// Builds the symbolic entities for `schema` once, up front.
439    pub fn new(schema: &Schema) -> crate::err::Result<Self> {
440        Ok(Self {
441            schema: schema.clone(),
442            entities: Arc::new(SymEntities::of_schema(schema.as_ref())?),
443        })
444    }
445
446    /// The [`Schema`] this symbolic schema was built from.
447    pub fn schema(&self) -> &Schema {
448        &self.schema
449    }
450
451    /// Returns a [`SymEnv`] for `req_env`, reusing the precomputed [`SymEntities`].
452    ///
453    /// Only the (cheap) symbolic request is built per call; the (expensive)
454    /// symbolic entities are shared via a reference-counted clone. The resulting
455    /// [`SymEnv`] is identical to one built by [`SymEnv::new`] for the same
456    /// schema and request environment.
457    // INVARIANT: must produce the same SymEnv as SymEnv::of_env / SymEnv::new.
458    // Guarded by test compiled_schema_sym_env_matches_sym_env_new.
459    pub fn sym_env(&self, req_env: &RequestEnv) -> crate::err::Result<SymEnv> {
460        let env = Environment::from_request_env(req_env, self.schema.as_ref())
461            .ok_or_else(|| crate::err::Error::ActionNotInSchema(req_env.action().to_string()))?;
462        Ok(SymEnv {
463            request: SymRequest::of_request_type(&env.req_ty)?,
464            entities: self.entities.clone(),
465        })
466    }
467}
468
469// TODO: test this
470
471// Convenience functions
472
473fn entity(ety: EntityType) -> TermType {
474    TermType::Entity { ety }
475}
476
477fn record(attrs: Attributes) -> Type {
478    Type::Record {
479        attrs,
480        open_attributes: OpenTag::ClosedAttributes,
481    }
482}
483
484// From `Validation/Types.lean`
485pub(super) struct StandardEntitySchemaEntry {
486    pub(super) ancestors: BTreeSet<EntityType>,
487    pub(super) attrs: Attributes,
488    pub(super) tags: Option<Type>,
489}
490
491pub(super) enum EntitySchemaEntry {
492    Standard(StandardEntitySchemaEntry),
493    Enum(BTreeSet<SmolStr>),
494}
495
496impl EntitySchemaEntry {
497    pub fn of_schema(
498        ety: &EntityType,
499        validator_ety: &ValidatorEntityType,
500        schema: &ValidatorSchema,
501    ) -> Self {
502        if let Some(entry) = schema.get_entity_type(ety.as_ref()) {
503            if let ValidatorEntityTypeKind::Enum(eids) = &entry.kind {
504                return EntitySchemaEntry::Enum(
505                    eids.into_iter().map(|e| e.as_ref().into()).collect(),
506                );
507            }
508        }
509
510        EntitySchemaEntry::Standard(StandardEntitySchemaEntry {
511            // Reverse the descendants relation to get the ancestors relation
512            ancestors: schema
513                .entity_types()
514                .filter(|other_ety| other_ety.descendants.contains(ety.as_ref()))
515                .map(|other_ety| core_entity_type_into_entity_type(other_ety.name()))
516                .cloned()
517                .collect(),
518            attrs: validator_ety.attributes().clone(),
519            tags: validator_ety.tag_type().cloned(),
520        })
521    }
522}
523
524// From `Validation/Types.lean`
525pub(crate) struct ActionSchemaEntry {
526    ancestors: BTreeSet<EntityUID>,
527    // present in the Lean, but not used in SymCC
528    // applies_to_principal: BTreeSet<EntityType>,
529    // present in the Lean, but not used in SymCC
530    // applies_to_resource: BTreeSet<EntityType>,
531    // present in the Lean, but not used in SymCC
532    // context: Attributes,
533}
534
535pub(crate) struct ActionSchemaEntries(BTreeMap<EntityUID, ActionSchemaEntry>);
536
537impl Deref for ActionSchemaEntries {
538    type Target = BTreeMap<EntityUID, ActionSchemaEntry>;
539
540    fn deref(&self) -> &Self::Target {
541        &self.0
542    }
543}
544
545impl ActionSchemaEntries {
546    pub(crate) fn of_schema(schema: &ValidatorSchema) -> Self {
547        Self(
548            #[expect(
549                clippy::expect_used,
550                reason = "ValidatorSchema::action_entities should not error"
551            )]
552            schema
553                .action_entities()
554                .expect("Failed to get action entities from schema")
555                .into_iter()
556                .map(|action| {
557                    let action_schema_entry = ActionSchemaEntry {
558                        ancestors: action.ancestors().map(core_uid_into_uid).cloned().collect(),
559                    };
560                    (core_uid_into_uid(action.uid()).clone(), action_schema_entry)
561                })
562                .collect(),
563        )
564    }
565}
566
567fn context_attributes(action: &ValidatorActionId) -> Result<&Attributes, CompileError> {
568    match action.context_type() {
569        Type::Record {
570            attrs,
571            open_attributes: OpenTag::ClosedAttributes,
572        } => Ok(attrs),
573        _ => Err(CompileError::NonRecordContext),
574    }
575}
576
577// From `Validation/Types.lean`
578#[derive(Debug)]
579struct RequestType<'a> {
580    principal: &'a EntityType,
581    action: &'a EntityUID,
582    resource: &'a EntityType,
583    context: &'a Attributes,
584}
585
586// From `Validation/Types.lean`
587#[derive(Debug)]
588pub struct Environment<'a> {
589    schema: &'a ValidatorSchema,
590    req_ty: RequestType<'a>,
591}
592
593impl<'a> Environment<'a> {
594    /// No real counterpart in the Lean; this function maps from
595    /// `cedar_policy::RequestEnv` to the `Environment` struct
596    ///
597    /// Returns `None` if the `renv` specifies an `action` not found in the `schema`
598    pub fn from_request_env(
599        renv: &'a cedar_policy::RequestEnv,
600        schema: &'a ValidatorSchema,
601    ) -> Option<Self> {
602        Some(Self {
603            schema,
604            req_ty: RequestType {
605                principal: renv.principal(),
606                action: renv.action(),
607                resource: renv.resource(),
608                context: context_attributes(schema.get_action_id(renv.action().as_ref())?).ok()?,
609            },
610        })
611    }
612
613    pub fn schema(&self) -> &'a ValidatorSchema {
614        self.schema
615    }
616
617    /// Returns the type of the context.
618    pub fn context_type(&self) -> Type {
619        Type::record_with_attributes(self.req_ty.context.clone(), OpenTag::ClosedAttributes)
620    }
621}
622
623pub fn to_validator_request_env<'a>(
624    env: &'a cedar_policy::RequestEnv,
625    schema: &'a ValidatorSchema,
626) -> Option<cedar_policy_core::validator::types::RequestEnv<'a>> {
627    let principal = env.principal().as_ref();
628    let resource = env.resource().as_ref();
629    let action = env.action().as_ref();
630    let context_type = schema.context_type(action);
631    context_type.map(
632        |context| cedar_policy_core::validator::types::RequestEnv::DeclaredAction {
633            principal,
634            action,
635            resource,
636            context,
637            principal_slot: None,
638            resource_slot: None,
639        },
640    )
641}