Skip to main content

unitycatalog_server/policy/
mod.rs

1//! Authorization policies.
2//!
3//! Policies are used to determine whether a recipient is allowed to perform a specific action on a
4//! resource. The action is represented by a [`Permission`] and the resource is represented by a
5//! [`Resource`]. The [`Decision`] represents whether the action is allowed or denied for the given
6//! recipient.
7
8use 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/// Permission that a policy can authorize.
36#[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/// Decision made by a policy.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum Decision {
57    /// Allow the action.
58    Allow,
59    /// Deny the action.
60    Deny,
61}
62
63/// Policy for access control.
64#[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    /// Check if the policy allows the action.
79    ///
80    /// Specifically, this method should return [`Decision::Allow`] if the context
81    /// is granted the requested permission on the resource, and [`Decision::Deny`] otherwise.
82    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    /// Check if the policy allows the action, and return an error if denied.
103    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
141/// Checks if the context has the given permission for each resource,
142/// and retains only those that receive an allow decision.
143pub 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
156/// [`process_resources`] for a `dyn`-typed policy.
157///
158/// Identical filtering behavior, but takes `&dyn Policy<Cx>` so it can be used
159/// with an `Arc<dyn Policy<Cx>>` (which does not satisfy the `Sized` bound on
160/// [`process_resources`]). Handler patterns that hold the policy behind a trait
161/// object — e.g. proxy/decorator handlers — use this.
162pub 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    // `decisions[i]` corresponds to `resources[i]`; pair them in forward order.
171    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    /// Minimal resource carrying a share name, for exercising [`filter_authorized`].
182    #[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    /// Policy that allows only resources whose ident matches one of `allow`,
198    /// and denies everything else — i.e. a non-uniform per-resource decision.
199    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    /// Regression test for the reversed decision mapping bug: a non-uniform
220    /// policy must filter the *correct* resources, in their original order.
221    /// Before the fix, decisions were consumed back-to-front via `pop()`, so
222    /// resource `i` was matched against decision `n-1-i`.
223    #[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        // Asymmetric layout so a reversed mapping yields a different result:
233        // allowed at indices 0 and 2, denied at 1 and 3.
234        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}