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}