use std::sync::Arc;
use strum::AsRefStr;
use unitycatalog_common::models::{ResourceExt, ResourceIdent};
pub use self::constant::*;
use crate::api::SecuredAction;
use crate::{Error, Result};
mod constant;
#[derive(Clone, Debug)]
pub enum Principal {
Anonymous,
User(String),
}
impl Principal {
pub fn anonymous() -> Self {
Self::Anonymous
}
pub fn user(name: impl Into<String>) -> Self {
Self::User(name.into())
}
}
#[derive(Debug, Clone, AsRefStr, PartialEq, Eq, strum::EnumString)]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
pub enum Permission {
Read,
Write,
Manage,
Create,
Use,
Browse,
Select,
}
impl From<Permission> for String {
fn from(val: Permission) -> Self {
val.as_ref().to_string()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Decision {
Allow,
Deny,
}
#[async_trait::async_trait]
pub trait Policy<Cx: Send + Sync + 'static>: Send + Sync + 'static {
async fn check(&self, obj: &dyn SecuredAction, context: &Cx) -> Result<Decision> {
self.authorize(&obj.resource(), obj.permission(), context)
.await
}
async fn check_required(&self, obj: &dyn SecuredAction, context: &Cx) -> Result<()> {
match self.check(obj, context).await? {
Decision::Allow => Ok(()),
Decision::Deny => Err(Error::NotAllowed),
}
}
async fn authorize(
&self,
resource: &ResourceIdent,
permission: &Permission,
context: &Cx,
) -> Result<Decision>;
async fn authorize_many(
&self,
resources: &[ResourceIdent],
permission: &Permission,
context: &Cx,
) -> Result<Vec<Decision>> {
let mut decisions = Vec::with_capacity(resources.len());
for resource in resources {
decisions.push(self.authorize(resource, permission, context).await?);
}
Ok(decisions)
}
async fn authorize_checked(
&self,
resource: &ResourceIdent,
permission: &Permission,
context: &Cx,
) -> Result<()> {
match self.authorize(resource, permission, context).await? {
Decision::Allow => Ok(()),
Decision::Deny => Err(Error::NotAllowed),
}
}
}
pub trait ProvidesPolicy<Cx: Send + Sync + 'static>: Send + Sync + 'static {
fn policy(&self) -> &Arc<dyn Policy<Cx>>;
}
#[async_trait::async_trait]
impl<T: Policy<Cx>, Cx: Send + Sync + 'static> Policy<Cx> for Arc<T> {
async fn authorize(
&self,
resource: &ResourceIdent,
permission: &Permission,
context: &Cx,
) -> Result<Decision> {
T::authorize(self, resource, permission, context).await
}
async fn authorize_many(
&self,
resources: &[ResourceIdent],
permission: &Permission,
context: &Cx,
) -> Result<Vec<Decision>> {
T::authorize_many(self, resources, permission, context).await
}
}
pub async fn process_resources<
T: Policy<Cx> + Sized,
Cx: Send + Sync + 'static,
R: ResourceExt + Send,
>(
handler: &T,
context: &Cx,
permission: &Permission,
resources: &mut Vec<R>,
) -> Result<()> {
filter_authorized(handler, context, permission, resources).await
}
pub async fn filter_authorized<Cx: Send + Sync + 'static, R: ResourceExt + Send>(
policy: &dyn Policy<Cx>,
context: &Cx,
permission: &Permission,
resources: &mut Vec<R>,
) -> Result<()> {
let res = resources.iter().map(|r| r.into()).collect::<Vec<_>>();
let decisions = policy.authorize_many(&res, permission, context).await?;
let mut allow = decisions.into_iter().map(|d| d == Decision::Allow);
resources.retain(|_| allow.next().unwrap_or(false));
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
use unitycatalog_common::models::{ResourceName, ResourceRef, resource_name};
#[derive(Debug, Clone, PartialEq)]
struct TestShare(&'static str);
impl ResourceExt for TestShare {
fn resource_name(&self) -> ResourceName {
ResourceName::new([self.0])
}
fn resource_ref(&self) -> ResourceRef {
ResourceRef::Name(self.resource_name())
}
fn resource_ident(&self) -> ResourceIdent {
ResourceIdent::share(self.resource_name())
}
}
struct AllowListPolicy {
allow: Vec<ResourceIdent>,
}
#[async_trait::async_trait]
impl Policy<()> for AllowListPolicy {
async fn authorize(
&self,
resource: &ResourceIdent,
_permission: &Permission,
_context: &(),
) -> Result<Decision> {
Ok(if self.allow.contains(resource) {
Decision::Allow
} else {
Decision::Deny
})
}
}
#[tokio::test]
async fn filter_authorized_pairs_decision_with_correct_resource() {
let policy = AllowListPolicy {
allow: vec![
ResourceIdent::share(resource_name!("a")),
ResourceIdent::share(resource_name!("c")),
],
};
let mut resources = vec![
TestShare("a"),
TestShare("b"),
TestShare("c"),
TestShare("d"),
];
filter_authorized(&policy, &(), &Permission::Read, &mut resources)
.await
.unwrap();
assert_eq!(resources, vec![TestShare("a"), TestShare("c")]);
}
}