diff options
Diffstat (limited to 'rust/src')
| -rw-r--r-- | rust/src/api_server.rs | 2 | ||||
| -rw-r--r-- | rust/src/api_wrappers.rs | 5 | ||||
| -rw-r--r-- | rust/src/auth.rs | 293 | ||||
| -rw-r--r-- | rust/src/bin/fatcat-auth.rs | 3 | ||||
| -rw-r--r-- | rust/src/bin/fatcatd.rs | 1 | ||||
| -rw-r--r-- | rust/src/database_models.rs | 2 | ||||
| -rw-r--r-- | rust/src/lib.rs | 3 | 
7 files changed, 121 insertions, 188 deletions
diff --git a/rust/src/api_server.rs b/rust/src/api_server.rs index af2a7e24..cbf5be21 100644 --- a/rust/src/api_server.rs +++ b/rust/src/api_server.rs @@ -2,6 +2,7 @@  use api_entity_crud::EntityCrud;  use api_helpers::*; +use auth::*;  use chrono;  use database_models::*;  use database_schema::*; @@ -41,6 +42,7 @@ macro_rules! entity_batch_handler {  #[derive(Clone)]  pub struct Server {      pub db_pool: ConnectionPool, +    pub auth_confectionary: AuthConfectionary,  }  pub fn get_release_files( diff --git a/rust/src/api_wrappers.rs b/rust/src/api_wrappers.rs index cf696d15..25b4fab1 100644 --- a/rust/src/api_wrappers.rs +++ b/rust/src/api_wrappers.rs @@ -2,6 +2,7 @@  use api_entity_crud::EntityCrud;  use api_helpers::*; +use auth::*;  use api_server::Server;  use database_models::EntityEditRow;  use diesel::Connection; @@ -80,10 +81,12 @@ macro_rules! wrap_entity_handlers {              &self,              entity: models::$model,              editgroup_id: Option<String>, -            _context: &Context, +            context: &Context,          ) -> Box<Future<Item = $post_resp, Error = ApiError> + Send> {              let conn = self.db_pool.get().expect("db_pool error");              let ret = match conn.transaction(|| { +                let auth_context = self.auth_confectionary.parse_swagger(&conn, &context.auth_data)?; +                // XXX: auth_context.expect("not authorized");                  let editgroup_id = if let Some(s) = editgroup_id {                      Some(FatCatId::from_str(&s)?)                  } else { None }; 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<String>, // 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<String, [u8; 32]>,  } -// 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 { -        OpenAuthMiddleware -    } -} -impl iron::middleware::BeforeMiddleware for OpenAuthMiddleware { -    fn before(&self, req: &mut iron::Request) -> iron::IronResult<()> { -        req.extensions.insert::<Authorization>(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<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 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<EditorRow> { +        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(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<AuthData>) -> Result<Option<AuthContext>> { -        let res: Option<(String, Vec<String>)> = match req.extensions.get::<AuthData>() { +        let token: Option<String> = match auth_data {              Some(AuthData::ApiKey(header)) => {                  let header: Vec<String> = 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>(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<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(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)") +        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<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<()>{ @@ -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<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(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(())  } diff --git a/rust/src/bin/fatcat-auth.rs b/rust/src/bin/fatcat-auth.rs index 9b4550d8..f11f7a67 100644 --- a/rust/src/bin/fatcat-auth.rs +++ b/rust/src/bin/fatcat-auth.rs @@ -90,6 +90,7 @@ fn run() -> Result<()> {          .get_matches();      let db_conn = database_worker_pool()?.get().expect("database pool"); +    let confectionary = fatcat::auth::AuthConfectionary::new();      match m.subcommand() {          ("list-editors", Some(_subm)) => {              fatcat::auth::print_editors(&db_conn)?; @@ -105,7 +106,7 @@ fn run() -> Result<()> {          },          ("create-token", Some(subm)) => {              let editor_id = FatCatId::from_str(subm.value_of("editor-id").unwrap())?; -            println!("{}", fatcat::auth::create_token(&db_conn, editor_id, None)?); +            println!("{}", confectionary.create_token(&db_conn, editor_id, None)?);          },          ("inspect-token", Some(subm)) => {              fatcat::auth::inspect_token(&db_conn, subm.value_of("token").unwrap())?; diff --git a/rust/src/bin/fatcatd.rs b/rust/src/bin/fatcatd.rs index e14296da..7def7f66 100644 --- a/rust/src/bin/fatcatd.rs +++ b/rust/src/bin/fatcatd.rs @@ -78,7 +78,6 @@ fn main() {      // authentication      chain.link_before(fatcat_api_spec::server::ExtractAuthData); -    chain.link_before(fatcat::auth::OpenAuthMiddleware::new());      chain.link_after(fatcat::XClacksOverheadMiddleware); diff --git a/rust/src/database_models.rs b/rust/src/database_models.rs index 55ba7fb9..f6cca3e1 100644 --- a/rust/src/database_models.rs +++ b/rust/src/database_models.rs @@ -572,7 +572,7 @@ impl EditgroupRow {      }  } -#[derive(Debug, Queryable, Identifiable, Associations, AsChangeset)] +#[derive(Debug, Clone, Queryable, Identifiable, Associations, AsChangeset)]  #[table_name = "editor"]  pub struct EditorRow {      pub id: Uuid, diff --git a/rust/src/lib.rs b/rust/src/lib.rs index f081be25..43911868 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -117,7 +117,8 @@ pub fn server() -> Result<api_server::Server> {      let pool = diesel::r2d2::Pool::builder()          .build(manager)          .expect("Failed to create database pool."); -    Ok(api_server::Server { db_pool: pool }) +    let confectionary = auth::AuthConfectionary::new(); +    Ok(api_server::Server { db_pool: pool, auth_confectionary: confectionary })  }  pub fn test_server() -> Result<api_server::Server> {  | 
