//! Editor bearer token authentication use swagger::auth::AuthData; use macaroons::v1::V1Token; use macaroons::verifier::*; use macaroons::caveat::Caveat; use macaroons::token::Token; 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 token = V1Token::new(&self.key, self.identifier.as_bytes().to_vec(), Some(self.location.as_bytes().to_vec())); token.add_caveat( &Caveat::first_party(Vec::from(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); token.add_caveat( &Caveat::first_party(Vec::from(format!("created = {}", now)))); if let Some(expires) = expires { token.add_caveat( &Caveat::first_party(Vec::from(format!("expires = {:?}", &expires.to_rfc3339_opts(SecondsFormat::Secs, true))))); }; let raw = token.serialize().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 V1Token::deserialize(raw) { Ok(m) => m, Err(e) => bail!("macaroon deserialize error: {:?}", e), }; let mut editor_id: Option = None; for caveat in mac.caveats.iter() { let caveat = String::from_utf8_lossy(&caveat.caveat_id); if caveat.starts_with("editor_id = ") { editor_id = Some(FatCatId::from_str(caveat.get(12..).unwrap())?); break } } let editor_id = editor_id.expect("expected an editor_id caveat"); let mut created: Option> = None; for caveat in mac.caveats.iter() { let caveat = String::from_utf8_lossy(&caveat.caveat_id); if caveat.starts_with("created = ") { created = Some(DateTime::parse_from_rfc3339(caveat.get(10..).unwrap()) .unwrap() .with_timezone(&Utc)); break } } let created = created.expect("expected a 'created' caveat"); 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)") } /* XXX: haven't implemented verification verifier.satisfy_exact(&format!("editor_id = {}", editor_id.to_string())); verifier.satisfy_exact(&format!("created = {}", created.to_rfc3339_opts(SecondsFormat::Secs, true))); 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 identifier = String::from_utf8_lossy(&mac.identifier).to_string(); let verify_key = match self.root_keys.get(&identifier) { Some(key) => key, None => bail!("key not found for identifier: {}", identifier), }; match mac.verify(verify_key, &mut verifier) { Ok(()) => (), 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, raw_token: &str) -> Result<()> { let raw = BASE64.decode(raw_token.as_bytes())?; let token = match V1Token::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): {:?}", token.location); println!("signing key name (identifier): {}", String::from_utf8_lossy(&token.identifier)); for caveat in token.caveats.iter() { println!("caveat: {}", String::from_utf8_lossy(&caveat.caveat_id)); } println!("verify: {:?}", self.parse_macaroon_token(conn, raw_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(()) }