Skip to main content

coven_database/gate/audience/
inbound.rs

1use super::routing::*;
2use super::*;
3
4pub(crate) fn filter_inbound_circle_changeset(
5    conn: &Connection,
6    changeset: &[u8],
7    circle_id: CircleId,
8    store_transitions: &StoreAudienceTransitions,
9    gates: &Gates,
10    routing_key: &RowRoutingKey,
11) -> Result<Vec<u8>, GateError> {
12    unsafe {
13        let package_audience = Audience::Circle(circle_id);
14        let (normalized, _) = normalize_inbound_private_routes_raw(
15            conn,
16            changeset,
17            &package_audience,
18            InboundRoutingSource::Published(store_transitions),
19            gates,
20            routing_key,
21        )?;
22        filter_inbound_audience_rows_raw(conn, &normalized, &package_audience, gates, routing_key)
23    }
24}
25
26enum InboundRoutingSource<'a> {
27    Published(&'a StoreAudienceTransitions),
28    InstalledSnapshot,
29}
30
31pub(crate) fn filter_snapshot_circle_changeset(
32    conn: &Connection,
33    changeset: &[u8],
34    circle_id: CircleId,
35    gates: &Gates,
36    routing_key: &RowRoutingKey,
37) -> Result<Vec<u8>, GateError> {
38    unsafe {
39        let audience = Audience::Circle(circle_id);
40        let (normalized, _) = normalize_inbound_private_routes_raw(
41            conn,
42            changeset,
43            &audience,
44            InboundRoutingSource::InstalledSnapshot,
45            gates,
46            routing_key,
47        )?;
48        filter_inbound_audience_rows_raw(conn, &normalized, &audience, gates, routing_key)
49    }
50}
51
52pub(crate) fn filter_inbound_store_rows(
53    conn: &Connection,
54    changeset: &[u8],
55    gates: &Gates,
56    routing_key: &RowRoutingKey,
57) -> Result<Vec<u8>, GateError> {
58    unsafe {
59        filter_inbound_audience_rows_raw(conn, changeset, &Audience::Store, gates, routing_key)
60    }
61}
62
63pub(crate) fn align_inbound_scoped_root_audiences(
64    conn: &Connection,
65    changeset: &[u8],
66    package_audience: &Audience,
67    gates: &Gates,
68    routing_key: &RowRoutingKey,
69) -> Result<(), GateError> {
70    unsafe {
71        for_each_change(changeset, |_iter, row| {
72            if row.op == ffi::SQLITE_DELETE {
73                return Ok(());
74            }
75            let Some(TableGate::ScopedRoot { audience_col }) = gates.tables.get(&row.table) else {
76                return Ok(());
77            };
78            let row_id = row
79                .pk()
80                .ok_or_else(|| GateError::MissingChangesetPrimaryKey(row.table.clone()))?;
81            let routing_id = row_routing_id(routing_key, &row.table, row_id).to_string();
82            let winning_audience = winning_store_audience(conn, &routing_id)?;
83            if winning_audience.as_ref() != Some(package_audience) {
84                return Err(GateError::InvalidInboundAudiencePackage(format!(
85                    "eligible {}.{row_id} package no longer matches its winning Store audience",
86                    row.table
87                )));
88            }
89            let updated = conn
90                .execute(
91                    &format!(
92                        "UPDATE {} SET {} = ?1 WHERE id = ?2",
93                        quote_ident(&row.table),
94                        quote_ident(&audience_col.name),
95                    ),
96                    rusqlite::params![package_audience.column_value(), row_id],
97                )
98                .map_err(|source| {
99                    GateError::Sql(
100                        format!("align inbound audience for {}.{row_id}", row.table),
101                        source,
102                    )
103                })?;
104            if updated != 1 {
105                return Err(GateError::InvalidInboundAudiencePackage(format!(
106                    "eligible {}.{row_id} did not materialize exactly one scoped root",
107                    row.table
108                )));
109            }
110            Ok(())
111        })
112    }
113}
114
115pub(crate) fn winning_store_audience(
116    conn: &Connection,
117    routing_id: &str,
118) -> Result<Option<Audience>, GateError> {
119    query_row_optional(
120        conn,
121        "SELECT circle_id FROM _coven_audience WHERE routing_id = ?1",
122        [routing_id],
123        |record| record.get::<_, Option<String>>(0),
124    )?
125    .map(|circle_id| {
126        Audience::from_column(circle_id.as_deref()).map_err(|source| {
127            GateError::InvalidInboundAudienceEncoding {
128                context: format!("winning Store audience for {routing_id} is invalid"),
129                source,
130            }
131        })
132    })
133    .transpose()
134}
135
136pub(crate) fn normalize_inbound_store_changeset(
137    conn: &Connection,
138    changeset: &[u8],
139    gates: &Gates,
140    routing_key: &RowRoutingKey,
141) -> Result<InboundStoreChangesets, GateError> {
142    let store_transitions = store_audience_transitions(changeset)?;
143    let normalized = unsafe {
144        normalize_inbound_private_routes_raw(
145            conn,
146            changeset,
147            &Audience::Store,
148            InboundRoutingSource::Published(&store_transitions),
149            gates,
150            routing_key,
151        )
152        .map(|(normalized, _)| normalized)?
153    };
154    unsafe {
155        let mirror = Changegroup::new()?;
156        mirror.set_schema(conn.handle())?;
157        let rows = Changegroup::new()?;
158        rows.set_schema(conn.handle())?;
159        for_each_change(&normalized, |iter, row| {
160            if row.table == "_coven_audience" {
161                mirror.add_change(iter)
162            } else {
163                rows.add_change(iter)
164            }
165        })?;
166        Ok(InboundStoreChangesets {
167            mirror: mirror.output()?,
168            rows: rows.output()?,
169        })
170    }
171}
172
173pub(crate) type PackageRoutes = HashMap<(String, String), (String, String)>;
174
175pub fn store_audience_transitions(changeset: &[u8]) -> Result<StoreAudienceTransitions, GateError> {
176    let mut transitions = StoreAudienceTransitions::default();
177    unsafe {
178        for_each_change(changeset, |_iter, row| {
179            if row.table != "_coven_audience"
180                || (row.op != ffi::SQLITE_INSERT && row.op != ffi::SQLITE_UPDATE)
181            {
182                return Ok(());
183            }
184            let routing_id = row
185                .pk()
186                .ok_or_else(|| GateError::MissingChangesetPrimaryKey(row.table.clone()))?;
187            let circle_id = row.new_value(1).ok_or_else(|| {
188                GateError::InvalidInboundAudiencePackage(format!(
189                    "Store audience transition {routing_id} has no audience"
190                ))
191            })?;
192            let audience = Audience::from_column(circle_id).map_err(|source| {
193                GateError::InvalidInboundAudienceEncoding {
194                    context: format!(
195                        "Store audience transition {routing_id} has an invalid audience"
196                    ),
197                    source,
198                }
199            })?;
200            if audience == Audience::Local {
201                return Err(GateError::InvalidInboundAudiencePackage(format!(
202                    "Store audience transition {routing_id} has a Local audience"
203                )));
204            }
205            let stamp = row.new_value(2).flatten().ok_or_else(|| {
206                GateError::InvalidInboundAudiencePackage(format!(
207                    "Store audience transition {routing_id} has no _updated_at"
208                ))
209            })?;
210            if transitions
211                .by_routing_id
212                .insert(routing_id.to_string(), (audience, stamp.to_string()))
213                .is_some()
214            {
215                return Err(GateError::InvalidInboundAudiencePackage(format!(
216                    "Store package contains duplicate audience transitions for {routing_id}"
217                )));
218            }
219            Ok(())
220        })?;
221    }
222    Ok(transitions)
223}
224
225pub(crate) unsafe fn filter_inbound_audience_rows_raw(
226    conn: &Connection,
227    changeset: &[u8],
228    package_audience: &Audience,
229    gates: &Gates,
230    routing_key: &RowRoutingKey,
231) -> Result<Vec<u8>, GateError> {
232    let allow_unscoped = package_audience == &Audience::Store;
233    for_each_change(changeset, |_iter, row| {
234        if row.table == "_coven_audience" {
235            return Err(GateError::InvalidInboundAudiencePackage(
236                "audience row package contains the Store audience mirror".to_string(),
237            ));
238        }
239        if row.table != "_coven_row_routes" && !gates.table_is_scoped(&row.table) && !allow_unscoped
240        {
241            return Err(GateError::InvalidInboundAudiencePackage(format!(
242                "Circle package contains unscoped table {}",
243                row.table
244            )));
245        }
246        if row.op != ffi::SQLITE_DELETE {
247            if let Some(TableGate::ScopedRoot { audience_col }) = gates.tables.get(&row.table) {
248                if let Some(value) = row.new_value(audience_col.index) {
249                    let row_audience = Audience::from_column(value).map_err(|source| {
250                        GateError::InvalidInboundAudienceEncoding {
251                            context: format!("scoped row {} has an invalid audience", row.table),
252                            source,
253                        }
254                    })?;
255                    if &row_audience != package_audience {
256                        return Err(GateError::InvalidInboundAudiencePackage(format!(
257                            "scoped row {} is packaged for a different audience than its row value",
258                            row.table
259                        )));
260                    }
261                }
262            }
263        }
264        Ok(())
265    })?;
266
267    let group = Changegroup::new()?;
268    group.set_schema(conn.handle())?;
269    for_each_change(changeset, |iter, row| {
270        if row.table != "_coven_row_routes" && !gates.table_is_scoped(&row.table) {
271            group.add_change(iter)?;
272            return Ok(());
273        }
274        let routing_id = if row.table == "_coven_row_routes" {
275            row.pk()
276                .ok_or_else(|| GateError::MissingChangesetPrimaryKey(row.table.clone()))?
277                .to_string()
278        } else {
279            let row_id = row
280                .pk()
281                .ok_or_else(|| GateError::MissingChangesetPrimaryKey(row.table.clone()))?;
282            row_routing_id(routing_key, &row.table, row_id).to_string()
283        };
284        let winning_audience = winning_store_audience(conn, &routing_id)?;
285        if winning_audience.as_ref() == Some(package_audience) {
286            group.add_change(iter)?;
287        }
288        Ok(())
289    })?;
290    group.output()
291}
292
293unsafe fn normalize_inbound_private_routes_raw(
294    conn: &Connection,
295    changeset: &[u8],
296    package_audience: &Audience,
297    routing_source: InboundRoutingSource<'_>,
298    gates: &Gates,
299    routing_key: &RowRoutingKey,
300) -> Result<(Vec<u8>, PackageRoutes), GateError> {
301    let package_routes = validate_inbound_private_routes_raw(
302        conn,
303        changeset,
304        package_audience,
305        routing_source,
306        gates,
307        routing_key,
308    )?;
309    let group = Changegroup::new()?;
310    group.set_schema(conn.handle())?;
311    for_each_change(changeset, |iter, row| {
312        if row.table != "_coven_row_routes" {
313            group.add_change(iter)?;
314        }
315        Ok(())
316    })?;
317    let mut rows = package_routes
318        .iter()
319        .map(|((table, row_id), (routing_id, stamp))| {
320            (
321                routing_id.clone(),
322                table.clone(),
323                row_id.clone(),
324                stamp.clone(),
325            )
326        })
327        .collect::<Vec<_>>();
328    rows.sort();
329    let canonical_routes = private_route_insert_changeset(&rows)?;
330    for_each_change(&canonical_routes, |iter, _row| group.add_change(iter))?;
331    Ok((group.output()?, package_routes))
332}
333
334unsafe fn validate_inbound_private_routes_raw(
335    conn: &Connection,
336    changeset: &[u8],
337    package_audience: &Audience,
338    routing_source: InboundRoutingSource<'_>,
339    gates: &Gates,
340    routing_key: &RowRoutingKey,
341) -> Result<PackageRoutes, GateError> {
342    let mut package_routes = PackageRoutes::new();
343    let mut package_row_inserts = HashSet::<(String, String)>::new();
344    for_each_change(changeset, |_iter, row| {
345        if row.table == "_coven_audience" {
346            return Ok(());
347        }
348        if row.table != "_coven_row_routes" {
349            if gates.table_is_scoped(&row.table) && row.op == ffi::SQLITE_INSERT {
350                let row_id = row
351                    .pk()
352                    .ok_or_else(|| GateError::MissingChangesetPrimaryKey(row.table.clone()))?;
353                let columns = crate::gate::gate_table_columns(conn, &row.table)?;
354                let stamp_index = columns
355                    .iter()
356                    .position(|column| column == "_updated_at")
357                    .ok_or_else(|| {
358                        GateError::MissingFkColumn(row.table.clone(), "_updated_at".to_string())
359                    })?;
360                row.new_value(stamp_index).flatten().ok_or_else(|| {
361                    GateError::InvalidInboundAudiencePackage(format!(
362                        "complete row INSERT {}.{row_id} has no _updated_at",
363                        row.table
364                    ))
365                })?;
366                package_row_inserts.insert((row.table.clone(), row_id.to_string()));
367            }
368            return Ok(());
369        }
370        if row.op != ffi::SQLITE_INSERT {
371            return Err(GateError::InvalidInboundAudiencePackage(
372                "private routes must be complete INSERT images".to_string(),
373            ));
374        }
375        let routing_id = row.new_value(0).flatten().ok_or_else(|| {
376            GateError::InvalidInboundAudiencePackage(
377                "private route INSERT has no routing id".to_string(),
378            )
379        })?;
380        let table = row.new_value(1).flatten().ok_or_else(|| {
381            GateError::InvalidInboundAudiencePackage("private route has no table name".to_string())
382        })?;
383        let row_id = row.new_value(2).flatten().ok_or_else(|| {
384            GateError::InvalidInboundAudiencePackage("private route has no row id".to_string())
385        })?;
386        let stamp = row.new_value(3).flatten().ok_or_else(|| {
387            GateError::InvalidInboundAudiencePackage("private route has no _updated_at".to_string())
388        })?;
389        if !gates.table_is_scoped(table) {
390            return Err(GateError::InvalidInboundAudiencePackage(format!(
391                "private route names unscoped table {table}"
392            )));
393        }
394        let identity = gates.row_identity(table).ok_or_else(|| {
395            GateError::InvalidInboundAudiencePackage(format!(
396                "private route names undeclared table {table}"
397            ))
398        })?;
399        identity.validate(table, row_id).map_err(|source| {
400            GateError::InvalidInboundRowIdentity {
401                context: "private route row identity is invalid".to_string(),
402                source,
403            }
404        })?;
405        let expected_routing_id = row_routing_id(routing_key, table, row_id).to_string();
406        if routing_id != expected_routing_id {
407            return Err(GateError::InvalidInboundAudiencePackage(format!(
408                "private route id does not authenticate {table}.{row_id}"
409            )));
410        }
411        if package_routes
412            .insert(
413                (table.to_string(), row_id.to_string()),
414                (routing_id.to_string(), stamp.to_string()),
415            )
416            .is_some()
417        {
418            return Err(GateError::InvalidInboundAudiencePackage(format!(
419                "duplicate private route for {table}.{row_id}"
420            )));
421        }
422        Ok(())
423    })?;
424    for (row, (routing_id, route_stamp)) in &package_routes {
425        if !package_row_inserts.contains(row) {
426            return Err(GateError::InvalidInboundAudiencePackage(format!(
427                "private route for {}.{} has no complete row INSERT",
428                row.0, row.1
429            )));
430        }
431        if let InboundRoutingSource::Published(store_transitions) = &routing_source {
432            let (transition_audience, audience_stamp) = store_transitions
433                .by_routing_id
434                .get(routing_id)
435                .ok_or_else(|| {
436                    GateError::InvalidInboundAudiencePackage(format!(
437                        "private route for {}.{} has no Store audience transition",
438                        row.0, row.1
439                    ))
440                })?;
441            if transition_audience != package_audience {
442                return Err(GateError::InvalidInboundAudiencePackage(format!(
443                    "private route for {}.{} is packaged for a different audience than its Store transition",
444                    row.0, row.1
445                )));
446            }
447            if route_stamp != audience_stamp {
448                return Err(GateError::InvalidInboundAudiencePackage(format!(
449                    "private route for {}.{} has a different _updated_at than its Store audience transition",
450                    row.0, row.1
451                )));
452            }
453        }
454    }
455    for row in &package_row_inserts {
456        if package_routes.contains_key(row) {
457            continue;
458        }
459        let existing = query_row_optional(
460            conn,
461            "SELECT routing_id FROM _coven_row_routes
462             WHERE table_name = ?1 AND row_id = ?2",
463            (&row.0, &row.1),
464            |record| record.get::<_, String>(0),
465        )?
466        .ok_or_else(|| {
467            GateError::InvalidInboundAudiencePackage(format!(
468                "scoped row INSERT {}.{} has no private route",
469                row.0, row.1
470            ))
471        })?;
472        let expected = row_routing_id(routing_key, &row.0, &row.1).to_string();
473        if existing != expected {
474            return Err(GateError::InvalidInboundAudiencePackage(format!(
475                "stored private route does not authenticate {}.{}",
476                row.0, row.1
477            )));
478        }
479    }
480    Ok(package_routes)
481}