drogue_bazaar/actix/auth/authorization/
mod.rs

1use crate::auth::{AuthError, UserInformation};
2use actix_web::dev::ServiceRequest;
3use async_trait::async_trait;
4use drogue_client::user::v1::authz::Outcome;
5use std::sync::Arc;
6use tracing::instrument;
7
8mod app;
9mod middleware;
10
11pub use app::*;
12
13/// An Authorization middleware for actix-web.
14///
15/// It will ask the authorizer, and if it abstains, rejects the request.
16
17#[derive(Clone)]
18pub struct AuthZ {
19    pub authorizer: Arc<dyn Authorizer>,
20}
21
22impl AuthZ {
23    pub fn new<A>(authorizer: A) -> Self
24    where
25        A: Authorizer + 'static,
26    {
27        Self {
28            authorizer: Arc::new(authorizer),
29        }
30    }
31
32    /// Authorise a request
33    #[instrument(skip_all, err)]
34    pub async fn authorize(&self, context: Context<'_>) -> Result<(), AuthError> {
35        match self.authorizer.authorize(&context).await {
36            Ok(Some(Outcome::Allow)) => Ok(()),
37            Ok(None) | Ok(Some(Outcome::Deny)) => Err(AuthError::Forbidden),
38            Err(err) => Err(err),
39        }
40    }
41}
42
43pub struct Context<'a> {
44    pub request: &'a ServiceRequest,
45    pub identity: &'a UserInformation,
46}
47
48#[async_trait(?Send)]
49pub trait Authorizer {
50    /// Try to authorize an operation.
51    ///
52    /// The outcome can be:
53    /// * `Ok(None)` -> Abstain: continue evaluating.
54    /// * `Ok(Some(outcome))` -> Depending on the outcome, pass or fail right away.
55    /// * `Err(err)` -> Some error, abort right away.
56    ///
57    /// In case all authorizers abstain, the caller has the final call in allowing/rejecting the
58    /// request.
59    ///
60    /// In order to return "not found" instead of "not allowed" it is possible to return
61    /// `Err(AuthError::NotFound(..))`.
62    async fn authorize(&self, context: &Context<'_>) -> Result<Option<Outcome>, AuthError>;
63}
64
65/// Iterate over an array of authorizers and return the first non-abstain result.
66#[async_trait(?Send)]
67impl Authorizer for Vec<Box<dyn Authorizer>> {
68    async fn authorize(&self, context: &Context<'_>) -> Result<Option<Outcome>, AuthError> {
69        for a in self {
70            match a.authorize(context).await? {
71                None => {
72                    // keep going
73                }
74                Some(outcome) => return Ok(Some(outcome)),
75            }
76        }
77
78        // no one voted
79        Ok(None)
80    }
81}
82
83#[async_trait(?Send)]
84impl<A: Authorizer> Authorizer for Option<A> {
85    async fn authorize(&self, context: &Context<'_>) -> Result<Option<Outcome>, AuthError> {
86        match &self {
87            Some(authorizer) => authorizer.authorize(context).await,
88            None => Ok(None),
89        }
90    }
91}
92
93/// An authorizer which returns the provided outcome in case the provided authorizer abstained.
94pub struct OrElseAuthorizer<A>(A, Outcome)
95where
96    A: Authorizer;
97
98#[async_trait(?Send)]
99impl<A> Authorizer for OrElseAuthorizer<A>
100where
101    A: Authorizer,
102{
103    async fn authorize(&self, context: &Context<'_>) -> Result<Option<Outcome>, AuthError> {
104        self.0
105            .authorize(context)
106            .await
107            .map(|r| Some(r.unwrap_or(self.1)))
108    }
109}
110
111pub struct IntoNotFound<A, F>(A, F)
112where
113    A: Authorizer,
114    F: Fn(&Context) -> (String, String);
115
116#[async_trait(?Send)]
117impl<A, F> Authorizer for IntoNotFound<A, F>
118where
119    A: Authorizer,
120    F: Fn(&Context) -> (String, String),
121{
122    async fn authorize(&self, context: &Context<'_>) -> Result<Option<Outcome>, AuthError> {
123        match self.0.authorize(context).await {
124            Ok(None) => Ok(None),
125            Ok(Some(Outcome::Allow)) => Ok(Some(Outcome::Allow)),
126            Ok(Some(Outcome::Deny)) => {
127                let (r#type, id) = self.1(context);
128                Err(AuthError::NotFound(r#type, id))
129            }
130            Err(err) => Err(err),
131        }
132    }
133}
134
135pub trait AuthorizerExt: Authorizer + Sized {
136    fn or_else(self, outcome: Outcome) -> OrElseAuthorizer<Self> {
137        OrElseAuthorizer(self, outcome)
138    }
139
140    fn or_else_allow(self) -> OrElseAuthorizer<Self> {
141        self.or_else(Outcome::Allow)
142    }
143
144    fn or_else_deny(self) -> OrElseAuthorizer<Self> {
145        self.or_else(Outcome::Deny)
146    }
147
148    fn into_not_found<F>(self, f: F) -> IntoNotFound<Self, F>
149    where
150        F: Fn(&Context) -> (String, String),
151    {
152        IntoNotFound(self, f)
153    }
154}
155
156impl<A> AuthorizerExt for A where A: Authorizer {}
157
158/// An authorizer which rejects [`UserInformation::Anonymous`] identities.
159pub struct NotAnonymous;
160
161#[async_trait(?Send)]
162impl Authorizer for NotAnonymous {
163    async fn authorize(&self, context: &Context<'_>) -> Result<Option<Outcome>, AuthError> {
164        Ok(match context.identity {
165            UserInformation::Anonymous => Some(Outcome::Deny),
166            UserInformation::Authenticated(_) => None,
167        })
168    }
169}