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
//! An implementation of [`bellhop::auth::Auth`] that authenticates a user based
//! on a header.
//!
//! ## Routes
//!
//! Provides no routes.
//!
//! ## Catchers
//!
//! Provides no catchers.
//!
//! ## Configuration
//!
//! There are two optional configuration options that can be specified in
//! `Rocket.toml`:
//!  - `auth_header`: The name of the header to pull the email address from
//!    (Default: `X-Bellhop-Email`.)
//!  - `auth_header_email_pattern': A regular expression with a named capture
//!    group for the email address (Default: `(?P<email>.*)`)
//!
//! ## Example
//!
//! ```no_run
//! use bellhop::Bellhop;
//! use bellhop_auth_header::Header;
//!
//! fn main() {
//!     Bellhop::default()
//!         .auth(Header)
//!         .start()
//! }
//! ```

#![deny(missing_docs)]

use bellhop::auth::*;
use bellhop::db::Db;
use bellhop::models::user::{CreateUser, User};

use regex::Regex;

use rocket::fairing::AdHoc;
use rocket::request::{Request, State};
use rocket::{Outcome, Rocket};

#[derive(Debug)]
struct AuthRegex {
    header_name: String,
    re: Regex,
}

/// An implementation of [`bellhop::auth::Auth`] that authenticates based on a
/// header.
///
/// See the crate documentation for more details.
#[derive(Debug)]
pub struct Header;

const DEFAULT: &str = "(?P<email>.*)";

impl Header {
    fn register(&self, c: &Db, email: &str) -> Result<User, Error> {
        CreateUser::builder()
            .email(email)
            .build()
            .insert(c)
            .map_err(Error::for_kind(ErrorKind::msg(
                "unable to insert new user from header",
            )))
    }
}

impl Auth for Header {
    fn prelaunch(&self, rocket: Rocket) -> Rocket {
        rocket.attach(AdHoc::on_attach("Auth Header Config", |rocket| {
            let name = rocket
                .config()
                .get_str("auth_header")
                .unwrap_or("X-Bellhop-Email");

            let re_str = rocket
                .config()
                .get_str("auth_header_email_pattern")
                .unwrap_or(DEFAULT);

            let re = match Regex::new(re_str) {
                Ok(x) => x,
                Err(_) => return Err(rocket),
            };

            let state = AuthRegex {
                re,
                header_name: name.to_owned(),
            };

            Ok(rocket.manage(state))
        }))
    }

    fn authenticate(&self, c: &Db, req: &Request) -> Result<Option<User>, Error> {
        let auths = match req.guard::<State<AuthRegex>>() {
            Outcome::Success(x) => x,
            Outcome::Failure(_) => return Err(Error::with_msg("unable to get AuthRegex")),
            Outcome::Forward(_) => return Ok(None),
        };

        let header = match req.headers().get_one(&auths.header_name) {
            None | Some("") => return Ok(None),
            Some(x) => x,
        };

        let mut email = None;

        for capture in auths.re.captures_iter(header) {
            if let Some(x) = capture.name("email") {
                email = Some(x.as_str());
                break;
            }
        }

        let email = match email {
            Some(x) => x,
            None => return Ok(None),
        };

        let user = User::by_email(c, email).map_err(Error::for_kind(ErrorKind::msg(
            "unable to get user for authentication",
        )))?;

        match user {
            None => self.register(c, email).map(Some),
            x => Ok(x),
        }
    }
}