1use chio_core::capability::token::CapabilityToken;
2pub use chio_kernel::capability_lineage::{
3 CapabilityLineageError, CapabilitySnapshot, CapabilitySnapshotProvenance,
4 StoredCapabilitySnapshot,
5};
6use rusqlite::types::Type;
7use rusqlite::{params, OptionalExtension, Row};
8
9use crate::receipt_store::support::sqlite_i64;
10use crate::receipt_store::{SqliteReceiptStore, SqliteStoreConnection};
11
12pub(crate) fn snapshot_from_row(row: &Row<'_>) -> rusqlite::Result<CapabilitySnapshot> {
13 snapshot_from_row_with_boundary(row, true)
14}
15
16fn snapshot_from_row_with_boundary(
17 row: &Row<'_>,
18 allow_legacy: bool,
19) -> rusqlite::Result<CapabilitySnapshot> {
20 validate_snapshot_from_row(
21 CapabilitySnapshot {
22 capability_id: row.get::<_, String>(0)?,
23 subject_key: row.get::<_, String>(1)?,
24 issuer_key: row.get::<_, String>(2)?,
25 issued_at: non_negative_u64_from_column(row, 3, "issued_at")?,
26 expires_at: non_negative_u64_from_column(row, 4, "expires_at")?,
27 grants_json: row.get::<_, String>(5)?,
28 delegation_depth: non_negative_u64_from_column(row, 6, "delegation_depth")?,
29 parent_capability_id: row.get::<_, Option<String>>(7)?,
30 federated_parent_capability_id: row.get::<_, Option<String>>(8)?,
31 provenance: provenance_from_row(row, 9)?,
32 signed_capability: signed_capability_from_row(row, 10)?,
33 },
34 10,
35 allow_legacy,
36 )
37}
38
39pub(crate) fn validate_snapshot_from_row(
40 snapshot: CapabilitySnapshot,
41 signed_capability_column: usize,
42 allow_legacy: bool,
43) -> rusqlite::Result<CapabilitySnapshot> {
44 let validation = if allow_legacy {
45 snapshot.validate_for_local_read()
46 } else {
47 snapshot.validate_for_transport()
48 };
49 validation.map_err(|error| {
50 rusqlite::Error::FromSqlConversionFailure(
51 signed_capability_column,
52 Type::Text,
53 Box::new(std::io::Error::new(
54 std::io::ErrorKind::InvalidData,
55 error.to_string(),
56 )),
57 )
58 })?;
59 Ok(snapshot)
60}
61
62pub(crate) fn provenance_from_row(
63 row: &Row<'_>,
64 column: usize,
65) -> rusqlite::Result<CapabilitySnapshotProvenance> {
66 row.get::<_, String>(column)?.parse().map_err(|error| {
67 rusqlite::Error::FromSqlConversionFailure(
68 column,
69 Type::Text,
70 Box::new(std::io::Error::new(std::io::ErrorKind::InvalidData, error)),
71 )
72 })
73}
74
75pub(crate) fn signed_capability_from_row(
76 row: &Row<'_>,
77 column: usize,
78) -> rusqlite::Result<Option<CapabilityToken>> {
79 row.get::<_, Option<String>>(column)?
80 .map(|json| {
81 serde_json::from_str(&json).map_err(|error| {
82 rusqlite::Error::FromSqlConversionFailure(column, Type::Text, Box::new(error))
83 })
84 })
85 .transpose()
86}
87
88pub(crate) fn validate_snapshot_for_transport(
89 snapshot: &CapabilitySnapshot,
90) -> Result<(), chio_kernel::ReceiptStoreError> {
91 snapshot.validate_for_transport()
92}
93
94pub(crate) fn ensure_snapshots_compatible(
95 existing: &CapabilitySnapshot,
96 incoming: &CapabilitySnapshot,
97) -> Result<(), chio_kernel::ReceiptStoreError> {
98 let existing_scope: serde_json::Value = serde_json::from_str(&existing.grants_json)?;
99 let incoming_scope: serde_json::Value = serde_json::from_str(&incoming.grants_json)?;
100 let scalar_fields_match = existing.capability_id == incoming.capability_id
101 && existing.subject_key == incoming.subject_key
102 && existing.issuer_key == incoming.issuer_key
103 && existing.issued_at == incoming.issued_at
104 && existing.expires_at == incoming.expires_at
105 && existing_scope == incoming_scope
106 && existing.delegation_depth == incoming.delegation_depth
107 && existing.parent_capability_id == incoming.parent_capability_id
108 && existing.federated_parent_capability_id == incoming.federated_parent_capability_id;
109 if !scalar_fields_match {
110 return Err(chio_kernel::ReceiptStoreError::Conflict(format!(
111 "capability lineage {} conflicts with the persisted projection",
112 incoming.capability_id
113 )));
114 }
115 if existing.provenance != incoming.provenance
116 && existing.provenance != CapabilitySnapshotProvenance::LegacyProjection
117 {
118 return Err(chio_kernel::ReceiptStoreError::Conflict(format!(
119 "capability lineage {} has conflicting provenance",
120 incoming.capability_id
121 )));
122 }
123 match (
124 existing.signed_capability.as_ref(),
125 incoming.signed_capability.as_ref(),
126 ) {
127 (Some(_), None) => Err(chio_kernel::ReceiptStoreError::Conflict(format!(
128 "capability lineage {} cannot discard its signed capability",
129 incoming.capability_id
130 ))),
131 (Some(existing), Some(incoming))
132 if serde_json::to_value(existing)? != serde_json::to_value(incoming)? =>
133 {
134 Err(chio_kernel::ReceiptStoreError::Conflict(format!(
135 "capability lineage {} has conflicting signed capabilities",
136 incoming.id
137 )))
138 }
139 _ => Ok(()),
140 }
141}
142
143pub(crate) fn persist_compatible_snapshot(
144 transaction: &rusqlite::Transaction<'_>,
145 incoming: &CapabilitySnapshot,
146) -> Result<(), chio_kernel::ReceiptStoreError> {
147 let issued_at = sqlite_i64(incoming.issued_at, "capability lineage issued_at")?;
148 let expires_at = sqlite_i64(incoming.expires_at, "capability lineage expires_at")?;
149 let delegation_depth = sqlite_i64(
150 incoming.delegation_depth,
151 "capability lineage delegation_depth",
152 )?;
153 let existing = transaction
154 .query_row(
155 r#"
156 SELECT
157 capability_id,
158 subject_key,
159 issuer_key,
160 issued_at,
161 expires_at,
162 grants_json,
163 delegation_depth,
164 parent_capability_id,
165 federated_parent_capability_id,
166 provenance,
167 signed_capability_json
168 FROM capability_lineage
169 WHERE capability_id = ?1
170 "#,
171 params![incoming.capability_id],
172 snapshot_from_row,
173 )
174 .optional()?;
175 let signed_capability_json = incoming
176 .signed_capability
177 .as_ref()
178 .map(serde_json::to_string)
179 .transpose()?;
180 let provenance = incoming.provenance.as_str();
181
182 if let Some(existing) = existing.as_ref() {
183 ensure_snapshots_compatible(existing, incoming)?;
184 if existing.provenance == CapabilitySnapshotProvenance::LegacyProjection
185 && incoming.provenance != CapabilitySnapshotProvenance::LegacyProjection
186 {
187 let max_rowid: i64 = transaction.query_row(
188 "SELECT COALESCE(MAX(rowid), 0) FROM capability_lineage",
189 [],
190 |row| row.get(0),
191 )?;
192 let next_rowid = max_rowid.checked_add(1).ok_or_else(|| {
193 chio_kernel::ReceiptStoreError::Conflict(
194 "capability lineage replication sequence is exhausted".to_string(),
195 )
196 })?;
197 let updated = transaction.execute(
198 r#"
199 UPDATE capability_lineage
200 SET signed_capability_json = ?2,
201 provenance = ?3,
202 rowid = ?4
203 WHERE capability_id = ?1
204 "#,
205 params![
206 incoming.capability_id,
207 signed_capability_json,
208 provenance,
209 next_rowid
210 ],
211 )?;
212 if updated != 1 {
213 return Err(chio_kernel::ReceiptStoreError::Conflict(format!(
214 "capability lineage {} disappeared during signed-token upgrade",
215 incoming.capability_id
216 )));
217 }
218 }
219 return Ok(());
220 }
221
222 transaction.execute(
223 r#"
224 INSERT INTO capability_lineage (
225 capability_id,
226 subject_key,
227 issuer_key,
228 issued_at,
229 expires_at,
230 grants_json,
231 delegation_depth,
232 parent_capability_id,
233 federated_parent_capability_id,
234 provenance,
235 signed_capability_json
236 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)
237 "#,
238 params![
239 incoming.capability_id,
240 incoming.subject_key,
241 incoming.issuer_key,
242 issued_at,
243 expires_at,
244 incoming.grants_json,
245 delegation_depth,
246 incoming.parent_capability_id,
247 incoming.federated_parent_capability_id,
248 provenance,
249 signed_capability_json,
250 ],
251 )?;
252 Ok(())
253}
254
255pub(crate) fn non_negative_u64_from_column(
256 row: &Row<'_>,
257 column: usize,
258 field_name: &'static str,
259) -> rusqlite::Result<u64> {
260 let value = row.get::<_, i64>(column)?;
261 if value < 0 {
262 return Err(negative_lineage_integer_error(column, field_name, value));
263 }
264 Ok(value as u64)
265}
266
267fn negative_lineage_integer_error(
268 column: usize,
269 field_name: &'static str,
270 value: i64,
271) -> rusqlite::Error {
272 rusqlite::Error::FromSqlConversionFailure(
273 column,
274 Type::Integer,
275 Box::new(std::io::Error::new(
276 std::io::ErrorKind::InvalidData,
277 format!("capability_lineage.{field_name} must be non-negative, got {value}"),
278 )),
279 )
280}
281
282impl SqliteReceiptStore {
283 pub fn record_capability_snapshot(
292 &self,
293 token: &CapabilityToken,
294 parent_capability_id: Option<&str>,
295 ) -> Result<(), CapabilityLineageError> {
296 self.record_capability_snapshot_bounded(token, parent_capability_id, None)
297 }
298
299 pub fn record_capability_snapshot_with_timeout(
304 &self,
305 token: &CapabilityToken,
306 parent_capability_id: Option<&str>,
307 budget: std::time::Duration,
308 ) -> Result<(), CapabilityLineageError> {
309 self.record_capability_snapshot_bounded(token, parent_capability_id, Some(budget))
310 }
311
312 fn record_capability_snapshot_bounded(
313 &self,
314 token: &CapabilityToken,
315 parent_capability_id: Option<&str>,
316 budget: Option<std::time::Duration>,
317 ) -> Result<(), CapabilityLineageError> {
318 let grants_json = serde_json::to_string(&token.scope)?;
319 let subject_key = token.subject.to_hex();
320 let issuer_key = token.issuer.to_hex();
321
322 let capability_id = token.id.clone();
323 let issued_at = token.issued_at;
324 let expires_at = token.expires_at;
325 let signed_capability = token.clone();
326 validate_snapshot_for_transport(&CapabilitySnapshot {
327 capability_id: capability_id.clone(),
328 subject_key: subject_key.clone(),
329 issuer_key: issuer_key.clone(),
330 issued_at,
331 expires_at,
332 grants_json: grants_json.clone(),
333 delegation_depth: token.delegation_chain.len() as u64,
334 parent_capability_id: token
335 .delegation_chain
336 .last()
337 .map(|link| link.capability_id.clone()),
338 federated_parent_capability_id: None,
339 provenance: CapabilitySnapshotProvenance::SignedToken,
340 signed_capability: Some(signed_capability.clone()),
341 })?;
342 let parent_capability_id = parent_capability_id.map(ToString::to_string);
343 let job = move |connection: &mut SqliteStoreConnection| {
344 let transaction =
345 connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
346 let delegation_depth: u64 = if let Some(parent_id) = parent_capability_id.as_deref() {
352 let parent_depth: Option<u64> = transaction
353 .query_row(
354 "SELECT delegation_depth FROM capability_lineage WHERE capability_id = ?1",
355 params![parent_id],
356 |row: &Row<'_>| non_negative_u64_from_column(row, 0, "delegation_depth"),
357 )
358 .optional()?;
359
360 parent_depth.map(|d| d.saturating_add(1)).unwrap_or(1)
361 } else {
362 0
363 };
364 let incoming = CapabilitySnapshot {
365 capability_id: capability_id.clone(),
366 subject_key: subject_key.clone(),
367 issuer_key: issuer_key.clone(),
368 issued_at,
369 expires_at,
370 grants_json: grants_json.clone(),
371 delegation_depth,
372 parent_capability_id: parent_capability_id.clone(),
373 federated_parent_capability_id: None,
374 provenance: CapabilitySnapshotProvenance::SignedToken,
375 signed_capability: Some(signed_capability),
376 };
377 validate_snapshot_for_transport(&incoming)?;
378 persist_compatible_snapshot(&transaction, &incoming)?;
379 transaction.commit()?;
380 Ok(())
381 };
382 match budget {
383 Some(budget) => self.writer_handle().run_write_with_timeout(job, budget)?,
384 None => self.writer_handle().run_write(job)?,
385 }
386
387 Ok(())
388 }
389
390 pub fn upsert_capability_snapshot(
395 &mut self,
396 snapshot: &CapabilitySnapshot,
397 ) -> Result<(), CapabilityLineageError> {
398 validate_snapshot_for_transport(snapshot)?;
399 let snapshot = snapshot.clone();
400 self.writer_handle().run_write(move |connection| {
401 let transaction =
402 connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
403 persist_compatible_snapshot(&transaction, &snapshot)?;
404 transaction.commit()?;
405 Ok(())
406 })?;
407 Ok(())
408 }
409
410 pub fn get_lineage(
414 &self,
415 capability_id: &str,
416 ) -> Result<Option<CapabilitySnapshot>, CapabilityLineageError> {
417 let row = self
418 .connection()?
419 .query_row(
420 r#"
421 SELECT
422 capability_id,
423 subject_key,
424 issuer_key,
425 issued_at,
426 expires_at,
427 grants_json,
428 delegation_depth,
429 parent_capability_id,
430 federated_parent_capability_id,
431 provenance,
432 signed_capability_json
433 FROM capability_lineage
434 WHERE capability_id = ?1
435 "#,
436 params![capability_id],
437 snapshot_from_row,
438 )
439 .optional()?;
440
441 Ok(row)
442 }
443
444 pub fn get_delegation_chain(
453 &self,
454 capability_id: &str,
455 ) -> Result<Vec<CapabilitySnapshot>, CapabilityLineageError> {
456 let connection = self.connection()?;
457 let mut stmt = connection.prepare(
458 r#"
459 WITH RECURSIVE chain(
460 capability_id,
461 subject_key,
462 issuer_key,
463 issued_at,
464 expires_at,
465 grants_json,
466 delegation_depth,
467 parent_capability_id,
468 federated_parent_capability_id,
469 provenance,
470 signed_capability_json,
471 level
472 ) AS (
473 SELECT
474 capability_id,
475 subject_key,
476 issuer_key,
477 issued_at,
478 expires_at,
479 grants_json,
480 delegation_depth,
481 parent_capability_id,
482 federated_parent_capability_id,
483 provenance,
484 signed_capability_json,
485 0 AS level
486 FROM capability_lineage
487 WHERE capability_id = ?1
488
489 UNION ALL
490
491 SELECT
492 cl.capability_id,
493 cl.subject_key,
494 cl.issuer_key,
495 cl.issued_at,
496 cl.expires_at,
497 cl.grants_json,
498 cl.delegation_depth,
499 cl.parent_capability_id,
500 cl.federated_parent_capability_id,
501 cl.provenance,
502 cl.signed_capability_json,
503 chain.level + 1
504 FROM capability_lineage cl
505 INNER JOIN chain ON cl.capability_id = chain.parent_capability_id
506 WHERE chain.level < 20
507 )
508 SELECT
509 capability_id,
510 subject_key,
511 issuer_key,
512 issued_at,
513 expires_at,
514 grants_json,
515 delegation_depth,
516 parent_capability_id,
517 federated_parent_capability_id,
518 provenance,
519 signed_capability_json
520 FROM chain
521 ORDER BY level DESC
522 "#,
523 )?;
524
525 let rows = stmt.query_map(params![capability_id], snapshot_from_row)?;
526
527 let mut chain = Vec::new();
528 for row in rows {
529 chain.push(row?);
530 }
531
532 Ok(chain)
533 }
534
535 pub fn list_capabilities_for_subject(
539 &self,
540 subject_key: &str,
541 ) -> Result<Vec<CapabilitySnapshot>, CapabilityLineageError> {
542 self.list_capability_snapshots(Some(subject_key), None)
543 }
544
545 pub fn list_capability_snapshots(
551 &self,
552 subject_key: Option<&str>,
553 issuer_key: Option<&str>,
554 ) -> Result<Vec<CapabilitySnapshot>, CapabilityLineageError> {
555 let connection = self.connection()?;
556 let mut stmt = connection.prepare(
557 r#"
558 SELECT
559 capability_id,
560 subject_key,
561 issuer_key,
562 issued_at,
563 expires_at,
564 grants_json,
565 delegation_depth,
566 parent_capability_id,
567 federated_parent_capability_id,
568 provenance,
569 signed_capability_json
570 FROM capability_lineage
571 WHERE (?1 IS NULL OR subject_key = ?1)
572 AND (?2 IS NULL OR issuer_key = ?2)
573 ORDER BY issued_at ASC, capability_id ASC
574 "#,
575 )?;
576
577 let rows = stmt.query_map(params![subject_key, issuer_key], snapshot_from_row)?;
578
579 let mut snapshots = Vec::new();
580 for row in rows {
581 snapshots.push(row?);
582 }
583
584 Ok(snapshots)
585 }
586
587 pub fn list_capability_snapshots_after_seq(
592 &self,
593 after_seq: u64,
594 limit: usize,
595 ) -> Result<Vec<StoredCapabilitySnapshot>, CapabilityLineageError> {
596 let after_seq = sqlite_i64(after_seq, "capability lineage after_seq")?;
597 let limit = i64::try_from(limit).map_err(|_| {
598 chio_kernel::ReceiptStoreError::Conflict(format!(
599 "capability lineage limit value {limit} exceeds SQLite INTEGER range"
600 ))
601 })?;
602 let connection = self.connection()?;
603 let mut stmt = connection.prepare(
604 r#"
605 SELECT
606 rowid,
607 capability_id,
608 subject_key,
609 issuer_key,
610 issued_at,
611 expires_at,
612 grants_json,
613 delegation_depth,
614 parent_capability_id,
615 federated_parent_capability_id,
616 provenance,
617 signed_capability_json
618 FROM capability_lineage
619 WHERE rowid > ?1
620 ORDER BY rowid ASC
621 LIMIT ?2
622 "#,
623 )?;
624
625 let rows = stmt.query_map(params![after_seq, limit], |row| {
626 Ok(StoredCapabilitySnapshot {
627 seq: non_negative_u64_from_column(row, 0, "rowid")?,
628 snapshot: validate_snapshot_from_row(
629 CapabilitySnapshot {
630 capability_id: row.get::<_, String>(1)?,
631 subject_key: row.get::<_, String>(2)?,
632 issuer_key: row.get::<_, String>(3)?,
633 issued_at: non_negative_u64_from_column(row, 4, "issued_at")?,
634 expires_at: non_negative_u64_from_column(row, 5, "expires_at")?,
635 grants_json: row.get::<_, String>(6)?,
636 delegation_depth: non_negative_u64_from_column(row, 7, "delegation_depth")?,
637 parent_capability_id: row.get::<_, Option<String>>(8)?,
638 federated_parent_capability_id: row.get::<_, Option<String>>(9)?,
639 provenance: provenance_from_row(row, 10)?,
640 signed_capability: signed_capability_from_row(row, 11)?,
641 },
642 11,
643 false,
644 )?,
645 })
646 })?;
647
648 let mut snapshots = Vec::new();
649 for row in rows {
650 snapshots.push(row?);
651 }
652
653 Ok(snapshots)
654 }
655
656 pub fn max_lineage_seq(&self) -> Result<u64, chio_kernel::ReceiptStoreError> {
661 let connection = self.connection()?;
662 let seq: i64 = connection.query_row(
663 "SELECT COALESCE(MAX(rowid), 0) FROM capability_lineage",
664 [],
665 |row| row.get(0),
666 )?;
667 crate::receipt_store::sqlite_u64(seq, "capability lineage max rowid")
668 }
669}
670
671#[cfg(test)]
672#[allow(clippy::expect_used, clippy::unwrap_used)]
673#[path = "capability_lineage_tests.rs"]
674mod tests;