From f9408344464285870409c2209a8edc8d25fd9bfa Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Fri, 28 Dec 2018 14:52:38 -0800 Subject: start refactor of auth code Pulls auth code (which requires the persistent state of a signing keyring) into a struct. Doesn't try verify macaroon in middleware, do it in individual wrappers. --- rust/src/auth.rs | 293 +++++++++++++++++++++---------------------------------- 1 file changed, 110 insertions(+), 183 deletions(-) (limited to 'rust/src/auth.rs') diff --git a/rust/src/auth.rs b/rust/src/auth.rs index 580cde28..c188233a 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -1,208 +1,151 @@ //! Editor bearer token authentication -use swagger::auth::{AuthData, Authorization, Scopes}; +use swagger::auth::AuthData; use macaroon::{Format, Macaroon, Verifier}; use data_encoding::BASE64; -use std::collections::BTreeSet; -use std::fmt; +use std::collections::{BTreeSet,HashMap}; use database_models::*; use database_schema::*; use api_helpers::*; use chrono::prelude::*; use diesel; -use iron; use diesel::prelude::*; use errors::*; -//use serde_json; use std::str::FromStr; -//use uuid::Uuid; // 32 bytes max (!) static DUMMY_KEY: &[u8] = b"dummy-key-a-one-two-three-a-la"; -#[derive(Debug)] -pub struct OpenAuthMiddleware; - -#[derive(Debug)] -pub struct AuthError { - msg: String, +#[derive(Clone)] +pub struct AuthContext { + pub editor_id: FatCatId, + editor_row: EditorRow, + roles: Vec, // TODO: BTreeSet } -impl fmt::Display for AuthError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "AuthError: {}", &self.msg) - } -} +impl AuthContext { -impl iron::Error for AuthError { - fn description(&self) -> &str { - &self.msg - } - fn cause(&self) -> Option<&iron::Error> { - None + pub fn has_role(&self, role: &str) -> bool { + self.roles.contains(&role.to_string()) || self.roles.contains(&"admin".to_string()) } } -/* -#[derive(Debug)] -pub struct FatcatAuthBakery { - root_key_store: bool, // hashmap - signing_key: bool, // string name +#[derive(Clone)] +pub struct AuthConfectionary { + pub root_keys: HashMap, } -// impl: -// - new() -// - verify(&str) -> Result -*/ - -fn new_auth_ironerror(m: &str) -> iron::error::IronError { - iron::error::IronError::new( - AuthError { msg: m.to_string() }, - (iron::status::BadRequest, m.to_string()) - ) -} - -impl OpenAuthMiddleware { - /// Create a middleware that authorizes with the configured subject. - pub fn new() -> OpenAuthMiddleware { - OpenAuthMiddleware - } -} -impl iron::middleware::BeforeMiddleware for OpenAuthMiddleware { - fn before(&self, req: &mut iron::Request) -> iron::IronResult<()> { - req.extensions.insert::(Authorization { - subject: "undefined".to_string(), - scopes: Scopes::All, - issuer: None, - }); - Ok(()) +impl AuthConfectionary { + pub fn new() -> AuthConfectionary { + AuthConfectionary { + root_keys: HashMap::new(), + } } -} -#[derive(Debug)] -pub struct MacaroonAuthMiddleware; - -impl MacaroonAuthMiddleware { + pub fn create_token(&self, conn: &DbConn, editor_id: FatCatId, expires: Option>) -> Result { + let _ed: EditorRow = editor::table + .find(&editor_id.to_uuid()) + .get_result(conn)?; + let mut mac = Macaroon::create("fatcat.wiki", DUMMY_KEY, "dummy-key").expect("Macaroon creation"); + mac.add_first_party_caveat(&format!("editor_id = {}", editor_id.to_string())); + // TODO: put created one second in the past to prevent timing synchronization glitches? + let now = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true); + mac.add_first_party_caveat(&format!("created = {}", now)); + if let Some(expires) = expires { + mac.add_first_party_caveat(&format!("expires = {:?}", + &expires.to_rfc3339_opts(SecondsFormat::Secs, true))); + }; + let raw = mac.serialize(Format::V2).expect("macaroon serialization"); + Ok(BASE64.encode(&raw)) + } - pub fn new() -> MacaroonAuthMiddleware { - macaroon::initialize().unwrap(); - MacaroonAuthMiddleware + /// On success, returns Some((editor_id, scopes)), where `scopes` is a vector of strings. + pub fn parse_macaroon_token(&self, conn: &DbConn, s: &str) -> Result { + let raw = BASE64.decode(s.as_bytes())?; + let mac = match Macaroon::deserialize(&raw) { + Ok(m) => m, + Err(e) => bail!("macaroon deserialize error: {:?}", e), + }; + let mac = match mac.validate() { + Ok(m) => m, + Err(e) => bail!("macaroon validate error: {:?}", e), + }; + let mut verifier = Verifier::new(); + let mut editor_id: Option = None; + for caveat in mac.first_party_caveats() { + if caveat.predicate().starts_with("editor_id = ") { + editor_id = Some(FatCatId::from_str(caveat.predicate().get(12..).unwrap())?); + break + } + } + let editor_id = editor_id.expect("expected an editor_id caveat"); + verifier.satisfy_exact(&format!("editor_id = {}", editor_id.to_string())); + let mut created: Option> = None; + for caveat in mac.first_party_caveats() { + if caveat.predicate().starts_with("created = ") { + created = Some(DateTime::parse_from_rfc3339(caveat.predicate().get(10..).unwrap()) + .unwrap() + .with_timezone(&Utc)); + break + } + } + let created = created.expect("expected a 'created' caveat"); + verifier.satisfy_exact(&format!("created = {}", created.to_rfc3339_opts(SecondsFormat::Secs, true))); + let editor: EditorRow = editor::table + .find(&editor_id.to_uuid()) + .get_result(conn)?; + let auth_epoch = DateTime::::from_utc(editor.auth_epoch, Utc); + if created < auth_epoch { + bail!("token created before current auth_epoch (was probably revoked by editor)") + } + verifier.satisfy_general(|p: &str| -> bool { + // not expired (based on expires) + if p.starts_with("expires = ") { + let expires: DateTime = DateTime::parse_from_rfc3339(p.get(12..).unwrap()) + .unwrap() + .with_timezone(&Utc); + expires < Utc::now() + } else { + false + } + }); + if !mac.verify_signature(DUMMY_KEY) { + bail!("token signature verification failed"); + }; + match mac.verify(DUMMY_KEY, &mut verifier) { + Ok(true) => (), + Ok(false) => bail!("token overall verification failed"), + Err(e) => bail!("token parsing failed: {:?}", e), + } + Ok(editor) } -} -impl iron::middleware::BeforeMiddleware for MacaroonAuthMiddleware { - fn before(&self, req: &mut iron::Request) -> iron::IronResult<()> { + pub fn parse_swagger(&self, conn: &DbConn, auth_data: &Option) -> Result> { - let res: Option<(String, Vec)> = match req.extensions.get::() { + let token: Option = match auth_data { Some(AuthData::ApiKey(header)) => { let header: Vec = header.split_whitespace().map(|s| s.to_string()).collect(); if !(header.len() == 2 && header[0] == "Bearer") { - return Err(new_auth_ironerror("invalid bearer auth HTTP Header")); + bail!("invalid Bearer Auth HTTP header"); } - sniff_macaroon_token(&header[1]).expect("valid macaroon") + Some(header[1].clone()) }, None => None, - _ => { - return Err(new_auth_ironerror("auth HTTP Header should be empty or API key")); - } + _ => bail!("Authentication HTTP Header should either be empty or a Beaerer API key"), }; - if let Some((editor_id, scopes)) = res { - let mut scope_set = BTreeSet::new(); - for s in scopes { - scope_set.insert(s); - } - req.extensions.insert::(Authorization { - subject: editor_id, - scopes: Scopes::Some(scope_set), - issuer: None, - }); + let token = match token { + Some(t) => t, + None => return Ok(None), }; - Ok(()) - } -} - -/// Just checks signature and expired time; can't hit database, so nothing else -pub fn sniff_macaroon_token(s: &str) -> Result)>> { - let raw = BASE64.decode(s.as_bytes())?; - let mac = match Macaroon::deserialize(&raw) { - Ok(m) => m, - Err(e) => bail!("macaroon deserialize error: {:?}", e), - }; - let mac = match mac.validate() { - Ok(m) => m, - Err(e) => bail!("macaroon validate error: {:?}", e), - }; - let mut editor_id: Option = None; - for caveat in mac.first_party_caveats() { - if caveat.predicate().starts_with("editor_id = ") { - editor_id = Some(FatCatId::from_str(caveat.predicate().get(12..).unwrap())?); - break - } - } - let editor_id = editor_id.expect("expected an editor_id caveat"); - Ok(Some((editor_id.to_string(), vec![]))) -} - -/// On success, returns Some((editor_id, scopes)), where `scopes` is a vector of strings. -pub fn parse_macaroon_token(conn: &DbConn, s: &str) -> Result)>> { - let raw = BASE64.decode(s.as_bytes())?; - let mac = match Macaroon::deserialize(&raw) { - Ok(m) => m, - Err(e) => bail!("macaroon deserialize error: {:?}", e), - }; - let mac = match mac.validate() { - Ok(m) => m, - Err(e) => bail!("macaroon validate error: {:?}", e), - }; - let mut verifier = Verifier::new(); - let mut editor_id: Option = None; - for caveat in mac.first_party_caveats() { - if caveat.predicate().starts_with("editor_id = ") { - editor_id = Some(FatCatId::from_str(caveat.predicate().get(12..).unwrap())?); - break - } - } - let editor_id = editor_id.expect("expected an editor_id caveat"); - verifier.satisfy_exact(&format!("editor_id = {}", editor_id.to_string())); - let mut created: Option> = None; - for caveat in mac.first_party_caveats() { - if caveat.predicate().starts_with("created = ") { - created = Some(DateTime::parse_from_rfc3339(caveat.predicate().get(10..).unwrap()) - .unwrap() - .with_timezone(&Utc)); - break - } - } - let created = created.expect("expected a 'created' caveat"); - verifier.satisfy_exact(&format!("created = {}", created.to_rfc3339_opts(SecondsFormat::Secs, true))); - let editor: EditorRow = editor::table - .find(&editor_id.to_uuid()) - .get_result(conn)?; - let auth_epoch = DateTime::::from_utc(editor.auth_epoch, Utc); - if created < auth_epoch { - bail!("token created before current auth_epoch (was probably revoked by editor)") + let editor_row = self.parse_macaroon_token(conn, &token)?; + let roles = if editor_row.is_admin { vec!["admin".to_string()] } else { vec![] }; + Ok(Some(AuthContext { + editor_id: FatCatId::from_uuid(&editor_row.id), + editor_row: editor_row, + roles: roles, + })) } - verifier.satisfy_general(|p: &str| -> bool { - // not expired (based on expires) - if p.starts_with("expires = ") { - let expires: DateTime = DateTime::parse_from_rfc3339(p.get(12..).unwrap()) - .unwrap() - .with_timezone(&Utc); - expires < Utc::now() - } else { - false - } - }); - if !mac.verify_signature(DUMMY_KEY) { - bail!("token signature verification failed"); - }; - match mac.verify(DUMMY_KEY, &mut verifier) { - Ok(true) => (), - Ok(false) => bail!("token overall verification failed"), - Err(e) => bail!("token parsing failed: {:?}", e), - } - Ok(Some((editor_id.to_string(), vec![]))) } pub fn print_editors(conn: &DbConn) -> Result<()>{ @@ -236,23 +179,6 @@ pub fn create_editor(conn: &DbConn, username: String, is_admin: bool, is_bot: bo Ok(ed) } -pub fn create_token(conn: &DbConn, editor_id: FatCatId, expires: Option>) -> Result { - let _ed: EditorRow = editor::table - .find(&editor_id.to_uuid()) - .get_result(conn)?; - let mut mac = Macaroon::create("fatcat.wiki", DUMMY_KEY, "dummy-key").expect("Macaroon creation"); - mac.add_first_party_caveat(&format!("editor_id = {}", editor_id.to_string())); - // TODO: put created one second in the past to prevent timing synchronization glitches? - let now = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true); - mac.add_first_party_caveat(&format!("created = {}", now)); - if let Some(expires) = expires { - mac.add_first_party_caveat(&format!("expires = {:?}", - &expires.to_rfc3339_opts(SecondsFormat::Secs, true))); - }; - let raw = mac.serialize(Format::V2).expect("macaroon serialization"); - Ok(BASE64.encode(&raw)) -} - pub fn inspect_token(conn: &DbConn, token: &str) -> Result<()> { let raw = BASE64.decode(token.as_bytes())?; let mac = match Macaroon::deserialize(&raw) { @@ -266,8 +192,9 @@ pub fn inspect_token(conn: &DbConn, token: &str) -> Result<()> { for caveat in mac.first_party_caveats() { println!("caveat: {}", caveat.predicate()); } + let ac = AuthConfectionary::new(); // TODO: don't display full stacktrace on failure - println!("verify: {:?}", parse_macaroon_token(conn, token)); + println!("verify: {:?}", ac.parse_macaroon_token(conn, token)); Ok(()) } -- cgit v1.2.3