diff options
Diffstat (limited to 'rust')
| -rw-r--r-- | rust/src/api_server.rs | 11 | ||||
| -rw-r--r-- | rust/src/auth.rs | 197 | ||||
| -rw-r--r-- | rust/src/bin/fatcat-auth.rs | 36 | ||||
| -rw-r--r-- | rust/src/lib.rs | 3 | 
4 files changed, 200 insertions, 47 deletions
| diff --git a/rust/src/api_server.rs b/rust/src/api_server.rs index d264afbc..af2a7e24 100644 --- a/rust/src/api_server.rs +++ b/rust/src/api_server.rs @@ -38,17 +38,6 @@ macro_rules! entity_batch_handler {      }  } -macro_rules! count_entity { -    ($table:ident, $conn:expr) => {{ -        let count: i64 = $table::table -            .filter($table::is_live.eq(true)) -            .filter($table::redirect_id.is_null()) -            .count() -            .first($conn)?; -        count -    }}; -} -  #[derive(Clone)]  pub struct Server {      pub db_pool: ConnectionPool, diff --git a/rust/src/auth.rs b/rust/src/auth.rs index 6ded1188..580cde28 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -1,24 +1,68 @@  //! Editor bearer token authentication  use swagger::auth::{AuthData, Authorization, Scopes}; -//use macaroon::{Macaroon, Verifier}; +use macaroon::{Format, Macaroon, Verifier}; +use data_encoding::BASE64;  use std::collections::BTreeSet; +use std::fmt;  use database_models::*;  use database_schema::*;  use api_helpers::*; -use chrono; +use chrono::prelude::*;  use diesel;  use iron;  use diesel::prelude::*;  use errors::*; -use serde_json; +//use serde_json;  use std::str::FromStr; -use uuid::Uuid; +//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, +} + +impl fmt::Display for AuthError { +    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { +        write!(f, "AuthError: {}", &self.msg) +    } +} + +impl iron::Error for AuthError { +    fn description(&self) -> &str { +        &self.msg +    } +    fn cause(&self) -> Option<&iron::Error> { +        None +    } +} + +/* +#[derive(Debug)] +pub struct FatcatAuthBakery { +    root_key_store: bool, // hashmap +    signing_key: bool, // string name +} + +// impl: +//  - new() +//  - verify(&str) -> Result<AuthContext> +*/ + +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 { @@ -42,6 +86,7 @@ pub struct MacaroonAuthMiddleware;  impl MacaroonAuthMiddleware {      pub fn new() -> MacaroonAuthMiddleware { +        macaroon::initialize().unwrap();          MacaroonAuthMiddleware      }  } @@ -52,13 +97,15 @@ impl iron::middleware::BeforeMiddleware for MacaroonAuthMiddleware {          let res: Option<(String, Vec<String>)> = match req.extensions.get::<AuthData>() {              Some(AuthData::ApiKey(header)) => {                  let header: Vec<String> = header.split_whitespace().map(|s| s.to_string()).collect(); -                // TODO: error types -                assert!(header.len() == 2); -                assert!(header[0] == "Bearer"); -                parse_macaroon_token(&header[1]).expect("valid macaroon") +                if !(header.len() == 2 && header[0] == "Bearer") { +                    return Err(new_auth_ironerror("invalid bearer auth HTTP Header")); +                } +                sniff_macaroon_token(&header[1]).expect("valid macaroon")              },              None => None, -            _ => panic!("valid auth header, or none") +            _ => { +                return Err(new_auth_ironerror("auth HTTP Header should be empty or API key")); +            }          };          if let Some((editor_id, scopes)) = res {              let mut scope_set = BTreeSet::new(); @@ -75,10 +122,87 @@ impl iron::middleware::BeforeMiddleware for MacaroonAuthMiddleware {      }  } -// DUMMY: parse macaroon +/// Just checks signature and expired time; can't hit database, so nothing else +pub fn sniff_macaroon_token(s: &str) -> Result<Option<(String,Vec<String>)>> { +    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<FatCatId> = 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(s: &str) -> Result<Option<(String,Vec<String>)>> { -    Ok(Some(("some_editor_id".to_string(), vec![]))) +pub fn parse_macaroon_token(conn: &DbConn, s: &str) -> Result<Option<(String,Vec<String>)>> { +    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<FatCatId> = 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<DateTime<Utc>> = 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::<Utc>::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<Utc> = 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<()>{ @@ -99,6 +223,8 @@ pub fn print_editors(conn: &DbConn) -> Result<()>{      Ok(())  } +// TODO: move to api_helpers or some such +// TODO: verify username  pub fn create_editor(conn: &DbConn, username: String, is_admin: bool, is_bot: bool) -> Result<EditorRow> {      let ed: EditorRow = diesel::insert_into(editor::table)          .values(( @@ -110,18 +236,51 @@ 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<chrono::NaiveDateTime>) -> Result<String> { -    unimplemented!(); +pub fn create_token(conn: &DbConn, editor_id: FatCatId, expires: Option<DateTime<Utc>>) -> Result<String> { +    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(token: &str) -> Result<()> { -    unimplemented!(); +pub fn inspect_token(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()); +    } +    // TODO: don't display full stacktrace on failure +    println!("verify: {:?}", parse_macaroon_token(conn, token)); +    Ok(())  }  pub fn revoke_tokens(conn: &DbConn, editor_id: FatCatId) -> Result<()>{ -    unimplemented!(); +    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<u64> { -    unimplemented!(); +pub fn revoke_tokens_everyone(conn: &DbConn) -> Result<()> { +    diesel::update(editor::table) +        .set(editor::auth_epoch.eq(Utc::now())) +        .execute(conn)?; +    Ok(())  } diff --git a/rust/src/bin/fatcat-auth.rs b/rust/src/bin/fatcat-auth.rs index a5fedc1f..9b4550d8 100644 --- a/rust/src/bin/fatcat-auth.rs +++ b/rust/src/bin/fatcat-auth.rs @@ -1,13 +1,13 @@  //! JSON Export Helper -#[macro_use] +//#[macro_use]  extern crate clap;  extern crate diesel;  extern crate dotenv;  #[macro_use]  extern crate error_chain;  extern crate fatcat; -#[macro_use] +//#[macro_use]  extern crate log;  extern crate env_logger;  extern crate serde_json; @@ -23,12 +23,12 @@ use fatcat::ConnectionPool;  use fatcat::errors::*;  use fatcat::api_helpers::FatCatId;  use std::str::FromStr; -use uuid::Uuid; +//use uuid::Uuid; -use error_chain::ChainedError; +//use error_chain::ChainedError;  //use std::io::{Stdout,StdoutLock}; -use std::io::prelude::*; -use std::io::{BufReader, BufWriter}; +//use std::io::prelude::*; +//use std::io::{BufReader, BufWriter};  /// Instantiate a new API server with a pooled database connection @@ -72,24 +72,29 @@ fn run() -> Result<()> {          .subcommand(              SubCommand::with_name("inspect-token")                  .about("Dumps token metadata (and whether it is valid)") +                .args_from_usage( +                    "<token> 'base64-encoded token (macaroon)'" +                )          )          .subcommand(              SubCommand::with_name("revoke-tokens")                  .about("Resets auth_epoch for a single editor (invalidating all existing tokens)") +                .args_from_usage( +                    "<editor-id> 'identifier (fcid) of editor'" +                )          )          .subcommand( -            SubCommand::with_name("revoke-tokens-all") +            SubCommand::with_name("revoke-tokens-everyone")                  .about("Resets auth_epoch for all editors (invalidating tokens for all users!)")          )          .get_matches(); +    let db_conn = database_worker_pool()?.get().expect("database pool");      match m.subcommand() {          ("list-editors", Some(_subm)) => { -            let db_conn = database_worker_pool()?.get().expect("database pool");              fatcat::auth::print_editors(&db_conn)?;          },          ("create-editor", Some(subm)) => { -            let db_conn = database_worker_pool()?.get().expect("database pool");              let editor = fatcat::auth::create_editor(                  &db_conn,                  subm.value_of("username").unwrap().to_string(), @@ -99,21 +104,20 @@ fn run() -> Result<()> {              println!("{}", FatCatId::from_uuid(&editor.id).to_string());          },          ("create-token", Some(subm)) => { -            let db_conn = database_worker_pool()?.get().expect("database pool"); -            let editor_id = FatCatId::from_str(subm.value_of("editor").unwrap())?; -            fatcat::auth::create_token(&db_conn, editor_id, None)?; +            let editor_id = FatCatId::from_str(subm.value_of("editor-id").unwrap())?; +            println!("{}", fatcat::auth::create_token(&db_conn, editor_id, None)?);          },          ("inspect-token", Some(subm)) => { -            fatcat::auth::inspect_token(subm.value_of("token").unwrap())?; +            fatcat::auth::inspect_token(&db_conn, subm.value_of("token").unwrap())?;          },          ("revoke-tokens", Some(subm)) => { -            let db_conn = database_worker_pool()?.get().expect("database pool"); -            let editor_id = FatCatId::from_str(subm.value_of("editor").unwrap())?; +            let editor_id = FatCatId::from_str(subm.value_of("editor-id").unwrap())?;              fatcat::auth::revoke_tokens(&db_conn, editor_id)?; +            println!("success!");          },          ("revoke-tokens-everyone", Some(_subm)) => { -            let db_conn = database_worker_pool()?.get().expect("database pool");              fatcat::auth::revoke_tokens_everyone(&db_conn)?; +            println!("success!");          },          _ => {              println!("Missing or unimplemented command!"); diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 2a6c7203..f081be25 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -15,7 +15,6 @@ extern crate swagger;  #[macro_use]  extern crate error_chain;  extern crate iron; -#[macro_use]  extern crate serde_json;  #[macro_use]  extern crate log; @@ -43,6 +42,8 @@ pub mod errors {                          Uuid(::uuid::ParseError);                          Io(::std::io::Error) #[cfg(unix)];                          Serde(::serde_json::Error); +                        Utf8Decode(::std::string::FromUtf8Error); +                        StringDecode(::data_encoding::DecodeError);          }          errors {              InvalidFatcatId(id: String) { | 
