Skip to main content

platform_system_plane/
enrollment.rs

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}