1use async_trait::async_trait;
2use lenso_service::system_plane::{
3 EnrollmentCapabilityGrant, EnrollmentOffer, EnrollmentPolicyGrant, EnrollmentReceipt,
4 EnrollmentSignature, EnrollmentSignatureAlgorithm, EnrollmentSignatureVerifier,
5 EnrollmentSigner, enrollment_offer_digest, enrollment_receipt_digest, sign_enrollment_receipt,
6 verify_enrollment_offer,
7};
8use platform_core::Migration;
9use serde::{Deserialize, Serialize};
10use sqlx::PgPool;
11use std::sync::Arc;
12
13pub const SYSTEM_PLANE_MIGRATIONS: &[Migration] = &[
14 Migration {
15 name: "system-plane/0001_create_enrollment_grants",
16 sql: include_str!("../migrations/0001_create_enrollment_grants.sql"),
17 },
18 Migration {
19 name: "system-plane/0002_create_enrollment_ceremonies",
20 sql: include_str!("../migrations/0002_create_enrollment_ceremonies.sql"),
21 },
22];
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "camelCase", deny_unknown_fields)]
26pub struct EnrollmentGrant {
27 pub system_id: String,
28 pub managed_service_id: String,
29 pub managed_service_principal: String,
30 pub managed_service_revision: String,
31 pub console_service_principal: String,
32 pub offer_digest: String,
33 pub receipt_digest: String,
34 pub grant_revision: u64,
35 pub authorization_epoch: u64,
36 pub expires_at_unix_ms: u64,
37 pub capabilities: Vec<EnrollmentCapabilityGrant>,
38 pub policy: EnrollmentPolicyGrant,
39}
40
41impl EnrollmentGrant {
42 #[must_use]
43 pub fn system_sandbox(
44 managed_service_id: impl Into<String>,
45 console_service_principal: impl Into<String>,
46 authorization_epoch: u64,
47 expires_at_unix_ms: u64,
48 ) -> Self {
49 let managed_service_id = managed_service_id.into();
50 Self {
51 system_id: "system-sandbox".to_owned(),
52 managed_service_principal: format!("service:{managed_service_id}"),
53 managed_service_revision: "system-sandbox".to_owned(),
54 managed_service_id,
55 console_service_principal: console_service_principal.into(),
56 offer_digest: format!("sha256:{}", "0".repeat(64)),
57 receipt_digest: format!("sha256:{}", "1".repeat(64)),
58 grant_revision: 1,
59 authorization_epoch,
60 expires_at_unix_ms,
61 capabilities: Vec::new(),
62 policy: EnrollmentPolicyGrant {
63 policy_id: "system-sandbox".to_owned(),
64 policy_revision: "1".to_owned(),
65 policy_digest: format!("sha256:{}", "2".repeat(64)),
66 },
67 }
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase", deny_unknown_fields)]
73pub struct EnrollmentRecord {
74 pub grant: EnrollmentGrant,
75 pub revoked_at_unix_ms: Option<u64>,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct EnrollmentAuthorization {
80 pub system_id: String,
81 pub managed_service_id: String,
82 pub managed_service_principal: String,
83 pub managed_service_revision: String,
84 pub console_service_principal: String,
85 pub receipt_digest: String,
86 pub grant_revision: u64,
87 pub authorization_epoch: u64,
88 pub expires_at_unix_ms: u64,
89 pub capabilities: Vec<EnrollmentCapabilityGrant>,
90 pub policy: EnrollmentPolicyGrant,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum EnrollmentErrorCode {
95 InvalidGrant,
96 InvalidDecision,
97 SignatureRejected,
98 NonceReused,
99 AlreadyEnrolled,
100 NotEnrolled,
101 PrincipalMismatch,
102 Revoked,
103 Expired,
104 StaleAuthorizationEpoch,
105 StoreUnavailable,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct EnrollmentAcceptance {
110 pub managed_service_id: String,
111 pub managed_service_principal: String,
112 pub managed_service_revision: String,
113 pub grant_revision: u64,
114 pub authorization_epoch: u64,
115 pub expires_at_unix_ms: u64,
116 pub capabilities: Vec<EnrollmentCapabilityGrant>,
117 pub policy: EnrollmentPolicyGrant,
118}
119
120#[derive(Debug)]
121pub struct EnrollmentCeremony {
122 store: PostgresEnrollmentStore,
123 console_verifier: Arc<dyn EnrollmentSignatureVerifier>,
124 service_signer: Arc<dyn EnrollmentSigner>,
125}
126
127impl EnrollmentCeremony {
128 #[must_use]
129 pub fn new(
130 store: PostgresEnrollmentStore,
131 console_verifier: Arc<dyn EnrollmentSignatureVerifier>,
132 service_signer: Arc<dyn EnrollmentSigner>,
133 ) -> Self {
134 Self {
135 store,
136 console_verifier,
137 service_signer,
138 }
139 }
140
141 pub async fn accept(
142 &self,
143 offer: &EnrollmentOffer,
144 acceptance: &EnrollmentAcceptance,
145 now_unix_ms: u64,
146 ) -> Result<EnrollmentReceipt, EnrollmentError> {
147 let offer_digest =
148 verify_enrollment_offer(offer, self.console_verifier.as_ref(), now_unix_ms).map_err(
149 |_| {
150 error(
151 EnrollmentErrorCode::SignatureRejected,
152 "Enrollment Offer signature, lifetime, or canonical content was rejected",
153 )
154 },
155 )?;
156 validate_acceptance(offer, acceptance, now_unix_ms)?;
157 let receipt = sign_enrollment_receipt(
158 EnrollmentReceipt {
159 protocol: String::new(),
160 offer_digest,
161 system_id: offer.system_id.clone(),
162 managed_service_id: acceptance.managed_service_id.clone(),
163 managed_service_principal: acceptance.managed_service_principal.clone(),
164 managed_service_revision: acceptance.managed_service_revision.clone(),
165 console_service_principal: offer.console_service_principal.clone(),
166 nonce: offer.nonce.clone(),
167 issued_at_unix_ms: now_unix_ms,
168 expires_at_unix_ms: acceptance.expires_at_unix_ms,
169 grant_revision: acceptance.grant_revision,
170 authorization_epoch: acceptance.authorization_epoch,
171 granted_capabilities: acceptance.capabilities.clone(),
172 granted_policy: acceptance.policy.clone(),
173 signature: EnrollmentSignature {
174 algorithm: EnrollmentSignatureAlgorithm::Ed25519,
175 key_id: self.service_signer.key_id().to_owned(),
176 subject_digest: String::new(),
177 value: String::new(),
178 },
179 },
180 self.service_signer.as_ref(),
181 )
182 .map_err(|_| {
183 error(
184 EnrollmentErrorCode::SignatureRejected,
185 "Managed Service could not sign the Enrollment Receipt",
186 )
187 })?;
188 self.store.persist_receipt(offer, &receipt).await
189 }
190}
191
192#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
193#[error("{message}")]
194pub struct EnrollmentError {
195 pub code: EnrollmentErrorCode,
196 pub message: String,
197}
198
199#[async_trait]
200pub trait EnrollmentAuthorizer: std::fmt::Debug + Send + Sync {
201 async fn authorize(
202 &self,
203 managed_service_id: &str,
204 console_service_principal: &str,
205 now_unix_ms: u64,
206 ) -> Result<EnrollmentAuthorization, EnrollmentError>;
207}
208
209#[derive(Debug, Clone)]
210pub struct PostgresEnrollmentStore {
211 pool: PgPool,
212}
213
214impl PostgresEnrollmentStore {
215 #[must_use]
216 pub fn new(pool: PgPool) -> Self {
217 Self { pool }
218 }
219
220 pub async fn revoke(
221 &self,
222 managed_service_id: &str,
223 expected_authorization_epoch: u64,
224 revoked_at_unix_ms: u64,
225 ) -> Result<EnrollmentRecord, EnrollmentError> {
226 if managed_service_id.trim().is_empty() || revoked_at_unix_ms == 0 {
227 return Err(error(
228 EnrollmentErrorCode::InvalidGrant,
229 "Revocation requires a managed Service identity and positive timestamp",
230 ));
231 }
232 let expected_epoch = to_i64(expected_authorization_epoch, "authorization epoch")?;
233 let revoked_at = to_i64(revoked_at_unix_ms, "revocation timestamp")?;
234 let mut transaction = self.pool.begin().await.map_err(store_error)?;
235 let row = sqlx::query_as::<_, EnrollmentRow>(
236 r#"
237 update platform.system_plane_enrollment_grants
238 set revoked_at_unix_ms = $3,
239 authorization_epoch = authorization_epoch + 1,
240 updated_at = now()
241 where managed_service_id = $1
242 and authorization_epoch = $2
243 and revoked_at_unix_ms is null
244 returning managed_service_id, console_service_principal, grant_revision,
245 authorization_epoch, expires_at_unix_ms, revoked_at_unix_ms,
246 system_id, managed_service_principal, managed_service_revision,
247 offer_digest, receipt_digest, capabilities, policy
248 "#,
249 )
250 .bind(managed_service_id)
251 .bind(expected_epoch)
252 .bind(revoked_at)
253 .fetch_optional(&mut *transaction)
254 .await
255 .map_err(store_error)?;
256 let record: EnrollmentRecord = match row {
257 Some(row) => row.try_into()?,
258 None => {
259 return Err(error(
260 EnrollmentErrorCode::StaleAuthorizationEpoch,
261 "Enrollment is missing, revoked, or has advanced beyond the expected authorization epoch",
262 ));
263 }
264 };
265 append_audit(&mut transaction, "enrollment_revoked", &record).await?;
266 transaction.commit().await.map_err(store_error)?;
267 Ok(record)
268 }
269
270 async fn record(&self, managed_service_id: &str) -> Result<EnrollmentRecord, EnrollmentError> {
271 let row = sqlx::query_as::<_, EnrollmentRow>(
272 r#"
273 select managed_service_id, console_service_principal, grant_revision,
274 authorization_epoch, expires_at_unix_ms, revoked_at_unix_ms,
275 system_id, managed_service_principal, managed_service_revision,
276 offer_digest, receipt_digest, capabilities, policy
277 from platform.system_plane_enrollment_grants
278 where managed_service_id = $1
279 "#,
280 )
281 .bind(managed_service_id)
282 .fetch_optional(&self.pool)
283 .await
284 .map_err(store_error)?
285 .ok_or_else(|| {
286 error(
287 EnrollmentErrorCode::NotEnrolled,
288 "The managed Service has no enrollment grant",
289 )
290 })?;
291 row.try_into()
292 }
293
294 async fn persist_receipt(
295 &self,
296 offer: &EnrollmentOffer,
297 receipt: &EnrollmentReceipt,
298 ) -> Result<EnrollmentReceipt, EnrollmentError> {
299 let receipt_digest = enrollment_receipt_digest(receipt);
300 let mut transaction = self.pool.begin().await.map_err(store_error)?;
301 sqlx::query(
302 "select pg_advisory_xact_lock(hashtextextended($1, 0)), pg_advisory_xact_lock(hashtextextended($2, 1))",
303 )
304 .bind(&receipt.managed_service_id)
305 .bind(&receipt.nonce)
306 .execute(&mut *transaction)
307 .await
308 .map_err(store_error)?;
309 if let Some(existing) = sqlx::query_scalar::<_, serde_json::Value>(
310 "select receipt from platform.system_plane_enrollment_receipts where offer_digest = $1",
311 )
312 .bind(&receipt.offer_digest)
313 .fetch_optional(&mut *transaction)
314 .await
315 .map_err(store_error)?
316 {
317 let existing = serde_json::from_value(existing).map_err(serialization_error)?;
318 transaction.commit().await.map_err(store_error)?;
319 return Ok(existing);
320 }
321 let nonce_owner = sqlx::query_scalar::<_, String>(
322 "select offer_digest from platform.system_plane_enrollment_receipts where nonce = $1",
323 )
324 .bind(&receipt.nonce)
325 .fetch_optional(&mut *transaction)
326 .await
327 .map_err(store_error)?;
328 if nonce_owner.is_some_and(|digest| digest != receipt.offer_digest) {
329 return Err(error(
330 EnrollmentErrorCode::NonceReused,
331 "Enrollment nonce was already consumed by a different Offer",
332 ));
333 }
334 let grant = EnrollmentGrant {
335 system_id: receipt.system_id.clone(),
336 managed_service_id: receipt.managed_service_id.clone(),
337 managed_service_principal: receipt.managed_service_principal.clone(),
338 managed_service_revision: receipt.managed_service_revision.clone(),
339 console_service_principal: receipt.console_service_principal.clone(),
340 offer_digest: enrollment_offer_digest(offer),
341 receipt_digest: receipt_digest.clone(),
342 grant_revision: receipt.grant_revision,
343 authorization_epoch: receipt.authorization_epoch,
344 expires_at_unix_ms: receipt.expires_at_unix_ms,
345 capabilities: receipt.granted_capabilities.clone(),
346 policy: receipt.granted_policy.clone(),
347 };
348 validate_grant(&grant)?;
349 let existing = sqlx::query_as::<_, (String, i64, i64, Option<i64>)>(
350 r#"
351 select system_id, grant_revision, authorization_epoch, revoked_at_unix_ms
352 from platform.system_plane_enrollment_grants
353 where managed_service_id = $1
354 for update
355 "#,
356 )
357 .bind(&grant.managed_service_id)
358 .fetch_optional(&mut *transaction)
359 .await
360 .map_err(store_error)?;
361 let event_kind = match existing {
362 None => {
363 insert_grant(&mut transaction, &grant).await?;
364 "enrollment_accepted"
365 }
366 Some((_, _, _, None)) => {
367 return Err(error(
368 EnrollmentErrorCode::AlreadyEnrolled,
369 "The managed Service already has an active enrollment; revoke it before transfer",
370 ));
371 }
372 Some((current_system_id, current_revision, current_epoch, Some(_))) => {
373 if grant.system_id != current_system_id
374 || grant.grant_revision <= to_u64(current_revision, "grant revision")?
375 || grant.authorization_epoch <= to_u64(current_epoch, "authorization epoch")?
376 {
377 return Err(error(
378 EnrollmentErrorCode::StaleAuthorizationEpoch,
379 "Signed enrollment transfer must preserve System identity and advance Grant revision and authorization epoch",
380 ));
381 }
382 replace_revoked_grant(&mut transaction, &grant, current_epoch).await?;
383 "enrollment_transferred"
384 }
385 };
386 let receipt_json = serde_json::to_value(receipt).map_err(serialization_error)?;
387 sqlx::query(
388 r#"
389 insert into platform.system_plane_enrollment_receipts (
390 receipt_digest, offer_digest, nonce, managed_service_id, receipt
391 ) values ($1, $2, $3, $4, $5)
392 "#,
393 )
394 .bind(&receipt_digest)
395 .bind(&receipt.offer_digest)
396 .bind(&receipt.nonce)
397 .bind(&receipt.managed_service_id)
398 .bind(&receipt_json)
399 .execute(&mut *transaction)
400 .await
401 .map_err(store_error)?;
402 sqlx::query(
403 r#"
404 insert into platform.system_plane_enrollment_audit (
405 managed_service_id, event_kind, receipt_digest, authorization_epoch, evidence
406 ) values ($1, $2, $3, $4, $5)
407 "#,
408 )
409 .bind(&receipt.managed_service_id)
410 .bind(event_kind)
411 .bind(&receipt_digest)
412 .bind(to_i64(receipt.authorization_epoch, "authorization epoch")?)
413 .bind(&receipt_json)
414 .execute(&mut *transaction)
415 .await
416 .map_err(store_error)?;
417 transaction.commit().await.map_err(store_error)?;
418 Ok(receipt.clone())
419 }
420}
421
422#[async_trait]
423impl EnrollmentAuthorizer for PostgresEnrollmentStore {
424 async fn authorize(
425 &self,
426 managed_service_id: &str,
427 console_service_principal: &str,
428 now_unix_ms: u64,
429 ) -> Result<EnrollmentAuthorization, EnrollmentError> {
430 authorize_record(
431 self.record(managed_service_id).await?,
432 console_service_principal,
433 now_unix_ms,
434 )
435 }
436}
437
438#[derive(Debug, Clone)]
439pub struct SystemSandboxEnrollmentAuthorizer {
440 record: Arc<EnrollmentRecord>,
441}
442
443impl SystemSandboxEnrollmentAuthorizer {
444 pub fn new(environment: &str, grant: EnrollmentGrant) -> Result<Self, EnrollmentError> {
445 if !matches!(environment, "local" | "development" | "test") {
446 return Err(error(
447 EnrollmentErrorCode::InvalidGrant,
448 "System Sandbox enrollment is forbidden outside local development and tests",
449 ));
450 }
451 validate_grant(&grant)?;
452 Ok(Self {
453 record: Arc::new(EnrollmentRecord {
454 grant,
455 revoked_at_unix_ms: None,
456 }),
457 })
458 }
459}
460
461#[async_trait]
462impl EnrollmentAuthorizer for SystemSandboxEnrollmentAuthorizer {
463 async fn authorize(
464 &self,
465 managed_service_id: &str,
466 console_service_principal: &str,
467 now_unix_ms: u64,
468 ) -> Result<EnrollmentAuthorization, EnrollmentError> {
469 if self.record.grant.managed_service_id != managed_service_id {
470 return Err(error(
471 EnrollmentErrorCode::NotEnrolled,
472 "The enrollment grant belongs to a different managed Service",
473 ));
474 }
475 authorize_record(
476 self.record.as_ref().clone(),
477 console_service_principal,
478 now_unix_ms,
479 )
480 }
481}
482
483#[derive(sqlx::FromRow)]
484struct EnrollmentRow {
485 system_id: String,
486 managed_service_id: String,
487 managed_service_principal: String,
488 managed_service_revision: String,
489 console_service_principal: String,
490 offer_digest: String,
491 receipt_digest: String,
492 grant_revision: i64,
493 authorization_epoch: i64,
494 expires_at_unix_ms: i64,
495 revoked_at_unix_ms: Option<i64>,
496 capabilities: serde_json::Value,
497 policy: serde_json::Value,
498}
499
500impl TryFrom<EnrollmentRow> for EnrollmentRecord {
501 type Error = EnrollmentError;
502
503 fn try_from(row: EnrollmentRow) -> Result<Self, Self::Error> {
504 Ok(Self {
505 grant: EnrollmentGrant {
506 system_id: row.system_id,
507 managed_service_id: row.managed_service_id,
508 managed_service_principal: row.managed_service_principal,
509 managed_service_revision: row.managed_service_revision,
510 console_service_principal: row.console_service_principal,
511 offer_digest: row.offer_digest,
512 receipt_digest: row.receipt_digest,
513 grant_revision: to_u64(row.grant_revision, "grant revision")?,
514 authorization_epoch: to_u64(row.authorization_epoch, "authorization epoch")?,
515 expires_at_unix_ms: to_u64(row.expires_at_unix_ms, "expiry")?,
516 capabilities: serde_json::from_value(row.capabilities)
517 .map_err(serialization_error)?,
518 policy: serde_json::from_value(row.policy).map_err(serialization_error)?,
519 },
520 revoked_at_unix_ms: row
521 .revoked_at_unix_ms
522 .map(|value| to_u64(value, "revocation timestamp"))
523 .transpose()?,
524 })
525 }
526}
527
528fn authorize_record(
529 record: EnrollmentRecord,
530 principal: &str,
531 now_unix_ms: u64,
532) -> Result<EnrollmentAuthorization, EnrollmentError> {
533 if record.revoked_at_unix_ms.is_some() {
534 return Err(error(
535 EnrollmentErrorCode::Revoked,
536 "The managed Service enrollment has been revoked",
537 ));
538 }
539 if record.grant.console_service_principal != principal {
540 return Err(error(
541 EnrollmentErrorCode::PrincipalMismatch,
542 "Authenticated Service Principal does not match the active enrollment grant",
543 ));
544 }
545 if record.grant.expires_at_unix_ms <= now_unix_ms {
546 return Err(error(
547 EnrollmentErrorCode::Expired,
548 "The managed Service enrollment grant has expired",
549 ));
550 }
551 Ok(EnrollmentAuthorization {
552 system_id: record.grant.system_id,
553 managed_service_id: record.grant.managed_service_id,
554 managed_service_principal: record.grant.managed_service_principal,
555 managed_service_revision: record.grant.managed_service_revision,
556 console_service_principal: record.grant.console_service_principal,
557 receipt_digest: record.grant.receipt_digest,
558 grant_revision: record.grant.grant_revision,
559 authorization_epoch: record.grant.authorization_epoch,
560 expires_at_unix_ms: record.grant.expires_at_unix_ms,
561 capabilities: record.grant.capabilities,
562 policy: record.grant.policy,
563 })
564}
565
566fn validate_grant(grant: &EnrollmentGrant) -> Result<(), EnrollmentError> {
567 if grant.system_id.trim().is_empty()
568 || grant.managed_service_id.trim().is_empty()
569 || grant.managed_service_principal.trim().is_empty()
570 || grant.managed_service_revision.trim().is_empty()
571 || grant.console_service_principal.trim().is_empty()
572 || !canonical_digest(&grant.offer_digest)
573 || !canonical_digest(&grant.receipt_digest)
574 || grant.grant_revision == 0
575 || grant.expires_at_unix_ms == 0
576 {
577 return Err(error(
578 EnrollmentErrorCode::InvalidGrant,
579 "Enrollment Grant requires identities, signed artifact digests, positive revision, and positive expiry",
580 ));
581 }
582 to_i64(grant.grant_revision, "grant revision")?;
583 to_i64(grant.authorization_epoch, "authorization epoch")?;
584 to_i64(grant.expires_at_unix_ms, "expiry")?;
585 Ok(())
586}
587
588fn validate_acceptance(
589 offer: &EnrollmentOffer,
590 acceptance: &EnrollmentAcceptance,
591 now_unix_ms: u64,
592) -> Result<(), EnrollmentError> {
593 if acceptance.managed_service_id.trim().is_empty()
594 || acceptance.managed_service_principal.trim().is_empty()
595 || acceptance.managed_service_revision.trim().is_empty()
596 || acceptance.grant_revision == 0
597 || acceptance.expires_at_unix_ms <= now_unix_ms
598 || acceptance.expires_at_unix_ms > offer.expires_at_unix_ms
599 {
600 return Err(error(
601 EnrollmentErrorCode::InvalidDecision,
602 "Enrollment acceptance requires Service identity, positive revision, and an expiry bounded by the Offer",
603 ));
604 }
605 if acceptance.capabilities.iter().any(|granted| {
606 !offer.requested_capabilities.iter().any(|requested| {
607 requested.contract_id == granted.contract_id
608 && requested.schema_digest == granted.schema_digest
609 && granted.feature_ids.is_subset(&requested.feature_ids)
610 })
611 }) || acceptance.policy != offer.requested_policy
612 {
613 return Err(error(
614 EnrollmentErrorCode::InvalidDecision,
615 "Enrollment acceptance cannot widen the requested capabilities or substitute policy",
616 ));
617 }
618 Ok(())
619}
620
621fn canonical_digest(value: &str) -> bool {
622 value.strip_prefix("sha256:").is_some_and(|digest| {
623 digest.len() == 64
624 && digest
625 .bytes()
626 .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
627 })
628}
629
630fn to_i64(value: u64, field: &str) -> Result<i64, EnrollmentError> {
631 i64::try_from(value).map_err(|_| {
632 error(
633 EnrollmentErrorCode::InvalidGrant,
634 format!("Enrollment {field} exceeds the supported storage range"),
635 )
636 })
637}
638
639fn to_u64(value: i64, field: &str) -> Result<u64, EnrollmentError> {
640 u64::try_from(value).map_err(|_| {
641 error(
642 EnrollmentErrorCode::StoreUnavailable,
643 format!("Stored enrollment {field} is invalid"),
644 )
645 })
646}
647
648fn store_error(_source: sqlx::Error) -> EnrollmentError {
649 error(
650 EnrollmentErrorCode::StoreUnavailable,
651 "Enrollment Store operation failed",
652 )
653}
654
655fn serialization_error(_source: serde_json::Error) -> EnrollmentError {
656 error(
657 EnrollmentErrorCode::StoreUnavailable,
658 "Enrollment Store contains invalid ceremony evidence",
659 )
660}
661
662async fn insert_grant(
663 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
664 grant: &EnrollmentGrant,
665) -> Result<(), EnrollmentError> {
666 sqlx::query(
667 r#"
668 insert into platform.system_plane_enrollment_grants (
669 system_id, managed_service_id, managed_service_principal,
670 managed_service_revision, console_service_principal, offer_digest,
671 receipt_digest, grant_revision, authorization_epoch, expires_at_unix_ms,
672 capabilities, policy
673 ) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
674 "#,
675 )
676 .bind(&grant.system_id)
677 .bind(&grant.managed_service_id)
678 .bind(&grant.managed_service_principal)
679 .bind(&grant.managed_service_revision)
680 .bind(&grant.console_service_principal)
681 .bind(&grant.offer_digest)
682 .bind(&grant.receipt_digest)
683 .bind(to_i64(grant.grant_revision, "grant revision")?)
684 .bind(to_i64(grant.authorization_epoch, "authorization epoch")?)
685 .bind(to_i64(grant.expires_at_unix_ms, "expiry")?)
686 .bind(serde_json::to_value(&grant.capabilities).map_err(serialization_error)?)
687 .bind(serde_json::to_value(&grant.policy).map_err(serialization_error)?)
688 .execute(&mut **transaction)
689 .await
690 .map_err(store_error)?;
691 Ok(())
692}
693
694async fn replace_revoked_grant(
695 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
696 grant: &EnrollmentGrant,
697 expected_authorization_epoch: i64,
698) -> Result<(), EnrollmentError> {
699 let updated = sqlx::query(
700 r#"
701 update platform.system_plane_enrollment_grants
702 set system_id = $3,
703 managed_service_principal = $4,
704 managed_service_revision = $5,
705 console_service_principal = $6,
706 offer_digest = $7,
707 receipt_digest = $8,
708 grant_revision = $9,
709 authorization_epoch = $10,
710 expires_at_unix_ms = $11,
711 capabilities = $12,
712 policy = $13,
713 revoked_at_unix_ms = null,
714 updated_at = now()
715 where managed_service_id = $1
716 and authorization_epoch = $2
717 and revoked_at_unix_ms is not null
718 "#,
719 )
720 .bind(&grant.managed_service_id)
721 .bind(expected_authorization_epoch)
722 .bind(&grant.system_id)
723 .bind(&grant.managed_service_principal)
724 .bind(&grant.managed_service_revision)
725 .bind(&grant.console_service_principal)
726 .bind(&grant.offer_digest)
727 .bind(&grant.receipt_digest)
728 .bind(to_i64(grant.grant_revision, "grant revision")?)
729 .bind(to_i64(grant.authorization_epoch, "authorization epoch")?)
730 .bind(to_i64(grant.expires_at_unix_ms, "expiry")?)
731 .bind(serde_json::to_value(&grant.capabilities).map_err(serialization_error)?)
732 .bind(serde_json::to_value(&grant.policy).map_err(serialization_error)?)
733 .execute(&mut **transaction)
734 .await
735 .map_err(store_error)?;
736 if updated.rows_affected() != 1 {
737 return Err(error(
738 EnrollmentErrorCode::StaleAuthorizationEpoch,
739 "Enrollment authority changed while accepting the signed transfer",
740 ));
741 }
742 Ok(())
743}
744
745async fn append_audit(
746 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
747 event_kind: &str,
748 record: &EnrollmentRecord,
749) -> Result<(), EnrollmentError> {
750 let evidence = serde_json::to_value(record).map_err(serialization_error)?;
751 sqlx::query(
752 r#"
753 insert into platform.system_plane_enrollment_audit (
754 managed_service_id, event_kind, receipt_digest, authorization_epoch, evidence
755 ) values ($1, $2, $3, $4, $5)
756 "#,
757 )
758 .bind(&record.grant.managed_service_id)
759 .bind(event_kind)
760 .bind(&record.grant.receipt_digest)
761 .bind(to_i64(
762 record.grant.authorization_epoch,
763 "authorization epoch",
764 )?)
765 .bind(evidence)
766 .execute(&mut **transaction)
767 .await
768 .map_err(store_error)?;
769 Ok(())
770}
771
772fn error(code: EnrollmentErrorCode, message: impl Into<String>) -> EnrollmentError {
773 EnrollmentError {
774 code,
775 message: message.into(),
776 }
777}