Skip to main content

rskit_authz/
middleware.rs

1//! Tower middleware for request-scoped authorization checks.
2
3use std::{future::Future, pin::Pin, sync::Arc};
4
5use http::{Request as HttpRequest, Response, StatusCode};
6use tower::{Layer, Service};
7
8use crate::{Checker, Request};
9
10/// Builds an authorization request from an HTTP request.
11pub trait RequestAuthorizer<B>: Send + Sync {
12    /// Return an authorization request, or `None` when required context is missing.
13    fn authorization_request(&self, request: &HttpRequest<B>) -> Option<Request>;
14}
15
16impl<B, F> RequestAuthorizer<B> for F
17where
18    F: Fn(&HttpRequest<B>) -> Option<Request> + Send + Sync,
19{
20    fn authorization_request(&self, request: &HttpRequest<B>) -> Option<Request> {
21        self(request)
22    }
23}
24
25/// Tower layer that denies requests unless the checker explicitly allows them.
26#[derive(Clone)]
27pub struct AuthorizationLayer<C, A> {
28    checker: Arc<C>,
29    authorizer: Arc<A>,
30}
31
32impl<C, A> AuthorizationLayer<C, A> {
33    /// Create an authorization layer from a checker and request mapper.
34    #[must_use]
35    pub fn new(checker: C, authorizer: A) -> Self {
36        Self {
37            checker: Arc::new(checker),
38            authorizer: Arc::new(authorizer),
39        }
40    }
41}
42
43impl<S, C, A> Layer<S> for AuthorizationLayer<C, A> {
44    type Service = AuthorizationService<S, C, A>;
45
46    fn layer(&self, inner: S) -> Self::Service {
47        AuthorizationService {
48            inner,
49            checker: Arc::clone(&self.checker),
50            authorizer: Arc::clone(&self.authorizer),
51        }
52    }
53}
54
55/// Tower service produced by [`AuthorizationLayer`].
56#[derive(Clone)]
57pub struct AuthorizationService<S, C, A> {
58    inner: S,
59    checker: Arc<C>,
60    authorizer: Arc<A>,
61}
62
63impl<S, C, A, ReqBody, ResBody> Service<HttpRequest<ReqBody>> for AuthorizationService<S, C, A>
64where
65    S: Service<HttpRequest<ReqBody>, Response = Response<ResBody>> + Clone + Send + 'static,
66    S::Future: Send + 'static,
67    C: Checker + 'static,
68    A: RequestAuthorizer<ReqBody> + 'static,
69    ReqBody: Send + 'static,
70    ResBody: Default + Send + 'static,
71{
72    type Response = S::Response;
73    type Error = S::Error;
74    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
75
76    fn poll_ready(
77        &mut self,
78        cx: &mut std::task::Context<'_>,
79    ) -> std::task::Poll<Result<(), Self::Error>> {
80        self.inner.poll_ready(cx)
81    }
82
83    fn call(&mut self, req: HttpRequest<ReqBody>) -> Self::Future {
84        let clone = self.inner.clone();
85        let mut inner = std::mem::replace(&mut self.inner, clone);
86        let checker = Arc::clone(&self.checker);
87        let authorizer = Arc::clone(&self.authorizer);
88
89        Box::pin(async move {
90            let Some(authz_request) = authorizer.authorization_request(&req) else {
91                return Ok(forbidden_response());
92            };
93            if checker.check(&authz_request) {
94                inner.call(req).await
95            } else {
96                Ok(forbidden_response())
97            }
98        })
99    }
100}
101
102fn forbidden_response<ResBody: Default>() -> Response<ResBody> {
103    let mut response = Response::new(ResBody::default());
104    *response.status_mut() = StatusCode::FORBIDDEN;
105    response
106}
107
108#[cfg(test)]
109mod tests {
110    use std::{collections::HashMap, convert::Infallible, future::Ready};
111
112    use http::{Request as HttpRequest, Response, StatusCode};
113    use tower::{Layer, Service, ServiceExt};
114
115    use super::{AuthorizationLayer, RequestAuthorizer};
116    use crate::{Checker, Decision, Request, Resource, Subject};
117
118    #[derive(Clone)]
119    struct StaticChecker(bool);
120
121    impl Checker for StaticChecker {
122        fn authorize(&self, _request: &Request) -> Decision {
123            Decision {
124                allowed: self.0,
125                reason: "test".into(),
126            }
127        }
128    }
129
130    #[derive(Clone)]
131    struct OkService;
132
133    impl Service<HttpRequest<()>> for OkService {
134        type Response = Response<()>;
135        type Error = Infallible;
136        type Future = Ready<Result<Self::Response, Self::Error>>;
137
138        fn poll_ready(
139            &mut self,
140            _cx: &mut std::task::Context<'_>,
141        ) -> std::task::Poll<Result<(), Self::Error>> {
142            std::task::Poll::Ready(Ok(()))
143        }
144
145        fn call(&mut self, _request: HttpRequest<()>) -> Self::Future {
146            std::future::ready(Ok(Response::builder()
147                .status(StatusCode::OK)
148                .body(())
149                .unwrap()))
150        }
151    }
152
153    fn authz_request() -> Request {
154        Request {
155            subject: Subject {
156                id: "user-1".into(),
157                roles: Vec::new(),
158                attributes: HashMap::new(),
159            },
160            resource: Resource {
161                resource_type: "article".into(),
162                id: "article-1".into(),
163                attributes: HashMap::new(),
164            },
165            action: "read".into(),
166            context: HashMap::new(),
167        }
168    }
169
170    #[derive(Clone)]
171    struct StaticAuthorizer(bool);
172
173    impl RequestAuthorizer<()> for StaticAuthorizer {
174        fn authorization_request(&self, _request: &HttpRequest<()>) -> Option<Request> {
175            self.0.then(authz_request)
176        }
177    }
178
179    #[tokio::test]
180    async fn authorization_layer_allows_only_explicit_allow_decisions() {
181        let mut service =
182            AuthorizationLayer::new(StaticChecker(true), StaticAuthorizer(true)).layer(OkService);
183        let response = service
184            .ready()
185            .await
186            .unwrap()
187            .call(HttpRequest::new(()))
188            .await
189            .unwrap();
190        assert_eq!(response.status(), StatusCode::OK);
191
192        let mut service =
193            AuthorizationLayer::new(StaticChecker(false), StaticAuthorizer(true)).layer(OkService);
194        let response = service
195            .ready()
196            .await
197            .unwrap()
198            .call(HttpRequest::new(()))
199            .await
200            .unwrap();
201        assert_eq!(response.status(), StatusCode::FORBIDDEN);
202    }
203
204    #[tokio::test]
205    async fn authorization_layer_denies_missing_authorization_context() {
206        let mut service =
207            AuthorizationLayer::new(StaticChecker(true), StaticAuthorizer(false)).layer(OkService);
208        let response = service
209            .ready()
210            .await
211            .unwrap()
212            .call(HttpRequest::new(()))
213            .await
214            .unwrap();
215        assert_eq!(response.status(), StatusCode::FORBIDDEN);
216    }
217}