use std::borrow::Cow;
use std::collections::HashMap;
use std::future::Future;
#[cfg(feature = "macros")]
pub use auth_macros::permissions;
pub use inventory;
use itertools::Itertools as _;
#[cfg(feature = "macros")]
pub use permission_macros::Permission;
pub use serde;
#[cfg(feature = "utoipa")]
pub use utoipa;
#[derive(Debug, Clone)]
pub struct PermissionEntry {
pub name: Cow<'static, str>,
pub enum_name: &'static str,
pub roles: &'static [&'static str],
}
inventory::collect!(PermissionEntry);
pub fn all_permissions() -> Vec<PermissionEntry> {
let all: Vec<PermissionEntry> = inventory::iter::<PermissionEntry>().cloned().collect();
let wildcards: Vec<PermissionEntry> = all
.iter()
.filter_map(|entry| {
let (prefix, _) = entry.name.split_once('.')?;
Some(PermissionEntry {
name: Cow::Owned(format!("{prefix}.*")),
enum_name: entry.enum_name,
roles: &[],
})
})
.unique_by(|entry| entry.name.clone())
.collect();
all.into_iter().chain(wildcards).collect()
}
pub fn all_permission_names() -> Vec<Cow<'static, str>> {
all_permissions()
.into_iter()
.map(|entry| entry.name)
.sorted()
.dedup()
.collect()
}
#[cfg(feature = "utoipa")]
pub enum PermissionName {}
#[cfg(feature = "utoipa")]
impl utoipa::PartialSchema for PermissionName {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
let names = all_permission_names();
utoipa::openapi::RefOr::T(utoipa::openapi::schema::Schema::Object(
utoipa::openapi::schema::ObjectBuilder::new()
.schema_type(utoipa::openapi::schema::SchemaType::Type(
utoipa::openapi::schema::Type::String,
))
.enum_values(Some(names))
.description(Some("Identifier of permissions."))
.build(),
))
}
}
#[cfg(feature = "utoipa")]
impl utoipa::ToSchema for PermissionName {
fn name() -> Cow<'static, str> {
Cow::Borrowed("PermissionName")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PermissionEffect {
Allow,
Deny,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PermissionGrant {
pub scope: String,
pub pattern: String,
pub effect: PermissionEffect,
}
#[derive(Debug, Clone, Default)]
pub struct EffectivePermissions {
scopes: HashMap<String, Vec<ScopedPermission>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ScopedPermission {
pattern: String,
effect: PermissionEffect,
}
impl EffectivePermissions {
pub fn empty() -> Self {
Self::default()
}
pub fn from_grants(grants: impl IntoIterator<Item = PermissionGrant>) -> Self {
let scopes = grants
.into_iter()
.map(|grant| {
(
grant.scope,
ScopedPermission {
pattern: grant.pattern,
effect: grant.effect,
},
)
})
.into_group_map();
Self { scopes }
}
pub fn allows(&self, permission_key: &str) -> bool {
self.scopes
.values()
.any(|grants| allows_in_scope(grants, permission_key))
}
}
fn allows_in_scope(grants: &[ScopedPermission], permission_key: &str) -> bool {
let denied = grants.iter().any(|permission| {
permission.effect == PermissionEffect::Deny
&& permission_pattern_matches(&permission.pattern, permission_key)
});
if denied {
return false;
}
grants.iter().any(|permission| {
permission.effect == PermissionEffect::Allow
&& permission_pattern_matches(&permission.pattern, permission_key)
})
}
pub fn permission_pattern_matches(pattern: &str, concrete: &str) -> bool {
if pattern == "*" {
return true;
}
let Some((module, action)) = concrete.split_once('.') else {
return false;
};
match pattern.split_once('.') {
None => false,
Some(("*", a)) => action == a,
Some((m, "*")) => module == m,
Some((m, a)) => m == module && a == action,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PermissionCheckError {
Forbidden,
}
pub trait PermissionDenied {
fn permission_denied() -> Self;
}
impl PermissionDenied for PermissionCheckError {
fn permission_denied() -> Self {
Self::Forbidden
}
}
pub struct And<L, R> {
left: L,
right: R,
}
pub struct Or<L, R> {
left: L,
right: R,
}
impl<Context, L, R> HasPermission<Context> for And<L, R>
where
Context: Sync,
L: HasPermission<Context>,
R: HasPermission<Context, Error = L::Error>,
{
type Error = L::Error;
async fn has_permission(&self, context: &Context) -> Result<bool, Self::Error> {
Ok(self.left.has_permission(context).await? && self.right.has_permission(context).await?)
}
}
impl<Context, L, R> HasPermission<Context> for Or<L, R>
where
Context: Sync,
L: HasPermission<Context>,
R: HasPermission<Context, Error = L::Error>,
{
type Error = L::Error;
async fn has_permission(&self, context: &Context) -> Result<bool, Self::Error> {
Ok(self.left.has_permission(context).await? || self.right.has_permission(context).await?)
}
}
pub trait HasPermission<Context>: Send + Sync {
type Error;
fn has_permission(
&self,
context: &Context,
) -> impl Future<Output = Result<bool, Self::Error>> + Send;
fn and<R>(self, right: R) -> And<Self, R>
where
Self: Sized,
R: HasPermission<Context, Error = Self::Error>,
{
And { left: self, right }
}
fn or<R>(self, right: R) -> Or<Self, R>
where
Self: Sized,
R: HasPermission<Context, Error = Self::Error>,
{
Or { left: self, right }
}
}
#[cfg(test)]
mod tests {
use super::{
EffectivePermissions,
PermissionEffect,
PermissionGrant,
permission_pattern_matches,
};
#[test]
fn matches_wildcard_patterns() {
assert!(permission_pattern_matches("*", "Companies.List"));
assert!(permission_pattern_matches("Companies.*", "Companies.List"));
assert!(permission_pattern_matches("*.List", "Companies.List"));
assert!(permission_pattern_matches(
"Companies.List",
"Companies.List"
));
assert!(!permission_pattern_matches("Products.*", "Companies.List"));
}
#[test]
fn deny_wins_inside_scope() {
let permissions = EffectivePermissions::from_grants([
PermissionGrant {
scope: "role".to_owned(),
pattern: "Companies.*".to_owned(),
effect: PermissionEffect::Allow,
},
PermissionGrant {
scope: "role".to_owned(),
pattern: "Companies.Delete".to_owned(),
effect: PermissionEffect::Deny,
},
]);
assert!(permissions.allows("Companies.List"));
assert!(!permissions.allows("Companies.Delete"));
}
}