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}