lace_sans_io/logic/
facet_constraints.rs1use std::collections::{BTreeMap, BTreeSet};
4
5use super::exposure_view::ExposureView;
6use super::limits::RuleCollector;
7use super::profile::{
8 ADVERTISED_FIELD, EXPOSE_RECORD, FIELD, SELECT_ADVERTISED_CANDIDATE, SELECT_STORED_RECORD,
9};
10use super::{BodyAtom, DatalogLimits, InterlaceModule, PreparationError, Rule, Symbol, Term};
11
12const INTERSECTED_FACETS: [&str; 3] = [
15 SELECT_STORED_RECORD.name,
16 SELECT_ADVERTISED_CANDIDATE.name,
17 EXPOSE_RECORD.name,
18];
19
20const GUEST_VIEW: &str = "constraint_authoring_exposerecord";
24const GUEST_PREFIX: &str = "constraint_";
25
26#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
32pub struct FieldExclusion {
33 pub name: Option<String>,
34 pub index: Option<String>,
35 pub value: Option<String>,
36}
37
38impl FieldExclusion {
39 pub fn named(name: impl Into<String>) -> Self {
41 Self {
42 name: Some(name.into()),
43 ..Self::default()
44 }
45 }
46
47 fn negated(&self, predicate: &str) -> BodyAtom {
48 let term = |component: &Option<String>| {
49 component
50 .as_deref()
51 .map_or(Term::Anonymous, |value| Term::Const(value.into()))
52 };
53 BodyAtom::Negated {
54 predicate: predicate.into(),
55 terms: vec![
56 record_term(),
57 term(&self.name),
58 term(&self.index),
59 term(&self.value),
60 ],
61 }
62 }
63}
64
65pub fn prepare_facet_constraints(
80 authoring: &[InterlaceModule],
81 guests: &[InterlaceModule],
82 exclusions: &[FieldExclusion],
83) -> Result<InterlaceModule, PreparationError> {
84 fn canonical(modules: &[InterlaceModule]) -> BTreeMap<&str, &InterlaceModule> {
85 modules
86 .iter()
87 .map(|module| (module.canonical_text(), module))
88 .collect()
89 }
90 let (authoring, guests) = (canonical(authoring), canonical(guests));
91 let exclusions = exclusions.iter().collect::<BTreeSet<_>>();
92 let excluded = |predicate: &'static str| {
93 exclusions
94 .iter()
95 .map(move |exclusion| exclusion.negated(predicate))
96 };
97 if authoring.is_empty() {
98 return Err("no authoring facet constraints".into());
99 }
100 if !authoring
101 .values()
102 .chain(guests.values())
103 .all(|module| module.has_facet())
104 {
105 return Err("policy has no executable facets".into());
106 }
107 if authoring.len() == 1 && guests.is_empty() && exclusions.is_empty() {
108 return Ok((*authoring.into_values().next().unwrap()).clone());
109 }
110
111 let mut rules = RuleCollector::new(DatalogLimits::default());
112 let mut constraints: [Vec<BodyAtom>; 3] = Default::default();
113 let mut guest_view = Vec::new();
114 let mut guest_rules = Vec::new();
115 let authoring_count = authoring.len();
116 for (index, module) in authoring
117 .into_values()
118 .chain(guests.into_values())
119 .enumerate()
120 {
121 for (facet_index, facet) in INTERSECTED_FACETS.iter().enumerate() {
122 if module
123 .program()
124 .strata
125 .iter()
126 .flatten()
127 .any(|rule| rule.head_predicate == *facet)
128 {
129 let constraint = BodyAtom::Positive {
130 predicate: scoped_predicate(facet, index),
131 terms: vec![record_term()],
132 };
133 if index < authoring_count && *facet == EXPOSE_RECORD.name {
134 guest_view.push(constraint.clone());
135 }
136 constraints[facet_index].push(constraint);
137 }
138 }
139 for rule in module.program().strata.iter().flatten() {
140 let mut rule = rule.clone();
141 rule.rename_relations(|name| scoped_predicate(name, index));
142 if index < authoring_count {
143 rules.push(rule)?;
144 } else {
145 guest_rules.push(rule);
146 }
147 }
148 }
149 if !guest_rules.is_empty() {
150 let confined = ExposureView {
151 gate: GUEST_VIEW,
152 prefix: GUEST_PREFIX,
153 specialize: false,
154 }
155 .confine(guest_rules)?;
156 let gated = confined.iter().any(|rule| {
157 rule.body
158 .iter()
159 .filter_map(BodyAtom::relation)
160 .any(|(predicate, _, _)| predicate == GUEST_VIEW)
161 });
162 for rule in confined {
163 rules.push(rule)?;
164 }
165 if gated {
166 if guest_view.is_empty() {
169 guest_view.push(BodyAtom::Positive {
170 predicate: GUEST_VIEW.into(),
171 terms: vec![record_term()],
172 });
173 } else {
174 guest_view.extend(excluded(FIELD.name));
175 }
176 rules.push(Rule {
177 head_predicate: GUEST_VIEW.into(),
178 head_terms: vec![record_term()],
179 body: guest_view,
180 })?;
181 }
182 }
183 for (facet, mut body) in INTERSECTED_FACETS.into_iter().zip(constraints) {
184 if !body.is_empty() {
185 body.extend(excluded(if facet == SELECT_ADVERTISED_CANDIDATE.name {
186 ADVERTISED_FIELD.name
187 } else {
188 FIELD.name
189 }));
190 rules.push(Rule {
191 head_predicate: facet.into(),
192 head_terms: vec![record_term()],
193 body,
194 })?;
195 }
196 }
197 InterlaceModule::from_rules(rules.into_rules())
198}
199
200fn record_term() -> Term {
201 Term::var("Record")
202}
203
204fn scoped_predicate(name: &str, index: usize) -> Symbol {
205 if let Some(facet) = INTERSECTED_FACETS.iter().position(|facet| *facet == name) {
206 format!("constraint{index}_facet{facet}").into()
207 } else if name.as_bytes().first().is_some_and(u8::is_ascii_lowercase) {
208 format!("constraint{index}_helper_{name}").into()
209 } else {
210 name.into()
212 }
213}
214
215#[cfg(test)]
216mod exclusion_tests;
217#[cfg(test)]
218mod guest_tests;
219#[cfg(test)]
220mod tests;