Skip to main content

gatekeep_keepsake/
resolver.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use async_trait::async_trait;
4use gatekeep::{
5    BindingProvenance, Clock, Context, Fact, FactId, FactResolution, FactResolutionMetadata,
6    FactResolver, KnownFacts, PartialFacts, Presence, QueryFactResolver, ResolveError,
7};
8use keepsake::{
9    ActiveRelationSource, LifecycleState, ObservationTime, RelationId, RelationSpec,
10    effective_state,
11};
12
13use crate::{
14    FactBinding, FactBindingError, KeepsakeRelationTarget, KeepsakeResolveError,
15    KeepsakeTargetError, QueryPresence, SubjectMapper, TenantScopedSubjectMapper,
16};
17
18/// Resolves gatekeep facts from active keepsake relations for the principal.
19#[derive(Clone, Debug)]
20pub struct KeepsakeResolver<S, M = TenantScopedSubjectMapper> {
21    source: S,
22    subject_mapper: M,
23    bindings: BTreeMap<FactId, FactBinding>,
24}
25
26impl<S> KeepsakeResolver<S, TenantScopedSubjectMapper> {
27    /// Builds a resolver around an active-relation source.
28    #[must_use]
29    pub const fn new(source: S) -> Self {
30        Self::with_subject_mapper(source, TenantScopedSubjectMapper)
31    }
32}
33
34impl<S, M> KeepsakeResolver<S, M> {
35    /// Builds a resolver with an explicit subject mapper.
36    #[must_use]
37    pub const fn with_subject_mapper(source: S, subject_mapper: M) -> Self {
38        Self {
39            source,
40            subject_mapper,
41            bindings: BTreeMap::new(),
42        }
43    }
44
45    /// Replaces the subject mapper.
46    #[must_use]
47    pub fn map_subjects<Next>(self, subject_mapper: Next) -> KeepsakeResolver<S, Next> {
48        KeepsakeResolver {
49            source: self.source,
50            subject_mapper,
51            bindings: self.bindings,
52        }
53    }
54
55    /// Adds or replaces a fact binding.
56    #[must_use]
57    pub fn with_binding(mut self, binding: FactBinding) -> Self {
58        self.insert_binding(binding);
59        self
60    }
61
62    /// Adds or replaces a fact binding.
63    pub fn insert_binding(&mut self, binding: FactBinding) {
64        self.bindings.insert(binding.fact.clone(), binding);
65    }
66
67    /// Adds or replaces a typed fact-to-relation binding.
68    ///
69    /// # Errors
70    ///
71    /// Returns [`FactBindingError::Gatekeep`] if the fact marker exposes an
72    /// invalid stable id.
73    pub fn with_relation_spec<F, R>(self) -> Result<Self, FactBindingError>
74    where
75        F: Fact,
76        R: RelationSpec,
77    {
78        self.with_relation_spec_query_presence::<F, R>(QueryPresence::Resolve)
79    }
80
81    /// Adds or replaces a typed binding that is resolved during query
82    /// preparation.
83    ///
84    /// # Errors
85    ///
86    /// Returns [`FactBindingError::Gatekeep`] if the fact marker exposes an
87    /// invalid stable id.
88    pub fn with_resolved_relation<F, R>(self) -> Result<Self, FactBindingError>
89    where
90        F: Fact,
91        R: RelationSpec,
92    {
93        self.with_relation_spec_query_presence::<F, R>(QueryPresence::Resolve)
94    }
95
96    /// Adds or replaces a typed binding that is deferred during query
97    /// preparation for row-level lowering.
98    ///
99    /// # Errors
100    ///
101    /// Returns [`FactBindingError::Gatekeep`] if the fact marker exposes an
102    /// invalid stable id.
103    pub fn with_deferred_relation<F, R>(self) -> Result<Self, FactBindingError>
104    where
105        F: Fact,
106        R: RelationSpec,
107    {
108        self.with_relation_spec_query_presence::<F, R>(QueryPresence::Defer)
109    }
110
111    /// Adds or replaces a typed binding with explicit query-mode behavior.
112    ///
113    /// # Errors
114    ///
115    /// Returns [`FactBindingError::Gatekeep`] if the fact marker exposes an
116    /// invalid stable id.
117    pub fn with_relation_spec_query_presence<F, R>(
118        self,
119        query_presence: QueryPresence,
120    ) -> Result<Self, FactBindingError>
121    where
122        F: Fact,
123        R: RelationSpec,
124    {
125        Ok(
126            self.with_binding(FactBinding::for_relation_spec_with_query_presence::<F, R>(
127                query_presence,
128            )?),
129        )
130    }
131
132    /// Adds or replaces a typed binding resolved against a request-scoped
133    /// subject slot.
134    ///
135    /// # Errors
136    ///
137    /// Returns [`FactBindingError::Gatekeep`] if the fact marker exposes an
138    /// invalid stable id.
139    pub fn with_relation_spec_on_subject<F, R>(
140        self,
141        subject_slot: gatekeep::SubjectSlot,
142    ) -> Result<Self, FactBindingError>
143    where
144        F: Fact,
145        R: RelationSpec,
146    {
147        Ok(
148            self.with_binding(FactBinding::for_relation_spec_on_subject::<F, R>(
149                subject_slot,
150            )?),
151        )
152    }
153
154    /// Returns the configured bindings keyed by fact id.
155    #[must_use]
156    pub const fn bindings(&self) -> &BTreeMap<FactId, FactBinding> {
157        &self.bindings
158    }
159
160    /// Returns the wrapped active-relation source.
161    #[must_use]
162    pub const fn source(&self) -> &S {
163        &self.source
164    }
165
166    /// Returns the subject mapper.
167    #[must_use]
168    pub const fn subject_mapper(&self) -> &M {
169        &self.subject_mapper
170    }
171}
172
173impl<S, M> KeepsakeResolver<S, M>
174where
175    M: SubjectMapper,
176{
177    /// Resolves one binding into the keepsake subject/relation target used for lookups.
178    ///
179    /// # Errors
180    ///
181    /// Returns [`KeepsakeTargetError::MissingSubjectSlot`] when the binding
182    /// targets a request-scoped subject absent from the context, or
183    /// [`KeepsakeTargetError::Subject`] when keepsake rejects the mapped subject.
184    pub fn target_for_binding(
185        &self,
186        binding: &FactBinding,
187        cx: &Context,
188    ) -> Result<KeepsakeRelationTarget, KeepsakeTargetError> {
189        let tenant_id = keepsake::TenantId::new(cx.tenant().as_str()).map_err(|source| {
190            KeepsakeTargetError::Tenant {
191                fact: binding.fact.clone(),
192                source,
193            }
194        })?;
195        let subject = if let Some(slot) = &binding.subject_slot {
196            let Some(subject) = cx.subjects().get(slot) else {
197                return Err(KeepsakeTargetError::MissingSubjectSlot {
198                    fact: binding.fact.clone(),
199                    slot: slot.clone(),
200                });
201            };
202            keepsake::SubjectRef::new(subject.kind(), subject.id()).map_err(|source| {
203                KeepsakeTargetError::Subject {
204                    fact: binding.fact.clone(),
205                    source,
206                }
207            })?
208        } else {
209            self.subject_mapper
210                .subject(cx)
211                .map_err(|source| KeepsakeTargetError::Subject {
212                    fact: binding.fact.clone(),
213                    source,
214                })?
215        };
216
217        Ok(KeepsakeRelationTarget {
218            tenant_id,
219            fact: binding.fact.clone(),
220            subject,
221            relation_id: binding.relation_id,
222            subject_slot: binding.subject_slot.clone(),
223        })
224    }
225
226    /// Resolves a configured fact id into its keepsake subject/relation target.
227    ///
228    /// # Errors
229    ///
230    /// Returns [`KeepsakeTargetError::MissingBinding`] when the fact has no
231    /// configured binding. Also returns the subject-resolution errors documented
232    /// by [`Self::target_for_binding`].
233    pub fn target_for_fact(
234        &self,
235        fact: &FactId,
236        cx: &Context,
237    ) -> Result<KeepsakeRelationTarget, KeepsakeTargetError> {
238        let binding = self
239            .bindings
240            .get(fact)
241            .ok_or_else(|| KeepsakeTargetError::MissingBinding { fact: fact.clone() })?;
242        self.target_for_binding(binding, cx)
243    }
244
245    /// Resolves configured fact ids into keepsake subject/relation targets.
246    ///
247    /// # Errors
248    ///
249    /// Returns the first [`KeepsakeTargetError`] encountered while resolving the
250    /// requested facts.
251    pub fn targets_for_facts(
252        &self,
253        facts: &[FactId],
254        cx: &Context,
255    ) -> Result<Vec<KeepsakeRelationTarget>, KeepsakeTargetError> {
256        facts
257            .iter()
258            .map(|fact| self.target_for_fact(fact, cx))
259            .collect()
260    }
261}
262
263#[async_trait]
264impl<S, M> FactResolver for KeepsakeResolver<S, M>
265where
266    S: ActiveRelationSource,
267    M: SubjectMapper,
268{
269    type Error = KeepsakeResolveError<S::Error>;
270
271    async fn resolve_for_decision(
272        &self,
273        required: &[FactId],
274        cx: &Context,
275        clock: &dyn Clock,
276    ) -> Result<FactResolution<KnownFacts>, ResolveError<Self::Error>> {
277        let observed_at = clock.now_utc();
278        let bindings = self.bindings_for(required)?;
279        let active_relations = self
280            .active_relation_ids_by_subject(cx, &bindings, observed_at)
281            .await?;
282        let entries = bindings.into_iter().map(|binding| {
283            let presence = relation_presence(
284                &active_relations,
285                binding.subject_slot.as_ref(),
286                binding.relation_id,
287            );
288            (binding.fact.clone(), presence)
289        });
290        FactResolution::new(
291            KnownFacts::from_entries(entries).map_err(KeepsakeResolveError::Gatekeep)?,
292            Some(
293                active_relations
294                    .metadata()
295                    .map_err(KeepsakeResolveError::Provenance)?,
296            ),
297            observed_at,
298        )
299        .map_err(ResolveError::Resolution)
300    }
301}
302
303#[async_trait]
304impl<S, M> QueryFactResolver for KeepsakeResolver<S, M>
305where
306    S: ActiveRelationSource,
307    M: SubjectMapper,
308{
309    async fn resolve_for_query(
310        &self,
311        required: &[FactId],
312        cx: &Context,
313        clock: &dyn Clock,
314    ) -> Result<FactResolution<PartialFacts>, ResolveError<Self::Error>> {
315        let observed_at = clock.now_utc();
316        let bindings = self.bindings_for(required)?;
317        let needs_active_lookup = bindings
318            .iter()
319            .any(|binding| binding.query_presence == QueryPresence::Resolve);
320        let active_relations = if needs_active_lookup {
321            let resolved_bindings = bindings
322                .iter()
323                .copied()
324                .filter(|binding| binding.query_presence == QueryPresence::Resolve)
325                .collect::<Vec<_>>();
326            self.active_relation_ids_by_subject(cx, &resolved_bindings, observed_at)
327                .await?
328        } else {
329            EffectiveRelations::default()
330        };
331
332        let entries = bindings.into_iter().map(|binding| {
333            let presence = match binding.query_presence {
334                QueryPresence::Resolve => relation_presence(
335                    &active_relations,
336                    binding.subject_slot.as_ref(),
337                    binding.relation_id,
338                ),
339                QueryPresence::Defer => Presence::Unknown,
340            };
341            (binding.fact.clone(), presence)
342        });
343        FactResolution::new(
344            PartialFacts::from_entries(entries),
345            Some(
346                active_relations
347                    .metadata()
348                    .map_err(KeepsakeResolveError::Provenance)?,
349            ),
350            observed_at,
351        )
352        .map_err(ResolveError::Resolution)
353    }
354}
355
356impl<S, M> KeepsakeResolver<S, M>
357where
358    S: ActiveRelationSource,
359    M: SubjectMapper,
360{
361    fn bindings_for<'binding>(
362        &'binding self,
363        required: &[FactId],
364    ) -> Result<Vec<&'binding FactBinding>, ResolveError<KeepsakeResolveError<S::Error>>> {
365        required
366            .iter()
367            .map(|fact| {
368                self.bindings
369                    .get(fact)
370                    .ok_or_else(|| ResolveError::MissingFact(fact.clone()))
371            })
372            .collect()
373    }
374
375    async fn active_relation_ids_by_subject(
376        &self,
377        cx: &Context,
378        bindings: &[&FactBinding],
379        at: time::OffsetDateTime,
380    ) -> Result<EffectiveRelations, ResolveError<KeepsakeResolveError<S::Error>>> {
381        let mut grouped = BTreeMap::<Option<gatekeep::SubjectSlot>, SubjectLookup>::new();
382        for binding in bindings {
383            let target = self
384                .target_for_binding(binding, cx)
385                .map_err(|error| match error {
386                    KeepsakeTargetError::MissingBinding { fact } => ResolveError::MissingFact(fact),
387                    KeepsakeTargetError::MissingSubjectSlot { fact, slot } => {
388                        ResolveError::MissingSubject { fact, slot }
389                    }
390                    KeepsakeTargetError::Subject { source, .. }
391                    | KeepsakeTargetError::Tenant { source, .. } => {
392                        ResolveError::Backend(KeepsakeResolveError::from(source))
393                    }
394                })?;
395            grouped
396                .entry(target.subject_slot)
397                .or_insert_with(|| SubjectLookup {
398                    subject: target.subject,
399                    relation_ids: BTreeSet::new(),
400                })
401                .relation_ids
402                .insert(target.relation_id);
403        }
404
405        let tenant_id = keepsake::TenantId::new(cx.tenant().as_str())
406            .map_err(|source| ResolveError::Backend(KeepsakeResolveError::from(source)))?;
407        let mut active = EffectiveRelations::default();
408        for (slot, lookup) in grouped {
409            let relation_ids = lookup.relation_ids.iter().copied().collect::<Vec<_>>();
410            let active_relations = self
411                .source
412                .active_relations_for_subject_by_ids(&tenant_id, &lookup.subject, &relation_ids)
413                .await
414                .map_err(KeepsakeResolveError::Source)?;
415
416            for assignment in lookup.effective::<S::Error>(&tenant_id, active_relations, at)? {
417                active.insert(slot.clone(), assignment.keepsake());
418            }
419        }
420
421        Ok(active)
422    }
423}
424
425struct SubjectLookup {
426    subject: keepsake::SubjectRef,
427    relation_ids: BTreeSet<RelationId>,
428}
429
430impl SubjectLookup {
431    fn effective<E>(
432        &self,
433        tenant: &keepsake::TenantId,
434        assignments: Vec<keepsake::ActiveRelation>,
435        at: time::OffsetDateTime,
436    ) -> Result<Vec<keepsake::ActiveRelation>, KeepsakeResolveError<E>> {
437        let mut effective = Vec::new();
438        for assignment in assignments {
439            let stored = assignment.keepsake();
440            if stored.tenant_id() != tenant
441                || stored.subject() != &self.subject
442                || !self.relation_ids.contains(&stored.relation_id())
443            {
444                return Err(KeepsakeResolveError::ScopeMismatch);
445            }
446
447            if effective_state(ObservationTime::Authoritative(at), &assignment, None)?
448                == LifecycleState::Applied
449            {
450                effective.push(assignment);
451            }
452        }
453        Ok(effective)
454    }
455}
456
457fn relation_presence(
458    active_relations: &EffectiveRelations,
459    subject_slot: Option<&gatekeep::SubjectSlot>,
460    relation_id: RelationId,
461) -> Presence {
462    if active_relations
463        .ids
464        .contains(&(subject_slot.cloned(), relation_id))
465    {
466        Presence::Present
467    } else {
468        Presence::Absent
469    }
470}
471
472#[derive(Default)]
473struct EffectiveRelations {
474    ids: BTreeSet<(Option<gatekeep::SubjectSlot>, RelationId)>,
475    expires_at: Option<time::OffsetDateTime>,
476}
477
478impl EffectiveRelations {
479    fn insert(&mut self, slot: Option<gatekeep::SubjectSlot>, stored: &keepsake::Keepsake) {
480        self.ids.insert((slot, stored.relation_id()));
481        if let Some(deadline) = stored.expires_at() {
482            self.expires_at = Some(
483                self.expires_at
484                    .map_or(deadline, |current| current.min(deadline)),
485            );
486        }
487    }
488
489    fn metadata(&self) -> Result<FactResolutionMetadata, gatekeep::TenantBindingError> {
490        Ok(FactResolutionMetadata::new(
491            BindingProvenance::new("keepsake.effective-snapshot")?,
492            None,
493            self.expires_at,
494        ))
495    }
496}