Skip to main content

coil_runtime/storage/
gate.rs

1use coil_assets::ManagedAsset;
2use coil_auth::{AuthModelPackage, Capability, CoilAuth, DefaultSubject};
3use zanzibar::RebacEngine;
4
5use super::RuntimeStorageError;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub struct ManagedAssetPublicationGate {
9    pub can_publish: bool,
10    pub can_replace: bool,
11    pub can_manage_storage: bool,
12    pub public_delivery_enabled: bool,
13}
14
15impl ManagedAssetPublicationGate {
16    pub fn can_publish_publicly(&self) -> bool {
17        self.can_publish
18            && self.can_replace
19            && self.can_manage_storage
20            && self.public_delivery_enabled
21    }
22
23    pub fn ensure_public_delivery_allowed(
24        &self,
25        asset_id: impl Into<String>,
26    ) -> Result<(), RuntimeStorageError> {
27        if self.can_publish_publicly() {
28            Ok(())
29        } else {
30            Err(RuntimeStorageError::PublicationAuthorizationDenied {
31                asset_id: asset_id.into(),
32                reason: self.denial_reason(),
33            })
34        }
35    }
36
37    pub async fn resolve<E>(
38        auth: &CoilAuth<E>,
39        package: &impl AuthModelPackage,
40        subject: &DefaultSubject,
41        asset: &ManagedAsset,
42    ) -> Result<Self, RuntimeStorageError>
43    where
44        E: RebacEngine,
45    {
46        let asset_entity = asset.auth_entity();
47        let can_publish = check_capability(
48            auth,
49            package,
50            subject,
51            asset,
52            Capability::AssetPublish,
53            &asset_entity,
54        )
55        .await?;
56        let can_replace = check_capability(
57            auth,
58            package,
59            subject,
60            asset,
61            Capability::AssetReplace,
62            &asset_entity,
63        )
64        .await?;
65        let can_manage_storage = check_capability(
66            auth,
67            package,
68            subject,
69            asset,
70            Capability::AssetManageStorage,
71            &asset_entity,
72        )
73        .await?;
74
75        Ok(Self {
76            can_publish,
77            can_replace,
78            can_manage_storage,
79            public_delivery_enabled: asset.publication().is_published()
80                && asset
81                    .publication()
82                    .live_revision()
83                    .is_some_and(|revision| revision.storage_plan().public_delivery_eligible()),
84        })
85    }
86
87    fn denial_reason(&self) -> String {
88        let mut missing = Vec::new();
89        if !self.can_publish {
90            missing.push(Capability::AssetPublish.to_string());
91        }
92        if !self.can_replace {
93            missing.push(Capability::AssetReplace.to_string());
94        }
95        if !self.can_manage_storage {
96            missing.push(Capability::AssetManageStorage.to_string());
97        }
98        if !self.public_delivery_enabled {
99            missing.push("published public delivery state".to_string());
100        }
101        missing.join(", ")
102    }
103}
104
105async fn check_capability<E>(
106    auth: &CoilAuth<E>,
107    package: &impl AuthModelPackage,
108    subject: &DefaultSubject,
109    asset: &ManagedAsset,
110    capability: Capability,
111    asset_entity: &coil_auth::Entity,
112) -> Result<bool, RuntimeStorageError>
113where
114    E: RebacEngine,
115{
116    auth.check_capability(package, subject, capability, asset_entity)
117        .await
118        .map_err(|_| RuntimeStorageError::PublicationAuthorizationDenied {
119            asset_id: asset.id().to_string(),
120            reason: capability.to_string(),
121        })
122}