//! Editor bearer token authentication use swagger::auth::AuthData; use macaroon::{Format, Macaroon, Verifier}; use data_encoding::BASE64; use std::collections::HashMap; use database_models::*; use database_schema::*; use api_helpers::*; use chrono::prelude::*; use diesel; use diesel::prelude::*; use errors::*; use std::str::FromStr; // 32 bytes max (!) static DUMMY_KEY: &[u8] = b"dummy-key-a-one-two-three-a-la"; #[derive(Clone)] pub struct AuthContext { pub editor_id: FatCatId, editor_row: EditorRow, roles: Vec, // TODO: BTreeSet } impl AuthContext { pub fn has_role(&self, role: &str) -> bool { self.roles.contains(&role.to_string()) || self.roles.contains(&"admin".to_string()) } } #[derive(Clone)] pub struct AuthConfectionary { pub location: String, pub identifier: String, pub key: Vec, pub root_keys: HashMap>, } impl AuthConfectionary { pub fn new(location: String, identifier: String, key: Vec) -> AuthConfectionary { let mut root_keys = HashMap::new(); root_keys.insert(identifier.clone(), key.clone()); AuthConfectionary { location: location, identifier: identifier, key: key, root_keys: root_keys, } } pub fn new_dummy() -> AuthConfectionary { AuthConfectionary::new( "test.fatcat.wiki".to_string(), "dummy".to_string(), DUMMY_KEY.to_vec()) } pub fn create_token(&self, editor_id: FatCatId, expires: Option>) -> Result { let mut mac = Macaroon::create(&self.location, &self.key, &self.identifier).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)) } /// 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 } }); let verify_key = match self.root_keys.get(mac.identifier()) { Some(key) => key, None => bail!("key not found for identifier: {}", mac.identifier()), }; match mac.verify(verify_key, &mut verifier) { Ok(true) => (), Ok(false) => bail!("token overall verification failed"), Err(e) => bail!("token parsing failed: {:?}", e), } Ok(editor) } pub fn parse_swagger(&self, conn: &DbConn, auth_data: &Option) -> Result> { 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") { bail!("invalid Bearer Auth HTTP header"); } Some(header[1].clone()) }, None => None, _ => bail!("Authentication HTTP Header should either be empty or a Beaerer API key"), }; let token = match token { Some(t) => t, None => return Ok(None), }; 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, })) } // TODO: refactor out of this file? /// Only used from CLI tool pub fn inspect_token(&self, conn: &DbConn, token: &str) -> Result<()> { let raw = BASE64.decode(token.as_bytes())?; let mac = match Macaroon::deserialize(&raw) { Ok(m) => m, Err(e) => bail!("macaroon deserialize error: {:?}", e), }; let now = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true); println!("current time: {}", now); println!("domain (location): {:?}", mac.location()); println!("signing key name (identifier): {}", mac.identifier()); for caveat in mac.first_party_caveats() { println!("caveat: {}", caveat.predicate()); } println!("verify: {:?}", self.parse_macaroon_token(conn, token)); Ok(()) } } pub fn revoke_tokens(conn: &DbConn, editor_id: FatCatId) -> Result<()>{ diesel::update(editor::table.filter(editor::id.eq(&editor_id.to_uuid()))) .set(editor::auth_epoch.eq(Utc::now())) .execute(conn)?; Ok(()) } pub fn revoke_tokens_everyone(conn: &DbConn) -> Result<()> { diesel::update(editor::table) .set(editor::auth_epoch.eq(Utc::now())) .execute(conn)?; Ok(()) } // TODO: refactor out of this file? /// Only used from CLI tool pub fn print_editors(conn: &DbConn) -> Result<()>{ // iterate over all editors. format id, print flags, auth_epoch let all_editors: Vec = editor::table .load(conn)?; println!("editor_id\t\t\tis_admin/is_bot\tauth_epoch\t\t\tusername\twrangler_id"); for e in all_editors { println!("{}\t{}\t{}\t{}\t{}\t{:?}", FatCatId::from_uuid(&e.id).to_string(), e.is_admin, e.is_bot, e.auth_epoch, e.username, e.wrangler_id, ); } Ok(()) }