quokka-admin 0.1.0

An admin panel for quokka
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
//!
//! Authentication flow
//!
//! 1. Request
//! 2. AdminAuthProvider::authenticate (authenticates a user from the request)
//! 3. AdminAuthProvider::authorize (authorizes the user)
//! 4. Continue with the request
//!
//! The request goes through separete authenticate and authorize handler so that one provider can be used to authenticate the user while
//! another provider can add the authorization. This allows setups where a user comes from one database and permissions are managed
//! somewhere else. Like using LDAP to provide the users and PostgreSQL for assigning groupes & other permissions.
//!
//! That said the the authenticate and authorize requests will **not** be executed after each other for each provider. It will instead call
//! the authenticate method on all the providers (until an [AuthenticatedUser] is successfully returned) and after that all the providers
//! get a chance of authorizing the request against the user. Also here the first provider that allows the request to pass will win.
//!

use std::{collections::HashMap, convert::Infallible, future::Future, pin::Pin, sync::Arc};

use axum::{
    extract::{FromRequestParts, MatchedPath, Request},
    http::request::Parts,
    response::{IntoResponse, Redirect, Response},
    RequestExt,
};
use quokka::{
    handler::html::TemplateDataLoader,
    state::{FromState, ProvideState},
};

use crate::{service::page_loader::AdminPageLoader, state::AdminState};

///
/// Provides the required permission for the requested action
///
/// By default for HTTP the verb will be the request method and the resource will be the URL
///
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct PermissionContext {
    pub verb: String,
    pub resource: String,
}

///
/// Provides the authentication information of a user
///
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct AuthenticatedUser {
    pub name: String,
    pub groups: Vec<String>,
    /// Contains additional context about the user
    pub context: HashMap<String, serde_json::Value>,
}

///
/// The auth provider takes care of authenticating and authorizing a request
///
/// The first provider authorizing the request will be win, so permissions are always addative
///
pub trait AdminAuthProvider<S> {
    type AuthParams: FromRequestParts<S>;

    ///
    /// Authenticate the user by whatever is required from the request to identify the user. If a [AuthenticatedUser] is returned no other
    /// Auth provider will have the chance of authenticating against the request.
    ///
    fn authenticate(
        &self,
        params: Self::AuthParams,
    ) -> impl Future<Output = quokka::Result<Option<AuthenticatedUser>>> + Send;

    ///
    /// Authorize the request to the authenticated user. As soon as a provider returns a "true" no other provider will be asked about it anymore.
    ///
    fn authorize(
        &self,
        user: &AuthenticatedUser,
        permission: &PermissionContext,
    ) -> impl Future<Output = quokka::Result<bool>> + Send;

    ///
    /// Internal function for error reporting + tracing
    ///
    fn provider_name(&self) -> &str {
        std::any::type_name_of_val(self)
    }
}
///
/// A collection of available auth providers
///
#[derive(Clone)]
pub struct AuthProviders<S> {
    pub(crate) providers: Vec<Arc<dyn InnerAuthProvider<S>>>,
}

///
/// A collections of available login providers
///
#[derive(Clone, Default)]
pub struct LoginProviders {
    pub(crate) providers: Vec<Arc<dyn InnerLoginProvider + Send + Sync>>,
}

///
/// The middleware used for handling the admin authentication. Apply this at the end of your custom admin router to include the admin
/// authentication gate.
///
#[derive(Clone)]
pub struct AdminAuthMiddleware<S> {
    state: S,
}

///
/// The submitted login data from a client.
///
/// **Note**: For multi factor authentication you need to take some custom action (for now) or rely on a trusted proxy that does the job of
/// logging the user in.
///
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct LoginData {
    pub login_name: String,
    #[serde(skip_serializing)]
    pub password: String,
}

///
/// Represents the result of a [AdminLoginProvider].
///
/// The user_identifier might be the user's id, the user name or whatever your providers can use to identify the user later. The only
/// requirement is' that it can represented as a string.
///
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct LoginResult {
    pub user_identifier: String,
}

///
/// Provides login methods
///
/// Validates the username & password of a submitted login form and returns a [LoginResult] containing a session key or username that can
/// be validated by an [AdminAuthProvider].
///
pub trait AdminLoginProvider {
    ///
    /// Does the login. The first Login Provider that returns a [Option::Some] is being used.
    ///
    fn do_login(
        &self,
        login_data: &LoginData,
    ) -> impl Future<Output = quokka::Result<Option<LoginResult>>> + Send;

    /// Provides the type name. Used for logging & debugging
    fn type_name(&self) -> &str {
        std::any::type_name_of_val(self)
    }
}

///
/// This is automatically implemented for all T: AdminAuthProvider<S>
///
#[doc(hidden)]
pub trait InnerAuthProvider<S>: Send + Sync {
    fn authenticate<'a>(
        &'a self,
        request: &'a mut Request,
        state: &'a S,
    ) -> Pin<Box<dyn Future<Output = quokka::Result<Option<AuthenticatedUser>>> + Send + 'a>>;

    fn authorize<'a>(
        &'a self,
        user: &'a AuthenticatedUser,
        permission: &'a PermissionContext,
    ) -> Pin<Box<dyn Future<Output = quokka::Result<bool>> + Send + 'a>>;

    fn provider_name(&self) -> &str;
}

///
/// A wrapper to make the [AdminLoginHandler] dyn-compatible.
///
/// This is automatically implemented for all T: [AdminLoginProvider]
///
#[doc(hidden)]
pub trait InnerLoginProvider {
    fn login<'a>(
        &'a self,
        login_data: &'a LoginData,
    ) -> Pin<Box<dyn Future<Output = quokka::Result<Option<LoginResult>>> + Send + 'a>>;

    fn provider_name(&self) -> &str;
}

#[derive(Clone)]
#[doc(hidden)]
pub struct AdminAuthLayer<S, I> {
    state: S,
    inner: I,
    admin: AdminState<S>,
    page_loader: AdminPageLoader,
}

impl<T: AdminLoginProvider> InnerLoginProvider for T {
    fn login<'a>(
        &'a self,
        login_data: &'a LoginData,
    ) -> Pin<Box<dyn Future<Output = quokka::Result<Option<LoginResult>>> + Send + 'a>> {
        Box::pin(self.do_login(login_data))
    }

    fn provider_name(&self) -> &str {
        self.type_name()
    }
}

impl<S, T> InnerAuthProvider<S> for T
where
    S: Send + Sync + 'static,
    T: AdminAuthProvider<S> + Send + Sync,
    T::AuthParams: 'static,
    <T::AuthParams as FromRequestParts<S>>::Rejection: std::fmt::Debug,
{
    fn authenticate<'a>(
        &'a self,
        request: &'a mut Request,
        state: &'a S,
    ) -> Pin<Box<dyn Future<Output = quokka::Result<Option<AuthenticatedUser>>> + Send + 'a>> {
        Box::pin(async move {
            let params = request
                .extract_parts_with_state::<T::AuthParams, S>(state)
                .await
                .inspect_err(|error| tracing::error!(?error, "Unable to extract request params"))
                .map_err(|_| quokka::Error::status("Unable authenticate user", 500))?;

            <T as AdminAuthProvider<S>>::authenticate(self, params).await
        })
    }

    fn authorize<'a>(
        &'a self,
        user: &'a AuthenticatedUser,
        permission: &'a PermissionContext,
    ) -> Pin<Box<dyn Future<Output = quokka::Result<bool>> + Send + 'a>> {
        Box::pin(<T as AdminAuthProvider<S>>::authorize(
            self, user, permission,
        ))
    }
    fn provider_name(&self) -> &str {
        <T as AdminAuthProvider<S>>::provider_name(self)
    }
}

impl<S, I> tower_layer::Layer<I> for AdminAuthMiddleware<S>
where
    S: Send + Sync + Clone,
    S: ProvideState<AdminState<S>>,
    S: ProvideState<AdminPageLoader>,
{
    type Service = AdminAuthLayer<S, I>;

    fn layer(&self, inner: I) -> Self::Service {
        AdminAuthLayer {
            state: self.state.clone(),
            inner,
            admin: self.state.provide(),
            page_loader: self.state.provide(),
        }
    }
}

impl<S, I> tower_service::Service<Request> for AdminAuthLayer<S, I>
where
    I: tower_service::Service<Request, Response = Response, Error = Infallible>
        + Clone
        + Send
        + 'static,
    I::Future: Send,
    S: Send + Sync + Clone + 'static,
    S: ProvideState<AdminState<S>>,
    S: ProvideState<AdminPageLoader>,
{
    type Response = Response;

    type Error = Infallible;

    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;

    fn poll_ready(
        &mut self,
        _: &mut std::task::Context<'_>,
    ) -> std::task::Poll<Result<(), Self::Error>> {
        std::task::Poll::Ready(Ok(()))
    }

    fn call(&mut self, mut request: Request) -> Self::Future {
        let state = self.state.clone();
        let admin = self.admin.clone();
        let page_loader = self.page_loader.clone();
        let mut inner = self.inner.clone();

        Box::pin(async move {
            let mut user: Option<AuthenticatedUser> = None;

            // It's [Infallible]
            let permission: PermissionContext = request.extract_parts().await.unwrap();

            for provider in &admin.auth_providers.providers {
                match provider.authenticate(&mut request, &state).await {
                    Ok(Some(authenticated_user)) => {
                        user = Some(authenticated_user);

                        break;
                    }
                    Err(error) => {
                        tracing::error!(
                            ?error,
                            provider = provider.provider_name(),
                            "Error while authenticating user"
                        )
                    }
                    _ => {}
                }
            }

            let Some(user) = user else {
                return Ok(Redirect::to(&admin.login_url).into_response());
            };

            let span = tracing::info_span!("authenticated user", ?user, ?permission);
            let _ = span.enter();

            if let Some(admin_group) = &admin.super_admin_group {
                if user.groups.contains(admin_group) {
                    tracing::debug!(?user, ?permission, "Granted permission for super_admin");

                    let span = tracing::info_span!("super_admin user", ?user);
                    let _ = span.enter();

                    request.extensions_mut().insert(user);
                    request.extensions_mut().insert(permission);

                    return inner.call(request).await;
                }
            }

            for provider in &admin.auth_providers.providers {
                match provider.authorize(&user, &permission).await {
                    Ok(true) => {
                        tracing::debug!(
                            provider = provider.provider_name(),
                            "Granted permissions for user"
                        );

                        let span = tracing::info_span!("authorized user", ?user);
                        let _ = span.enter();

                        request.extensions_mut().insert(user);
                        request.extensions_mut().insert(permission);

                        return inner.call(request).await;
                    }
                    Err(error) => {
                        tracing::error!(
                            ?error,
                            provider = provider.provider_name(),
                            "Error while checking authorization of user"
                        )
                    }
                    _ => {}
                }
            }

            Ok(<AdminPageLoader as TemplateDataLoader<S>>::render_error(
                &page_loader,
                quokka::Error::status("Forbidden", 403),
            )
            .await
            .into_response())
        })
    }
}

impl<S: Send + Sync> FromRequestParts<S> for PermissionContext {
    type Rejection = Infallible;

    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
        if let Some(permission) = parts.extensions.get::<PermissionContext>() {
            return Ok(permission.clone());
        }

        let uri = MatchedPath::from_request_parts(parts, state).await.unwrap();

        Ok(PermissionContext {
            verb: parts.method.to_string(),
            resource: uri.as_str().to_string(),
        })
    }
}

impl<S> Default for AuthProviders<S> {
    fn default() -> Self {
        Self {
            providers: Default::default(),
        }
    }
}

impl<S: Clone> FromState<S> for AdminAuthMiddleware<S> {
    fn from_state(state: &S) -> Self {
        Self {
            state: state.clone(),
        }
    }
}