use super::AuthClaims;
use std::collections::HashSet;
use std::sync::Arc;
pub(crate) fn default_error_msg(resource_metadata_url: Option<&str>) -> String {
let base = r#"Bearer error="insufficient_scope" error_description="User does not have required role or permission""#;
match resource_metadata_url {
Some(url) => format!(r#"{base}, resource_metadata="{url}""#),
None => base.to_string(),
}
}
pub fn role<C>(name: impl Into<String>) -> Authorizer<C>
where
C: AuthClaims,
{
Authorizer::Role(HashSet::from([name.into()]))
}
pub fn roles<S, I, C>(roles: I) -> Authorizer<C>
where
C: AuthClaims,
S: Into<String>,
I: IntoIterator<Item = S>,
{
Authorizer::Role(roles.into_iter().map(Into::into).collect())
}
pub fn permission<C>(name: impl Into<String>) -> Authorizer<C>
where
C: AuthClaims,
{
Authorizer::Permission(HashSet::from([name.into()]))
}
pub fn permissions<S, I, C>(permissions: I) -> Authorizer<C>
where
C: AuthClaims,
S: Into<String>,
I: IntoIterator<Item = S>,
{
Authorizer::Permission(permissions.into_iter().map(Into::into).collect())
}
pub fn predicate<C, F>(f: F) -> Authorizer<C>
where
C: AuthClaims,
F: Fn(&C) -> bool + Send + Sync + 'static,
{
Authorizer::Predicate(Arc::new(f))
}
pub type ClaimsValidator<C> = dyn Fn(&C) -> bool + Send + Sync + 'static;
#[non_exhaustive]
pub enum Authorizer<C: AuthClaims> {
Role(HashSet<String>),
Permission(HashSet<String>),
Predicate(Arc<ClaimsValidator<C>>),
And(Vec<Authorizer<C>>),
Or(Vec<Authorizer<C>>),
}
impl<C: AuthClaims> std::fmt::Debug for Authorizer<C> {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Authorizer(..)")
}
}
impl<C: AuthClaims> Authorizer<C> {
pub fn validate(&self, claims: &C) -> bool {
match self {
Authorizer::Predicate(pred) => pred(claims),
Authorizer::And(auths) => auths.iter().all(|a| a.validate(claims)),
Authorizer::Or(auths) => auths.iter().any(|a| a.validate(claims)),
Authorizer::Role(roles) => match (claims.role(), claims.roles()) {
(Some(r), None) => roles.contains(r),
(_, Some(rs)) => rs.iter().any(|r| roles.contains(r)),
(None, None) => false,
},
Authorizer::Permission(perms) => claims
.permissions()
.is_some_and(|p| p.iter().any(|perm| perms.contains(perm))),
}
}
pub fn and(self, other: Authorizer<C>) -> Self {
match (self, other) {
(Authorizer::And(mut a), Authorizer::And(mut b)) => {
a.append(&mut b);
Authorizer::And(a)
}
(Authorizer::And(mut a), b) => {
a.push(b);
Authorizer::And(a)
}
(a, Authorizer::And(mut b)) => {
let mut v = vec![a];
v.append(&mut b);
Authorizer::And(v)
}
(a, b) => Authorizer::And(vec![a, b]),
}
}
pub fn or(self, other: Authorizer<C>) -> Self {
match (self, other) {
(Authorizer::Or(mut a), Authorizer::Or(mut b)) => {
a.append(&mut b);
Authorizer::Or(a)
}
(Authorizer::Or(mut a), b) => {
a.push(b);
Authorizer::Or(a)
}
(a, Authorizer::Or(mut b)) => {
let mut v = vec![a];
v.append(&mut b);
Authorizer::Or(v)
}
(a, b) => Authorizer::Or(vec![a, b]),
}
}
}
#[cfg(test)]
mod tests {
use super::{AuthClaims, Authorizer, role, roles};
#[derive(Clone, serde::Deserialize)]
struct Claims {
role: String,
}
impl AuthClaims for Claims {
fn role(&self) -> Option<&str> {
Some(&self.role)
}
}
#[test]
fn it_tests_the_and_flattening() {
let a = role::<Claims>("admin");
let b = role::<Claims>("editor");
let c = role::<Claims>("moderator");
let ab = a.and(b); let abc = ab.and(c);
match abc {
Authorizer::And(inner) => {
assert_eq!(inner.len(), 3);
assert!(
matches!(inner[0], Authorizer::Role(ref s) if s.contains(&"admin".to_owned()))
);
assert!(
matches!(inner[1], Authorizer::Role(ref s) if s.contains(&"editor".to_owned()))
);
assert!(
matches!(inner[2], Authorizer::Role(ref s) if s.contains(&"moderator".to_owned()))
);
}
_ => panic!("Expected And variant"),
}
}
#[test]
fn it_tests_the_or_flattening() {
let a = role("admin");
let b = role("editor");
let c = roles(["viewer"]);
let ab = a.or(b); let abc: Authorizer<Claims> = ab.or(c);
match abc {
Authorizer::Or(inner) => {
assert_eq!(inner.len(), 3);
assert!(
matches!(inner[0], Authorizer::Role(ref s) if s.contains(&"admin".to_owned()))
);
assert!(
matches!(inner[1], Authorizer::Role(ref s) if s.contains(&"editor".to_owned()))
);
assert!(
matches!(inner[2], Authorizer::Role(ref s) if s.contains(&"viewer".to_owned()))
);
}
_ => panic!("Expected Or variant"),
}
}
#[test]
fn it_tests_mixed_and_or_structure() {
let a = role::<Claims>("admin");
let b = role::<Claims>("editor");
let c = roles(["moderator"]);
let or_expr = a.or(b); let combined = or_expr.and(c);
match combined {
Authorizer::And(inner) => {
assert_eq!(inner.len(), 2);
match &inner[0] {
Authorizer::Or(or_inner) => {
assert_eq!(or_inner.len(), 2);
}
_ => panic!("Expected Or inside And[0]"),
}
assert!(
matches!(inner[1], Authorizer::Role(ref s) if s.contains(&"moderator".to_owned()))
);
}
_ => panic!("Expected And variant"),
}
}
}