unitycatalog_server/policy/
mod.rs1use std::sync::Arc;
9
10use strum::AsRefStr;
11use unitycatalog_common::models::{ResourceExt, ResourceIdent};
12
13pub use self::constant::*;
14use crate::api::SecuredAction;
15use crate::{Error, Result};
16
17mod constant;
18
19#[derive(Clone, Debug)]
20pub enum Principal {
21 Anonymous,
22 User(String),
23}
24
25impl Principal {
26 pub fn anonymous() -> Self {
27 Self::Anonymous
28 }
29
30 pub fn user(name: impl Into<String>) -> Self {
31 Self::User(name.into())
32 }
33}
34
35#[derive(Debug, Clone, AsRefStr, PartialEq, Eq, strum::EnumString)]
37#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
38pub enum Permission {
39 Read,
40 Write,
41 Manage,
42 Create,
43 Use,
44 Browse,
45 Select,
46}
47
48impl From<Permission> for String {
49 fn from(val: Permission) -> Self {
50 val.as_ref().to_string()
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum Decision {
57 Allow,
59 Deny,
61}
62
63#[async_trait::async_trait]
65pub trait Policy<Cx: Send + Sync + 'static>: Send + Sync + 'static {
66 async fn check(&self, obj: &dyn SecuredAction, context: &Cx) -> Result<Decision> {
67 self.authorize(&obj.resource(), obj.permission(), context)
68 .await
69 }
70
71 async fn check_required(&self, obj: &dyn SecuredAction, context: &Cx) -> Result<()> {
72 match self.check(obj, context).await? {
73 Decision::Allow => Ok(()),
74 Decision::Deny => Err(Error::NotAllowed),
75 }
76 }
77
78 async fn authorize(
83 &self,
84 resource: &ResourceIdent,
85 permission: &Permission,
86 context: &Cx,
87 ) -> Result<Decision>;
88
89 async fn authorize_many(
90 &self,
91 resources: &[ResourceIdent],
92 permission: &Permission,
93 context: &Cx,
94 ) -> Result<Vec<Decision>> {
95 let mut decisions = Vec::with_capacity(resources.len());
96 for resource in resources {
97 decisions.push(self.authorize(resource, permission, context).await?);
98 }
99 Ok(decisions)
100 }
101
102 async fn authorize_checked(
104 &self,
105 resource: &ResourceIdent,
106 permission: &Permission,
107 context: &Cx,
108 ) -> Result<()> {
109 match self.authorize(resource, permission, context).await? {
110 Decision::Allow => Ok(()),
111 Decision::Deny => Err(Error::NotAllowed),
112 }
113 }
114}
115
116pub trait ProvidesPolicy<Cx: Send + Sync + 'static>: Send + Sync + 'static {
117 fn policy(&self) -> &Arc<dyn Policy<Cx>>;
118}
119
120#[async_trait::async_trait]
121impl<T: Policy<Cx>, Cx: Send + Sync + 'static> Policy<Cx> for Arc<T> {
122 async fn authorize(
123 &self,
124 resource: &ResourceIdent,
125 permission: &Permission,
126 context: &Cx,
127 ) -> Result<Decision> {
128 T::authorize(self, resource, permission, context).await
129 }
130
131 async fn authorize_many(
132 &self,
133 resources: &[ResourceIdent],
134 permission: &Permission,
135 context: &Cx,
136 ) -> Result<Vec<Decision>> {
137 T::authorize_many(self, resources, permission, context).await
138 }
139}
140
141pub async fn process_resources<
144 T: Policy<Cx> + Sized,
145 Cx: Send + Sync + 'static,
146 R: ResourceExt + Send,
147>(
148 handler: &T,
149 context: &Cx,
150 permission: &Permission,
151 resources: &mut Vec<R>,
152) -> Result<()> {
153 filter_authorized(handler, context, permission, resources).await
154}
155
156pub async fn filter_authorized<Cx: Send + Sync + 'static, R: ResourceExt + Send>(
163 policy: &dyn Policy<Cx>,
164 context: &Cx,
165 permission: &Permission,
166 resources: &mut Vec<R>,
167) -> Result<()> {
168 let res = resources.iter().map(|r| r.into()).collect::<Vec<_>>();
169 let decisions = policy.authorize_many(&res, permission, context).await?;
170 let mut allow = decisions.into_iter().map(|d| d == Decision::Allow);
172 resources.retain(|_| allow.next().unwrap_or(false));
173 Ok(())
174}
175
176#[cfg(test)]
177mod test {
178 use super::*;
179 use unitycatalog_common::models::{ResourceName, ResourceRef, resource_name};
180
181 #[derive(Debug, Clone, PartialEq)]
183 struct TestShare(&'static str);
184
185 impl ResourceExt for TestShare {
186 fn resource_name(&self) -> ResourceName {
187 ResourceName::new([self.0])
188 }
189 fn resource_ref(&self) -> ResourceRef {
190 ResourceRef::Name(self.resource_name())
191 }
192 fn resource_ident(&self) -> ResourceIdent {
193 ResourceIdent::share(self.resource_name())
194 }
195 }
196
197 struct AllowListPolicy {
200 allow: Vec<ResourceIdent>,
201 }
202
203 #[async_trait::async_trait]
204 impl Policy<()> for AllowListPolicy {
205 async fn authorize(
206 &self,
207 resource: &ResourceIdent,
208 _permission: &Permission,
209 _context: &(),
210 ) -> Result<Decision> {
211 Ok(if self.allow.contains(resource) {
212 Decision::Allow
213 } else {
214 Decision::Deny
215 })
216 }
217 }
218
219 #[tokio::test]
224 async fn filter_authorized_pairs_decision_with_correct_resource() {
225 let policy = AllowListPolicy {
226 allow: vec![
227 ResourceIdent::share(resource_name!("a")),
228 ResourceIdent::share(resource_name!("c")),
229 ],
230 };
231
232 let mut resources = vec![
235 TestShare("a"),
236 TestShare("b"),
237 TestShare("c"),
238 TestShare("d"),
239 ];
240
241 filter_authorized(&policy, &(), &Permission::Read, &mut resources)
242 .await
243 .unwrap();
244
245 assert_eq!(resources, vec![TestShare("a"), TestShare("c")]);
246 }
247}