actix_web_sql_identity/
lib.rs

1//! A database (SQL) Identity Provider
2//!
3//! Provides a way to interact with SQL databases with actix-web's
4//! identity policy.
5//!
6//! # Example
7//!
8//! ```no_run
9//! extern crate actix_web;
10//! extern crate actix_web_sql_identity;
11//!
12//! use actix_web::{http, server, App, HttpRequest, Responder};
13//! use actix_web::middleware::identity::{IdentityService, RequestIdentity};
14//! use actix_web_sql_identity::SqlIdentityBuilder;
15//!
16//! const POOL_SIZE: usize = 3;     // Number of connections per pool
17//!
18//! fn login(mut req: HttpRequest) -> impl Responder {
19//!     // Should pull username/id from request
20//!     req.remember("username_or_id".to_string());
21//!     "Logged in!".to_string()
22//! }
23//!
24//! fn profile(req: HttpRequest) -> impl Responder {
25//!     if let Some(user) = req.identity() {
26//!         format!("Hello, {}!", user)
27//!     } else {
28//!         "Hello, anonymous user!".to_string()
29//!     }
30//! }
31//!
32//! fn logout(mut req: HttpRequest) -> impl Responder {
33//!     req.forget();
34//!    "Logged out!".to_string()
35//! }
36//!
37//! fn main() {
38//!     server::new(|| {
39//!         let policy = SqlIdentityBuilder::new("sqlite://my.db")
40//!             .pool_size(POOL_SIZE);
41//!
42//!         App::new()
43//!            .route("/login", http::Method::POST, login)
44//!            .route("/profile", http::Method::GET, profile)
45//!            .route("/logout", http::Method::POST, logout)
46//!            .middleware(IdentityService::new(
47//!                     policy.finish()
48//!                         .expect("failed to connect to database")))
49//!     })
50//!     .bind("127.0.0.1:7070").unwrap()
51//!     .run();
52//! }
53//! ```
54extern crate actix;
55extern crate actix_web;
56extern crate base64;
57extern crate chrono;
58extern crate failure;
59extern crate futures;
60extern crate rand;
61
62#[macro_use]
63extern crate diesel;
64#[macro_use]
65extern crate failure_derive;
66#[macro_use]
67extern crate log;
68
69mod sql;
70
71use chrono::prelude::Utc;
72use chrono::NaiveDateTime;
73
74use std::rc::Rc;
75
76use failure::Error;
77
78use actix::Addr;
79
80// Actix Web imports
81use actix_web::error::{self, Error as ActixWebError};
82use actix_web::http::header::HeaderValue;
83use actix_web::middleware::identity::{Identity, IdentityPolicy};
84use actix_web::middleware::Response as MiddlewareResponse;
85use actix_web::{HttpMessage, HttpRequest, HttpResponse};
86
87// Futures imports
88use futures::future::{err as FutErr, ok as FutOk};
89use futures::Future;
90
91// (Local) Sql Imports
92use sql::{DeleteIdentity, FindIdentity, SqlActor, SqlIdentityModel, UpdateIdentity, Variant};
93
94// Rand Imports (thread secure!)
95use rand::Rng;
96
97const DEFAULT_RESPONSE_HDR: &'static str = "X-Actix-Auth";
98const DEFAULT_POOL_SIZE: usize = 3;
99
100/// Error representing different failure cases
101#[derive(Debug, Fail)]
102enum SqlIdentityError {
103    #[allow(dead_code)]
104    #[fail(display = "sql variant not supported")]
105    SqlVariantNotSupported,
106
107    #[fail(display = "token not found")]
108    TokenNotFound,
109
110    #[fail(display = "token failed to set in header")]
111    TokenNotSet,
112
113    #[fail(display = "token not provided but required, bad request")]
114    TokenRequired,
115}
116
117enum SqlIdentityState {
118    Created,
119    Updated,
120    Deleted,
121    Unchanged,
122}
123
124/// Identity that uses a SQL database as identity storage
125pub struct SqlIdentity {
126    id: i64,
127    state: SqlIdentityState,
128    identity: Option<String>,
129    token: Option<String>,
130    ip: Option<String>,
131    user_agent: Option<String>,
132    created: NaiveDateTime,
133    inner: Rc<SqlIdentityInner>,
134}
135
136impl Identity for SqlIdentity {
137    /// Returns the current identity, or none
138    fn identity(&self) -> Option<&str> {
139        self.identity.as_ref().map(|s| s.as_ref())
140    }
141
142    /// Remembers a given user (by setting a token value)
143    ///
144    /// # Arguments
145    ///
146    /// * `value` - User to remember
147    fn remember(&mut self, value: String) {
148        self.identity = Some(value);
149
150        // Generate a random token
151        let mut arr = [0u8; 24];
152        rand::thread_rng().fill(&mut arr[..]);
153        self.token = Some(base64::encode(&arr));
154
155        self.state = SqlIdentityState::Created;
156    }
157
158    /// Forgets a user, by deleting the identity
159    fn forget(&mut self) {
160        self.identity = None;
161        self.state = SqlIdentityState::Deleted;
162    }
163
164    /// Saves the identity to the backing store, if it has changed
165    ///
166    /// # Arguments
167    ///
168    /// * `resp` - HTTP response to modify
169    fn write(&mut self, resp: HttpResponse) -> Result<MiddlewareResponse, ActixWebError> {
170        match self.state {
171            SqlIdentityState::Created => {
172                self.state = SqlIdentityState::Unchanged;
173                Ok(MiddlewareResponse::Future(self.inner.create(self, resp)))
174            },
175
176            SqlIdentityState::Updated if self.token.is_some() && self.identity.is_some() => {
177                self.state = SqlIdentityState::Unchanged;
178                Ok(MiddlewareResponse::Future(self.inner.save(self, resp)))
179            }
180
181            SqlIdentityState::Deleted if self.token.is_some() => {
182                let token = self.token.as_ref().expect("[SIS::Deleted] Token is None!");
183                self.state = SqlIdentityState::Unchanged;
184                Ok(MiddlewareResponse::Future(self.inner.remove(token, resp)))
185            }
186
187            SqlIdentityState::Deleted | SqlIdentityState::Updated => {
188                // Not logged in/log in failed
189                Err(error::ErrorBadRequest(SqlIdentityError::TokenRequired))
190            }
191
192            _ => {
193                self.state = SqlIdentityState::Unchanged;
194                Ok(MiddlewareResponse::Done(resp))
195            }
196        }
197    }
198}
199
200/// Wrapped inner-provider for SQL storage
201struct SqlIdentityInner {
202    addr: Addr<SqlActor>,
203    hdr: &'static str,
204}
205
206impl SqlIdentityInner {
207    /// Creates a new instance of a SqlIdentityInner struct
208    ///
209    /// # Arguments
210    ///
211    /// * `addr` - A SQL connection, already opened
212    fn new(addr: Addr<SqlActor>, hdr: &'static str) -> SqlIdentityInner {
213        SqlIdentityInner { addr, hdr }
214    }
215
216    fn create(&self, identity: &SqlIdentity, mut resp: HttpResponse) -> Box<Future<Item = HttpResponse, Error = ActixWebError>> {
217        if let Some(ref token) = identity.token {
218            let headers = resp.headers_mut();
219
220            if let Ok(token) = token.parse() {
221                headers.append(self.hdr, token);
222            } else {
223                error!("Failed to parse token to place in header!");
224                return Box::new(FutErr(error::ErrorInternalServerError(
225                    SqlIdentityError::TokenNotSet,
226                )));
227            }
228        } else {
229            error!("Identity token not set!");
230            return Box::new(FutErr(error::ErrorUnauthorized(
231                SqlIdentityError::TokenNotFound,
232            )));
233        }
234
235        Box::new(
236            self.addr
237                .send(UpdateIdentity::create(identity))
238                .map_err(ActixWebError::from)
239                .and_then(move |res| match res {
240                    Ok(_) => Ok(resp),
241                    Err(e) => {
242                        error!("ERROR: {:?}", e);
243                        Err(error::ErrorInternalServerError(e))
244                    }
245                }),
246        )
247    }
248
249    /// Saves an identity to the backend provider (SQL database)
250    fn save(
251        &self,
252        identity: &SqlIdentity,
253        resp: HttpResponse,
254    ) -> Box<Future<Item = HttpResponse, Error = ActixWebError>> {
255
256        Box::new(
257            self.addr
258                .send(UpdateIdentity::update(identity))
259                .map_err(ActixWebError::from)
260                .and_then(move |res| match res {
261                    Ok(_) => Ok(resp),
262                    Err(e) => {
263                        error!("ERROR: {:?}", e);
264                        Err(error::ErrorInternalServerError(e))
265                    }
266                }),
267        )
268    }
269
270    /// Removes an identity from the backend provider (SQL database)
271    fn remove(
272        &self,
273        token: &str,
274        resp: HttpResponse,
275    ) -> Box<Future<Item = HttpResponse, Error = ActixWebError>> {
276        Box::new(
277            self.addr
278                .send(DeleteIdentity {
279                    token: token.to_string(),
280                })
281                .map_err(ActixWebError::from)
282                .and_then(move |res| match res {
283                    Ok(_) => Ok(resp),
284                    Err(e) => {
285                        error!("ERROR: {:?}", e);
286                        Err(error::ErrorInternalServerError(e))
287                    }
288                }),
289        )
290    }
291
292    /// Loads an identity from the backend provider (SQL database)
293    fn load<S>(
294        &self,
295        req: &HttpRequest<S>,
296    ) -> Box<Future<Item = Option<SqlIdentityModel>, Error = ActixWebError>> {
297        let headers = req.headers();
298        let auth_header = headers.get("Authorization");
299
300        if let Some(auth_header) = auth_header {
301            // Return the identity (or none, if it doesn't exist)
302
303            if let Ok(auth_header) = auth_header.to_str() {
304                let mut iter = auth_header.split(' ');
305                let scheme = iter.next();
306                let token = iter.next();
307
308                if scheme.is_some() && token.is_some() {
309                    let _scheme = scheme.expect("[SII::load] Scheme is None!");
310                    let token = token.expect("[SII::load] Token is None!");
311
312                    return Box::new(
313                        self.addr
314                            .send(FindIdentity {
315                                token: token.to_string(),
316                            })
317                            .map_err(ActixWebError::from)
318                            .and_then(move |res| match res {
319                                Ok(val) => Ok(Some(val)),
320                                Err(e) => {
321                                    warn!("WARN: {:?}", e);
322                                    Ok(None)
323                                }
324                            }),
325                    );
326                }
327            }
328        }
329
330        Box::new(FutOk(None))
331    }
332}
333
334/// Use a SQL database for request identity storage
335#[derive(Clone)]
336pub struct SqlIdentityPolicy(Rc<SqlIdentityInner>);
337
338#[derive(Clone)]
339pub struct SqlIdentityBuilder {
340    pool: usize,
341    uri: String,
342    hdr: &'static str,
343    variant: Variant,
344}
345
346impl SqlIdentityBuilder {
347    /// Creates a new SqlIdentityBuilder that constructs a SqlIdentityPolicy
348    ///
349    /// # Arguments
350    ///
351    /// * `uri` - Database connection string
352    ///
353    /// # Example
354    ///
355    /// ```no_run
356    /// # extern crate actix_web;
357    /// # extern crate actix_web_sql_identity;
358    ///
359    /// use actix_web::App;
360    /// use actix_web::middleware::identity::IdentityService;
361    /// use actix_web_sql_identity::SqlIdentityBuilder;
362    ///
363    /// // Create the identity policy
364    /// let policy = SqlIdentityBuilder::new("postgres://user:pass@host/database")
365    ///                 .pool_size(5)
366    ///                 .response_header("X-MY-RESPONSE")
367    ///                 .finish()
368    ///                 .expect("failed to open database");
369    ///
370    /// let app = App::new().middleware(IdentityService::new(
371    ///     policy
372    /// ));
373    /// ```
374    pub fn new<T: Into<String>>(uri: T) -> SqlIdentityBuilder {
375        let uri: String = uri.into();
376        let variant = SqlIdentityBuilder::variant(&uri);
377
378        SqlIdentityBuilder {
379            pool: DEFAULT_POOL_SIZE,
380            uri: uri,
381            hdr: DEFAULT_RESPONSE_HDR,
382            variant: variant,
383        }
384    }
385
386    fn variant(uri: &str) -> Variant {
387        if uri.starts_with("mysql://") {
388            return Variant::Mysql;
389        } else if uri.starts_with("postgres://") || uri.starts_with("postgresql://") {
390            return Variant::Pg;
391        } else {
392            return Variant::Sqlite;
393        }
394    }
395
396    /// Change the response header when an identity is remembered
397    ///
398    /// # Arguments
399    ///
400    /// * `hdr` - Response header name to use
401    pub fn response_header(mut self, hdr: &'static str) -> SqlIdentityBuilder {
402        self.hdr = hdr;
403        self
404    }
405
406    /// Change how many SQL connections are in each pool
407    ///
408    /// # Arguments
409    ///
410    /// * `count` - Number of connections per pool
411    pub fn pool_size(mut self, count: usize) -> SqlIdentityBuilder {
412        self.pool = count;
413        self
414    }
415
416    /// Finish building this SQL identity policy.  This will attempt
417    /// to construct the a pool of connections to the database
418    /// specified.  The type of database is determined by the uri set.
419    /// On success, a new SqlIdentityPolicy is returned, on failure
420    /// an error is returned.
421    pub fn finish(self) -> Result<SqlIdentityPolicy, Error> {
422        info!("Registering identity provider: {:?}", self.variant);
423
424        Ok(SqlIdentityPolicy(Rc::new(SqlIdentityInner::new(
425            match self.variant {
426                Variant::Sqlite => SqlActor::sqlite(self.pool, &self.uri)?,
427                Variant::Mysql => SqlActor::mysql(self.pool, &self.uri)?,
428                Variant::Pg => SqlActor::pg(self.pool, &self.uri)?,
429            },
430            self.hdr,
431        ))))
432    }
433
434    /// Forces a SQLite identity policy to be created.  This function does
435    /// not normally need to be used, `new` will automatically determine
436    /// the appropriate variant by parsing the connection string.  This function
437    /// exists if the parsing fails
438    pub fn sqlite(mut self) -> SqlIdentityBuilder {
439        self.variant = Variant::Sqlite;
440        self
441    }
442
443    /// Forces a MySQL identity policy to be created.  This function does
444    /// not normally need to be used, `new` will automatically determine
445    /// the appropriate variant by parsing the connection string.  This function
446    /// exists if the parsing fails
447    pub fn mysql(mut self) -> SqlIdentityBuilder {
448        self.variant = Variant::Mysql;
449        self
450    }
451
452    /// Forces a PostgreSQL identity policy to be created.  This function does
453    /// not normally need to be used, `new` will automatically determine
454    /// the appropriate variant by parsing the connection string.  This function
455    /// exists if the parsing fails
456    pub fn postgresql(mut self) -> SqlIdentityBuilder {
457        self.variant = Variant::Pg;
458        self
459    }
460}
461
462impl<S> IdentityPolicy<S> for SqlIdentityPolicy {
463    type Identity = SqlIdentity;
464    type Future = Box<Future<Item = SqlIdentity, Error = ActixWebError>>;
465
466    /// Process a request recieved by the server, returns an Identity struct
467    ///
468    /// # Arguments
469    ///
470    /// * `req` - The HTTP request recieved
471    fn from_request(&self, req: &HttpRequest<S>) -> Self::Future {
472        let inner = Rc::clone(&self.0);
473        let ip = req.connection_info()
474            .remote()
475            .unwrap_or("0.0.0.0")
476            .to_owned();
477        let unk = HeaderValue::from_static("Unknown");
478        let ua = req.headers()
479            .get("user-agent")
480            .unwrap_or(&unk)
481            .to_str()
482            .unwrap_or("Unknown")
483            .to_owned();
484
485        Box::new(self.0.load(req).map(move |ident| {
486            if let Some(id) = ident {
487                let uip = match id.ip {
488                    Some(ref nip) if &ip == nip => nip.clone(),
489                    _ =>  ip,
490                };
491
492                SqlIdentity {
493                    id: id.id,
494                    identity: Some(id.userid),
495                    token: Some(id.token),
496                    ip: Some(uip),
497                    user_agent: Some(ua),
498                    created: id.created,
499                    state: SqlIdentityState::Updated,
500                    inner: inner,
501                }
502            } else {
503                SqlIdentity {
504                    id: -1,
505                    identity: None,
506                    token: None,
507                    ip: Some(ip),
508                    user_agent: Some(ua),
509                    created: Utc::now().naive_utc(),
510                    state: SqlIdentityState::Unchanged,
511                    inner: inner,
512                }
513            }
514        }))
515    }
516}