diff options
Diffstat (limited to 'rust')
| -rw-r--r-- | rust/src/api_helpers.rs | 17 | ||||
| -rw-r--r-- | rust/src/api_server.rs | 3 | ||||
| -rw-r--r-- | rust/src/api_wrappers.rs | 113 | ||||
| -rw-r--r-- | rust/src/auth.rs | 192 | ||||
| -rw-r--r-- | rust/src/bin/fatcat-auth.rs | 53 | ||||
| -rw-r--r-- | rust/src/bin/fatcatd.rs | 1 | ||||
| -rw-r--r-- | rust/src/lib.rs | 20 | ||||
| -rw-r--r-- | rust/tests/test_auth.rs | 12 | 
8 files changed, 304 insertions, 107 deletions
| diff --git a/rust/src/api_helpers.rs b/rust/src/api_helpers.rs index 3c5a2e17..7478da9d 100644 --- a/rust/src/api_helpers.rs +++ b/rust/src/api_helpers.rs @@ -205,23 +205,23 @@ fn test_hide_flags() {  pub fn make_edit_context(      conn: &DbConn, +    editor_id: FatCatId,      editgroup_id: Option<FatCatId>,      autoaccept: bool,  ) -> Result<EditContext> { -    let editor_id = Uuid::parse_str("00000000-0000-0000-AAAA-000000000001")?; // TODO: auth      let editgroup_id: FatCatId = match (editgroup_id, autoaccept) {          (Some(eg), _) => eg,          // If autoaccept and no editgroup_id passed, always create a new one for this transaction          (None, true) => {              let eg_row: EditgroupRow = diesel::insert_into(editgroup::table) -                .values((editgroup::editor_id.eq(editor_id),)) +                .values((editgroup::editor_id.eq(editor_id.to_uuid()),))                  .get_result(conn)?;              FatCatId::from_uuid(&eg_row.id)          } -        (None, false) => FatCatId::from_uuid(&get_or_create_editgroup(editor_id, conn)?), +        (None, false) => FatCatId::from_uuid(&get_or_create_editgroup(editor_id.to_uuid(), conn)?),      };      Ok(EditContext { -        editor_id: FatCatId::from_uuid(&editor_id), +        editor_id: editor_id,          editgroup_id: editgroup_id,          extra_json: None,          autoaccept: autoaccept, @@ -229,7 +229,12 @@ pub fn make_edit_context(  }  // TODO: verify username (alphanum, etc) -pub fn create_editor(conn: &DbConn, username: String, is_admin: bool, is_bot: bool) -> Result<EditorRow> { +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((              editor::username.eq(username), @@ -237,7 +242,7 @@ pub fn create_editor(conn: &DbConn, username: String, is_admin: bool, is_bot: bo              editor::is_bot.eq(is_bot),          ))          .get_result(conn)?; -    Ok(ed)  +    Ok(ed)  }  /// This function should always be run within a transaction diff --git a/rust/src/api_server.rs b/rust/src/api_server.rs index cbf5be21..853f7bc2 100644 --- a/rust/src/api_server.rs +++ b/rust/src/api_server.rs @@ -20,11 +20,12 @@ macro_rules! entity_batch_handler {              &self,              entity_list: &[models::$model],              autoaccept: bool, +            editor_id: FatCatId,              editgroup_id: Option<FatCatId>,              conn: &DbConn,          ) -> Result<Vec<EntityEdit>> { -            let edit_context = make_edit_context(conn, editgroup_id, autoaccept)?; +            let edit_context = make_edit_context(conn, editor_id, editgroup_id, autoaccept)?;              edit_context.check(&conn)?;              let model_list: Vec<&models::$model> = entity_list.iter().map(|e| e).collect();              let edits = $model::db_create_batch(conn, &edit_context, model_list.as_slice())?; diff --git a/rust/src/api_wrappers.rs b/rust/src/api_wrappers.rs index 25b4fab1..ae070e02 100644 --- a/rust/src/api_wrappers.rs +++ b/rust/src/api_wrappers.rs @@ -2,8 +2,8 @@  use api_entity_crud::EntityCrud;  use api_helpers::*; -use auth::*;  use api_server::Server; +use auth::*;  use database_models::EntityEditRow;  use diesel::Connection;  use errors::*; @@ -85,12 +85,14 @@ macro_rules! wrap_entity_handlers {          ) -> 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 auth_context = self.auth_confectionary.require_auth(&conn, &context.auth_data)?; +                auth_context.require_role(FatcatRole::Editor)?;                  let editgroup_id = if let Some(s) = editgroup_id { -                    Some(FatCatId::from_str(&s)?) +                    let eg_id = FatCatId::from_str(&s)?; +                    auth_context.require_editgroup(&conn, eg_id)?; +                    Some(eg_id)                  } else { None }; -                let edit_context = make_edit_context(&conn, editgroup_id, false)?; +                let edit_context = make_edit_context(&conn, auth_context.editor_id, editgroup_id, false)?;                  edit_context.check(&conn)?;                  entity.db_create(&conn, &edit_context)?.into_model()              }) { @@ -111,6 +113,11 @@ macro_rules! wrap_entity_handlers {                      $post_resp::BadRequest(ErrorResponse { message: e.to_string() }),                  Err(Error(ErrorKind::EditgroupAlreadyAccepted(e), _)) =>                      $post_resp::BadRequest(ErrorResponse { message: e.to_string() }), +                Err(Error(ErrorKind::InvalidCredentials(e), _)) => +                    // TODO: why can't I NotAuthorized here? +                    $post_resp::Forbidden(ErrorResponse { message: e.to_string() }), +                Err(Error(ErrorKind::InsufficientPrivileges(e), _)) => +                    $post_resp::Forbidden(ErrorResponse { message: e.to_string() }),                  Err(Error(ErrorKind::OtherBadRequest(e), _)) =>                      $post_resp::BadRequest(ErrorResponse { message: e.to_string() }),                  Err(e) => { @@ -126,14 +133,18 @@ macro_rules! wrap_entity_handlers {              entity_list: &Vec<models::$model>,              autoaccept: Option<bool>,              editgroup_id: Option<String>, -            _context: &Context, +            context: &Context,          ) -> Box<Future<Item = $post_batch_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.require_auth(&conn, &context.auth_data)?; +                auth_context.require_role(FatcatRole::Editor)?;                  let editgroup_id = if let Some(s) = editgroup_id { -                    Some(FatCatId::from_str(&s)?) +                    let eg_id = FatCatId::from_str(&s)?; +                    auth_context.require_editgroup(&conn, eg_id)?; +                    Some(eg_id)                  } else { None }; -                self.$post_batch_handler(entity_list, autoaccept.unwrap_or(false), editgroup_id, &conn) +                self.$post_batch_handler(entity_list, autoaccept.unwrap_or(false), auth_context.editor_id, editgroup_id, &conn)              }) {                  Ok(edit) =>                      $post_batch_resp::CreatedEntities(edit), @@ -152,6 +163,11 @@ macro_rules! wrap_entity_handlers {                      $post_batch_resp::BadRequest(ErrorResponse { message: e.to_string() }),                  Err(Error(ErrorKind::EditgroupAlreadyAccepted(e), _)) =>                      $post_batch_resp::BadRequest(ErrorResponse { message: e.to_string() }), +                Err(Error(ErrorKind::InvalidCredentials(e), _)) => +                    // TODO: why can't I NotAuthorized here? +                    $post_batch_resp::Forbidden(ErrorResponse { message: e.to_string() }), +                Err(Error(ErrorKind::InsufficientPrivileges(e), _)) => +                    $post_batch_resp::Forbidden(ErrorResponse { message: e.to_string() }),                  Err(Error(ErrorKind::OtherBadRequest(e), _)) =>                      $post_batch_resp::BadRequest(ErrorResponse { message: e.to_string() }),                  Err(e) => { @@ -167,15 +183,19 @@ macro_rules! wrap_entity_handlers {              ident: String,              entity: models::$model,              editgroup_id: Option<String>, -            _context: &Context, +            context: &Context,          ) -> Box<Future<Item = $update_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.require_auth(&conn, &context.auth_data)?; +                auth_context.require_role(FatcatRole::Editor)?;                  let entity_id = FatCatId::from_str(&ident)?;                  let editgroup_id = if let Some(s) = editgroup_id { -                    Some(FatCatId::from_str(&s)?) +                    let eg_id = FatCatId::from_str(&s)?; +                    auth_context.require_editgroup(&conn, eg_id)?; +                    Some(eg_id)                  } else { None }; -                let edit_context = make_edit_context(&conn, editgroup_id, false)?; +                let edit_context = make_edit_context(&conn, auth_context.editor_id, editgroup_id, false)?;                  edit_context.check(&conn)?;                  entity.db_update(&conn, &edit_context, entity_id)?.into_model()              }) { @@ -202,6 +222,11 @@ macro_rules! wrap_entity_handlers {                      $update_resp::BadRequest(ErrorResponse { message: e.to_string() }),                  Err(Error(ErrorKind::OtherBadRequest(e), _)) =>                      $update_resp::BadRequest(ErrorResponse { message: e.to_string() }), +                Err(Error(ErrorKind::InvalidCredentials(e), _)) => +                    // TODO: why can't I NotAuthorized here? +                    $update_resp::Forbidden(ErrorResponse { message: e.to_string() }), +                Err(Error(ErrorKind::InsufficientPrivileges(e), _)) => +                    $update_resp::Forbidden(ErrorResponse { message: e.to_string() }),                  Err(e) => {                      error!("{}", e);                      $update_resp::GenericError(ErrorResponse { message: e.to_string() }) @@ -214,16 +239,22 @@ macro_rules! wrap_entity_handlers {              &self,              ident: String,              editgroup_id: Option<String>, -            _context: &Context, +            context: &Context,          ) -> Box<Future<Item = $delete_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.require_auth(&conn, &context.auth_data)?; +                auth_context.require_role(FatcatRole::Editor)?;                  let entity_id = FatCatId::from_str(&ident)?;                  let editgroup_id: Option<FatCatId> = match editgroup_id { -                    Some(s) => Some(FatCatId::from_str(&s)?), +                    Some(s) => { +                        let editgroup_id = FatCatId::from_str(&s)?; +                        auth_context.require_editgroup(&conn, editgroup_id)?; +                        Some(editgroup_id) +                    },                      None => None,                  }; -                let edit_context = make_edit_context(&conn, editgroup_id, false)?; +                let edit_context = make_edit_context(&conn, auth_context.editor_id, editgroup_id, false)?;                  edit_context.check(&conn)?;                  $model::db_delete(&conn, &edit_context, entity_id)?.into_model()              }) { @@ -246,6 +277,11 @@ macro_rules! wrap_entity_handlers {                      $delete_resp::BadRequest(ErrorResponse { message: e.to_string() }),                  Err(Error(ErrorKind::OtherBadRequest(e), _)) =>                      $delete_resp::BadRequest(ErrorResponse { message: e.to_string() }), +                Err(Error(ErrorKind::InvalidCredentials(e), _)) => +                    // TODO: why can't I NotAuthorized here? +                    $delete_resp::Forbidden(ErrorResponse { message: e.to_string() }), +                Err(Error(ErrorKind::InsufficientPrivileges(e), _)) => +                    $delete_resp::Forbidden(ErrorResponse { message: e.to_string() }),                  Err(e) => {                      error!("{}", e);                      $delete_resp::GenericError(ErrorResponse { message: e.to_string() }) @@ -356,11 +392,15 @@ macro_rules! wrap_entity_handlers {          fn $delete_edit_fn(              &self,              edit_id: String, -            _context: &Context, +            context: &Context,          ) -> Box<Future<Item = $delete_edit_resp, Error = ApiError> + Send> {              let conn = self.db_pool.get().expect("db_pool error");              let ret = match conn.transaction(|| {                  let edit_id = Uuid::from_str(&edit_id)?; +                let auth_context = self.auth_confectionary.require_auth(&conn, &context.auth_data)?; +                auth_context.require_role(FatcatRole::Editor)?; +                let edit = $model::db_get_edit(&conn, edit_id)?; +                auth_context.require_editgroup(&conn, FatCatId::from_uuid(&edit.editgroup_id))?;                  $model::db_delete_edit(&conn, edit_id)              }) {                  Ok(()) => @@ -373,6 +413,11 @@ macro_rules! wrap_entity_handlers {                      $delete_edit_resp::BadRequest(ErrorResponse { message: e.to_string() }),                  Err(Error(ErrorKind::OtherBadRequest(e), _)) =>                      $delete_edit_resp::BadRequest(ErrorResponse { message: e.to_string() }), +                Err(Error(ErrorKind::InvalidCredentials(e), _)) => +                    // TODO: why can't I NotAuthorized here? +                    $delete_edit_resp::Forbidden(ErrorResponse { message: e.to_string() }), +                Err(Error(ErrorKind::InsufficientPrivileges(e), _)) => +                    $delete_edit_resp::Forbidden(ErrorResponse { message: e.to_string() }),                  Err(e) => {                      error!("{}", e);                      $delete_edit_resp::GenericError(ErrorResponse { message: e.to_string() }) @@ -862,11 +907,17 @@ impl Api for Server {      fn accept_editgroup(          &self,          editgroup_id: String, -        _context: &Context, +        context: &Context,      ) -> Box<Future<Item = AcceptEditgroupResponse, Error = ApiError> + Send> {          let conn = self.db_pool.get().expect("db_pool error");          let ret = match conn.transaction(|| {              let editgroup_id = FatCatId::from_str(&editgroup_id)?; +            let auth_context = self +                .auth_confectionary +                .require_auth(&conn, &context.auth_data)?; +            auth_context.require_role(FatcatRole::Admin)?; +            // NOTE: this is currently redundant, but zero-cost +            auth_context.require_editgroup(&conn, editgroup_id)?;              self.accept_editgroup_handler(editgroup_id, &conn)          }) {              Ok(()) => AcceptEditgroupResponse::MergedSuccessfully(Success { @@ -882,6 +933,16 @@ impl Api for Server {                      message: ErrorKind::EditgroupAlreadyAccepted(e).to_string(),                  })              } +            Err(Error(ErrorKind::InvalidCredentials(e), _)) => { +                AcceptEditgroupResponse::Forbidden(ErrorResponse { +                    message: e.to_string(), +                }) +            } +            Err(Error(ErrorKind::InsufficientPrivileges(e), _)) => { +                AcceptEditgroupResponse::Forbidden(ErrorResponse { +                    message: e.to_string(), +                }) +            }              Err(e) => AcceptEditgroupResponse::GenericError(ErrorResponse {                  message: e.to_string(),              }), @@ -919,11 +980,27 @@ impl Api for Server {      fn create_editgroup(          &self,          entity: models::Editgroup, -        _context: &Context, +        context: &Context,      ) -> Box<Future<Item = CreateEditgroupResponse, Error = ApiError> + Send> {          let conn = self.db_pool.get().expect("db_pool error"); -        let ret = match conn.transaction(|| self.create_editgroup_handler(entity, &conn)) { +        let ret = match conn.transaction(|| { +            let auth_context = self +                .auth_confectionary +                .require_auth(&conn, &context.auth_data)?; +            auth_context.require_role(FatcatRole::Editor)?; +            self.create_editgroup_handler(entity, &conn) +        }) {              Ok(eg) => CreateEditgroupResponse::SuccessfullyCreated(eg), +            Err(Error(ErrorKind::InvalidCredentials(e), _)) => { +                CreateEditgroupResponse::Forbidden(ErrorResponse { +                    message: e.to_string(), +                }) +            } +            Err(Error(ErrorKind::InsufficientPrivileges(e), _)) => { +                CreateEditgroupResponse::Forbidden(ErrorResponse { +                    message: e.to_string(), +                }) +            }              Err(e) =>              // TODO: dig in to error type here              { diff --git a/rust/src/auth.rs b/rust/src/auth.rs index 450a19d6..ee3c6fb0 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -1,33 +1,76 @@  //! Editor bearer token authentication -use swagger::auth::AuthData; -use macaroon::{Format, Macaroon, Verifier};  use data_encoding::BASE64; +use macaroon::{Format, Macaroon, Verifier}; +use swagger::auth::AuthData; -use std::collections::HashMap; -use database_models::*; -use database_schema::*;  use api_helpers::*;  use chrono::prelude::*; +use database_models::*; +use database_schema::*;  use diesel;  use diesel::prelude::*;  use errors::*; +use std::collections::HashMap;  use std::str::FromStr;  // 32 bytes max (!)  static DUMMY_KEY: &[u8] = b"dummy-key-a-one-two-three-a-la"; +#[derive(Clone, Copy, Debug)] +pub enum FatcatRole { +    Public, +    Editor, +    Bot, +    Human, +    Admin, +} +  #[derive(Clone)]  pub struct AuthContext {      pub editor_id: FatCatId,      editor_row: EditorRow, -    roles: Vec<String>, // TODO: BTreeSet  }  impl AuthContext { +    pub fn has_role(&self, role: FatcatRole) -> bool { +        if self.editor_row.is_admin { +            return true; +        } +        match role { +            FatcatRole::Public => true, +            FatcatRole::Editor => true, +            FatcatRole::Bot => self.editor_row.is_bot, +            FatcatRole::Human => !self.editor_row.is_bot, +            FatcatRole::Admin => self.editor_row.is_admin, +        } +    } -    pub fn has_role(&self, role: &str) -> bool { -        self.roles.contains(&role.to_string()) || self.roles.contains(&"admin".to_string()) +    pub fn require_role(&self, role: FatcatRole) -> Result<()> { +        match self.has_role(role) { +            true => Ok(()), +            // TODO: better message +            false => Err(ErrorKind::InsufficientPrivileges( +                "doesn't have required role".to_string(), +            ) +            .into()), +        } +    } + +    pub fn require_editgroup(&self, conn: &DbConn, editgroup_id: FatCatId) -> Result<()> { +        if self.has_role(FatcatRole::Admin) { +            return Ok(()) +        } +        let editgroup: EditgroupRow = editgroup::table +            .find(editgroup_id.to_uuid()) +            .get_result(conn)?; +        match editgroup.editor_id == self.editor_id.to_uuid() { +            true => Ok(()), +            false => Err(ErrorKind::InsufficientPrivileges( +                "editor does not own this editgroup".to_string(), +            ) +            .into()), +        }      }  } @@ -40,7 +83,11 @@ pub struct AuthConfectionary {  }  impl AuthConfectionary { -    pub fn new(location: String, identifier: String, key_base64: String) -> Result<AuthConfectionary> { +    pub fn new( +        location: String, +        identifier: String, +        key_base64: String, +    ) -> Result<AuthConfectionary> {          let key = BASE64.decode(key_base64.as_bytes())?;          let mut root_keys = HashMap::new();          root_keys.insert(identifier.clone(), key.clone()); @@ -57,18 +104,26 @@ impl AuthConfectionary {              "test.fatcat.wiki".to_string(),              "dummy".to_string(),              BASE64.encode(DUMMY_KEY), -        ).unwrap() +        ) +        .unwrap()      } -    pub fn create_token(&self, editor_id: FatCatId, expires: Option<DateTime<Utc>>) -> Result<String> { -        let mut mac = Macaroon::create(&self.location, &self.key, &self.identifier).expect("Macaroon creation"); +    pub fn create_token( +        &self, +        editor_id: FatCatId, +        expires: Option<DateTime<Utc>>, +    ) -> Result<String> { +        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))); +            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)) @@ -79,18 +134,30 @@ impl AuthConfectionary {          let raw = BASE64.decode(s.as_bytes())?;          let mac = match Macaroon::deserialize(&raw) {              Ok(m) => m, -            Err(e) => bail!("macaroon deserialize error: {:?}", e), +            Err(_e) => { +                // TODO: should be "chaining" here +                //bail!("macaroon deserialize error: {:?}", e), +                return Err( +                    ErrorKind::InvalidCredentials("macaroon deserialize error".to_string()).into(), +                ); +            }          };          let mac = match mac.validate() {              Ok(m) => m, -            Err(e) => bail!("macaroon validate error: {:?}", e), +            Err(_e) => { +                // TODO: should be "chaining" here +                //bail!("macaroon validate error: {:?}", e), +                return Err( +                    ErrorKind::InvalidCredentials("macaroon validate error".to_string()).into(), +                ); +            }          };          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 +                break;              }          }          let editor_id = editor_id.expect("expected an editor_id caveat"); @@ -98,20 +165,27 @@ impl AuthConfectionary {          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 +                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)?; +        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)") +            return Err(ErrorKind::InvalidCredentials( +                "token created before current auth_epoch (was probably revoked by editor)" +                    .to_string(), +            ) +            .into());          }          verifier.satisfy_general(|p: &str| -> bool {              // not expired (based on expires) @@ -126,42 +200,78 @@ impl AuthConfectionary {          });          let verify_key = match self.root_keys.get(mac.identifier()) {              Some(key) => key, -            None => bail!("key not found for identifier: {}", mac.identifier()), +            None => { +                // TODO: better message +                //bail!("key not found for identifier: {}", mac.identifier()), +                return Err(ErrorKind::InvalidCredentials( +                    "key not found for identifier".to_string(), +                ) +                .into()); +            }          };          match mac.verify(verify_key, &mut verifier) {              Ok(true) => (), -            Ok(false) => bail!("token overall verification failed"), -            Err(e) => bail!("token parsing failed: {:?}", e), +            Ok(false) => { +                return Err(ErrorKind::InvalidCredentials( +                    "token overall verification failed".to_string(), +                ) +                .into()); +            } +            Err(_e) => { +                // TODO: chain +                //bail!("token parsing failed: {:?}", e), +                return Err( +                    ErrorKind::InvalidCredentials("token parsing failed".to_string()).into(), +                ); +            }          }          Ok(editor)      } -    pub fn parse_swagger(&self, conn: &DbConn, auth_data: &Option<AuthData>) -> Result<Option<AuthContext>> { - +    pub fn parse_swagger( +        &self, +        conn: &DbConn, +        auth_data: &Option<AuthData>, +    ) -> Result<Option<AuthContext>> {          let token: Option<String> = match auth_data {              Some(AuthData::ApiKey(header)) => { -                let header: Vec<String> = header.split_whitespace().map(|s| s.to_string()).collect(); +                let header: Vec<String> = +                    header.split_whitespace().map(|s| s.to_string()).collect();                  if !(header.len() == 2 && header[0] == "Bearer") { -                    bail!("invalid Bearer Auth HTTP header"); +                    return Err(ErrorKind::InvalidCredentials( +                        "invalid Bearer Auth HTTP header".to_string(), +                    ) +                    .into());                  }                  Some(header[1].clone()) -            }, +            }              None => None, -            _ => bail!("Authentication HTTP Header should either be empty or a Beaerer API key"), +            _ => { +                return Err(ErrorKind::InvalidCredentials( +                    "Authentication HTTP Header should either be empty or a Beaerer API key" +                        .to_string(), +                ) +                .into()); +            }          };          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,          }))      } +    pub fn require_auth(&self, conn: &DbConn, auth_data: &Option<AuthData>) -> Result<AuthContext> { +        match self.parse_swagger(conn, auth_data)? { +            Some(auth) => Ok(auth), +            None => Err(ErrorKind::InvalidCredentials("no token supplied".to_string()).into()), +        } +    } +      // TODO: refactor out of this file?      /// Only used from CLI tool      pub fn inspect_token(&self, conn: &DbConn, token: &str) -> Result<()> { @@ -206,13 +316,13 @@ pub fn revoke_tokens_everyone(conn: &DbConn) -> Result<()> {  // TODO: refactor out of this file?  /// Only used from CLI tool -pub fn print_editors(conn: &DbConn) -> Result<()>{ +pub fn print_editors(conn: &DbConn) -> Result<()> {      // iterate over all editors. format id, print flags, auth_epoch -    let all_editors: Vec<EditorRow> = editor::table -        .load(conn)?; +    let all_editors: Vec<EditorRow> = 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{:?}", +        println!( +            "{}\t{}\t{}\t{}\t{}\t{:?}",              FatCatId::from_uuid(&e.id).to_string(),              e.is_admin,              e.is_bot, diff --git a/rust/src/bin/fatcat-auth.rs b/rust/src/bin/fatcat-auth.rs index 3240964f..addd2b66 100644 --- a/rust/src/bin/fatcat-auth.rs +++ b/rust/src/bin/fatcat-auth.rs @@ -8,16 +8,16 @@ extern crate dotenv;  extern crate error_chain;  extern crate fatcat;  //#[macro_use] -extern crate log;  extern crate env_logger; +extern crate log;  extern crate serde_json;  extern crate uuid;  use clap::{App, SubCommand};  use diesel::prelude::*; -use fatcat::errors::*;  use fatcat::api_helpers::FatCatId; +use fatcat::errors::*;  use std::str::FromStr;  //use uuid::Uuid; @@ -26,15 +26,13 @@ use std::str::FromStr;  //use std::io::prelude::*;  //use std::io::{BufReader, BufWriter}; -  fn run() -> Result<()> {      let m = App::new("fatcat-auth")          .version(env!("CARGO_PKG_VERSION"))          .author("Bryan Newbold <bnewbold@archive.org>")          .about("Editor authentication admin tool")          .subcommand( -            SubCommand::with_name("list-editors") -                .about("Prints all currently registered editors") +            SubCommand::with_name("list-editors").about("Prints all currently registered editors"),          )          .subcommand(              SubCommand::with_name("create-editor") @@ -42,41 +40,37 @@ fn run() -> Result<()> {                  .args_from_usage(                      "<username> 'username for editor'                       --admin 'creates editor with admin privs' -                     --bot 'this editor is a bot'" -                ) +                     --bot 'this editor is a bot'", +                ),          )          .subcommand(              SubCommand::with_name("create-token")                  .about("Creates a new auth token (macaroon) for the given editor")                  .args_from_usage(                      "<editor-id> 'id of the editor (fatcatid, not username)' -                     --env-format 'outputs in a format that shells can source'" // TODO -                ) +                     --env-format 'outputs in a format that shells can source'", // TODO +                ),          )          .subcommand(              SubCommand::with_name("inspect-token")                  .about("Dumps token metadata (and whether it is valid)") -                .args_from_usage( -                    "<token> 'base64-encoded token (macaroon)'" -                ) +                .args_from_usage("<token> 'base64-encoded token (macaroon)'"),          )          .subcommand(              SubCommand::with_name("create-key")                  .about("Creates a new auth secret key (aka, root/signing key for tokens)")                  .args_from_usage( -                    "--env-format 'outputs in a format that shells can source'" // TODO -                ) +                    "--env-format 'outputs in a format that shells can source'", // TODO +                ),          )          .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'" -                ) +                .args_from_usage("<editor-id> 'identifier (fcid) of editor'"),          )          .subcommand(              SubCommand::with_name("revoke-tokens-everyone") -                .about("Resets auth_epoch for all editors (invalidating tokens for all users!)") +                .about("Resets auth_epoch for all editors (invalidating tokens for all users!)"),          )          .get_matches(); @@ -84,27 +78,30 @@ fn run() -> Result<()> {      match m.subcommand() {          ("create-key", Some(_subm)) => {              println!("{}", fatcat::auth::create_key()); -            return Ok(()) -        }, +            return Ok(()); +        }          _ => (),      }      // Then the ones that do -    let db_conn = fatcat::database_worker_pool()?.get().expect("database pool"); +    let db_conn = fatcat::database_worker_pool()? +        .get() +        .expect("database pool");      let confectionary = fatcat::env_confectionary()?;      match m.subcommand() {          ("list-editors", Some(_subm)) => {              fatcat::auth::print_editors(&db_conn)?; -        }, +        }          ("create-editor", Some(subm)) => {              let editor = fatcat::api_helpers::create_editor(                  &db_conn,                  subm.value_of("username").unwrap().to_string(),                  subm.is_present("admin"), -                subm.is_present("bot"))?; +                subm.is_present("bot"), +            )?;              //println!("{:?}", editor);              println!("{}", FatCatId::from_uuid(&editor.id).to_string()); -        }, +        }          ("create-token", Some(subm)) => {              let editor_id = FatCatId::from_str(subm.value_of("editor-id").unwrap())?;              // check that editor exists @@ -112,19 +109,19 @@ fn run() -> Result<()> {                  .find(&editor_id.to_uuid())                  .get_result(&db_conn)?;              println!("{}", confectionary.create_token(editor_id, None)?); -        }, +        }          ("inspect-token", Some(subm)) => {              confectionary.inspect_token(&db_conn, subm.value_of("token").unwrap())?; -        }, +        }          ("revoke-tokens", Some(subm)) => {              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)) => {              fatcat::auth::revoke_tokens_everyone(&db_conn)?;              println!("success!"); -        }, +        }          _ => {              println!("Missing or unimplemented command!");              println!("{}", m.usage()); diff --git a/rust/src/bin/fatcatd.rs b/rust/src/bin/fatcatd.rs index 7def7f66..7d77d90b 100644 --- a/rust/src/bin/fatcatd.rs +++ b/rust/src/bin/fatcatd.rs @@ -21,7 +21,6 @@ use iron::{status, Chain, Iron, IronResult, Request, Response};  use iron_slog::{DefaultLogFormatter, LoggerMiddleware};  use slog::{Drain, Logger}; -  /// Create custom server, wire it to the autogenerated router,  /// and pass it to the web server.  fn main() { diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 233f8642..a31404da 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -22,16 +22,16 @@ extern crate data_encoding;  extern crate regex;  #[macro_use]  extern crate lazy_static; -extern crate sha1;  extern crate macaroon; +extern crate sha1;  pub mod api_entity_crud;  pub mod api_helpers;  pub mod api_server;  pub mod api_wrappers; +pub mod auth;  pub mod database_models;  pub mod database_schema; -pub mod auth;  pub mod errors {      // Create the Error, ErrorKind, ResultExt, and Result types @@ -74,6 +74,14 @@ pub mod errors {                  description("Invalid Entity State Transform")                  display("tried to mutate an entity which was not in an appropriate state: {}", message)              } +            InvalidCredentials(message: String) { +                description("auth token was missing, expired, revoked, or corrupt") +                display("auth token was missing, expired, revoked, or corrupt: {}", message) +            } +            InsufficientPrivileges(message: String) { +                description("editor account doesn't have authorization") +                display("editor account doesn't have authorization: {}", message) +            }              OtherBadRequest(message: String) {                  description("catch-all error for bad or unallowed requests")                  display("broke a constraint or made an otherwise invalid request: {}", message) @@ -86,14 +94,13 @@ pub mod errors {  pub use errors::*;  pub use self::errors::*; +use auth::AuthConfectionary;  use diesel::pg::PgConnection; -use diesel::prelude::*;  use diesel::r2d2::ConnectionManager;  use dotenv::dotenv;  use iron::middleware::AfterMiddleware;  use iron::{Request, Response};  use std::env; -use auth::AuthConfectionary;  #[cfg(feature = "postgres")]  embed_migrations!("../migrations/"); @@ -127,7 +134,10 @@ pub fn server() -> Result<api_server::Server> {          .build(manager)          .expect("Failed to create database pool.");      let confectionary = env_confectionary()?; -    Ok(api_server::Server { db_pool: pool, auth_confectionary: confectionary }) +    Ok(api_server::Server { +        db_pool: pool, +        auth_confectionary: confectionary, +    })  }  pub fn test_server() -> Result<api_server::Server> { diff --git a/rust/tests/test_auth.rs b/rust/tests/test_auth.rs index 5b04d595..8d20dafd 100644 --- a/rust/tests/test_auth.rs +++ b/rust/tests/test_auth.rs @@ -1,17 +1,16 @@ - +extern crate chrono;  extern crate fatcat;  extern crate uuid; -extern crate chrono; -use std::str::FromStr;  use chrono::prelude::*; -use fatcat::auth::*;  use fatcat::api_helpers::*; +use fatcat::auth::*; +use std::str::FromStr;  #[test]  fn test_macaroons() {      // Test everything we can without connecting to database -  +      let c = fatcat::auth::AuthConfectionary::new_dummy();      let editor_id = FatCatId::from_str("q3nouwy3nnbsvo3h5klxsx4a7y").unwrap(); @@ -23,7 +22,6 @@ fn test_macaroons() {      c.create_token(editor_id, Some(tomorrow)).unwrap();  } -  #[test]  fn test_auth_db() {      // Test things that require database @@ -39,7 +37,7 @@ fn test_auth_db() {      // verify token      let editor_row = c.parse_macaroon_token(&conn, &token).unwrap();      assert_eq!(editor_row.id, editor_id.to_uuid()); -     +      // revoke token      revoke_tokens(&conn, editor_id).unwrap(); | 
