Skip to main content

coil_auth/capability/
model.rs

1use std::collections::HashMap;
2use std::fmt;
3use std::sync::Arc;
4
5use zanzibar::Schema;
6
7use crate::{CoilAuthError, Entity, Namespace, Relation};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum Capability {
11    SystemModuleManage,
12    SystemConfigRead,
13    SystemConfigWrite,
14    AdminShellAccess,
15    AdminAuditRead,
16    CmsPageRead,
17    CmsPagePublish,
18    CmsPageEdit,
19    CmsNavigationEdit,
20    CatalogProductRead,
21    CatalogProductEdit,
22    CatalogCollectionEdit,
23    CatalogFeaturedEdit,
24    CheckoutSessionCreate,
25    OrderRead,
26    OrderRefundIssue,
27    MembershipSubscriptionManage,
28    MembershipTierEdit,
29    EventsEventPublish,
30    EventsSlotManage,
31    EventsBookingManage,
32    EventsBookingCreate,
33    EventsBookingCheckIn,
34    AssetRead,
35    AssetReadPublic,
36    AssetPublish,
37    AssetReplace,
38    AssetManageStorage,
39    SeoMetadataEdit,
40    I18nTranslationEdit,
41}
42
43impl Capability {
44    pub const fn as_str(self) -> &'static str {
45        match self {
46            Self::SystemModuleManage => "system.module.manage",
47            Self::SystemConfigRead => "system.config.read",
48            Self::SystemConfigWrite => "system.config.write",
49            Self::AdminShellAccess => "admin.shell.access",
50            Self::AdminAuditRead => "admin.audit.read",
51            Self::CmsPageRead => "cms.page.read",
52            Self::CmsPagePublish => "cms.page.publish",
53            Self::CmsPageEdit => "cms.page.edit",
54            Self::CmsNavigationEdit => "cms.navigation.edit",
55            Self::CatalogProductRead => "catalog.product.read",
56            Self::CatalogProductEdit => "catalog.product.edit",
57            Self::CatalogCollectionEdit => "catalog.collection.edit",
58            Self::CatalogFeaturedEdit => "catalog.featured.edit",
59            Self::CheckoutSessionCreate => "checkout.session.create",
60            Self::OrderRead => "order.read",
61            Self::OrderRefundIssue => "order.refund.issue",
62            Self::MembershipSubscriptionManage => "membership.subscription.manage",
63            Self::MembershipTierEdit => "membership.tier.edit",
64            Self::EventsEventPublish => "events.event.publish",
65            Self::EventsSlotManage => "events.slot.manage",
66            Self::EventsBookingManage => "events.booking.manage",
67            Self::EventsBookingCreate => "events.booking.create",
68            Self::EventsBookingCheckIn => "events.booking.check_in",
69            Self::AssetRead => "asset.read",
70            Self::AssetReadPublic => "asset.read_public",
71            Self::AssetPublish => "asset.publish",
72            Self::AssetReplace => "asset.replace",
73            Self::AssetManageStorage => "asset.manage_storage",
74            Self::SeoMetadataEdit => "seo.metadata.edit",
75            Self::I18nTranslationEdit => "i18n.translation.edit",
76        }
77    }
78
79    pub fn from_str(value: &str) -> Option<Self> {
80        match value {
81            "system.module.manage" => Some(Self::SystemModuleManage),
82            "system.config.read" => Some(Self::SystemConfigRead),
83            "system.config.write" => Some(Self::SystemConfigWrite),
84            "admin.shell.access" => Some(Self::AdminShellAccess),
85            "admin.audit.read" => Some(Self::AdminAuditRead),
86            "cms.page.read" => Some(Self::CmsPageRead),
87            "cms.page.publish" => Some(Self::CmsPagePublish),
88            "cms.page.edit" => Some(Self::CmsPageEdit),
89            "cms.navigation.edit" => Some(Self::CmsNavigationEdit),
90            "catalog.product.read" => Some(Self::CatalogProductRead),
91            "catalog.product.edit" => Some(Self::CatalogProductEdit),
92            "catalog.collection.edit" => Some(Self::CatalogCollectionEdit),
93            "catalog.featured.edit" => Some(Self::CatalogFeaturedEdit),
94            "checkout.session.create" => Some(Self::CheckoutSessionCreate),
95            "order.read" => Some(Self::OrderRead),
96            "order.refund.issue" => Some(Self::OrderRefundIssue),
97            "membership.subscription.manage" => Some(Self::MembershipSubscriptionManage),
98            "membership.tier.edit" => Some(Self::MembershipTierEdit),
99            "events.event.publish" => Some(Self::EventsEventPublish),
100            "events.slot.manage" => Some(Self::EventsSlotManage),
101            "events.booking.manage" => Some(Self::EventsBookingManage),
102            "events.booking.create" => Some(Self::EventsBookingCreate),
103            "events.booking.check_in" => Some(Self::EventsBookingCheckIn),
104            "asset.read" => Some(Self::AssetRead),
105            "asset.read_public" => Some(Self::AssetReadPublic),
106            "asset.publish" => Some(Self::AssetPublish),
107            "asset.replace" => Some(Self::AssetReplace),
108            "asset.manage_storage" => Some(Self::AssetManageStorage),
109            "seo.metadata.edit" => Some(Self::SeoMetadataEdit),
110            "i18n.translation.edit" => Some(Self::I18nTranslationEdit),
111            _ => None,
112        }
113    }
114}
115
116impl fmt::Display for Capability {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        f.write_str(self.as_str())
119    }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub enum PackageMode {
124    Replace,
125    Extend,
126}
127
128impl fmt::Display for PackageMode {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        match self {
131            Self::Replace => f.write_str("replace"),
132            Self::Extend => f.write_str("extend"),
133        }
134    }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct PackageVersion {
139    pub major: u16,
140    pub minor: u16,
141    pub patch: u16,
142}
143
144impl PackageVersion {
145    pub const fn new(major: u16, minor: u16, patch: u16) -> Self {
146        Self {
147            major,
148            minor,
149            patch,
150        }
151    }
152}
153
154impl fmt::Display for PackageVersion {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
157    }
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct AuthModelManifest {
162    pub name: String,
163    pub version: PackageVersion,
164    pub mode: PackageMode,
165    pub storage_schema_version: u32,
166    pub model_version: u32,
167    pub capability_binding_version: u32,
168    pub imports: Vec<String>,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct CapabilityBinding {
173    pub capability: Capability,
174    pub resource_namespaces: Vec<Namespace>,
175    pub relation: Relation,
176}
177
178impl CapabilityBinding {
179    pub fn matches_namespace(&self, namespace: Namespace) -> bool {
180        self.resource_namespaces.contains(&namespace)
181    }
182}
183
184pub trait AuthModelPackage: Send + Sync {
185    fn manifest(&self) -> &AuthModelManifest;
186    fn schema(&self) -> &Schema;
187    fn capability_bindings(&self) -> &HashMap<Capability, CapabilityBinding>;
188
189    fn binding_for(&self, capability: Capability) -> Option<&CapabilityBinding> {
190        self.capability_bindings().get(&capability)
191    }
192
193    fn resolve_binding(
194        &self,
195        capability: Capability,
196        resource: &Entity,
197    ) -> Result<&CapabilityBinding, CoilAuthError> {
198        let binding = self
199            .binding_for(capability)
200            .ok_or(CoilAuthError::MissingCapabilityBinding { capability })?;
201
202        if binding.matches_namespace(resource.namespace()) {
203            Ok(binding)
204        } else {
205            Err(CoilAuthError::ResourceNamespaceMismatch {
206                capability,
207                actual: resource.namespace(),
208                expected: binding.resource_namespaces.clone(),
209            })
210        }
211    }
212}
213
214#[derive(Clone)]
215pub struct AuthModelPackageSelection {
216    package: Arc<dyn AuthModelPackage>,
217}
218
219impl AuthModelPackageSelection {
220    pub fn new<P>(package: P) -> Self
221    where
222        P: AuthModelPackage + 'static,
223    {
224        Self {
225            package: Arc::new(package),
226        }
227    }
228
229    pub fn manifest(&self) -> &AuthModelManifest {
230        self.package.manifest()
231    }
232
233    pub fn package(&self) -> &dyn AuthModelPackage {
234        self.package.as_ref()
235    }
236}
237
238impl fmt::Debug for AuthModelPackageSelection {
239    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240        f.debug_struct("AuthModelPackageSelection")
241            .field("manifest", &self.package.manifest())
242            .finish()
243    }
244}