From 802bafc92160205a2a3068b7b780a6a5eeb331d9 Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Tue, 18 Sep 2018 11:38:34 -0700 Subject: start skeleton of auth internal bits --- rust/src/bin/fatcatd.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) (limited to 'rust/src/bin/fatcatd.rs') diff --git a/rust/src/bin/fatcatd.rs b/rust/src/bin/fatcatd.rs index 57b6a3da..e14296da 100644 --- a/rust/src/bin/fatcatd.rs +++ b/rust/src/bin/fatcatd.rs @@ -20,9 +20,7 @@ use iron::modifiers::RedirectRaw; use iron::{status, Chain, Iron, IronResult, Request, Response}; use iron_slog::{DefaultLogFormatter, LoggerMiddleware}; use slog::{Drain, Logger}; -//use dotenv::dotenv; -//use std::env; -//use swagger::auth::AllowAllMiddleware; + /// Create custom server, wire it to the autogenerated router, /// and pass it to the web server. @@ -78,11 +76,9 @@ fn main() { let mut chain = Chain::new(LoggerMiddleware::new(router, logger, formatter)); - // Auth stuff unused for now - //chain.link_before(fatcat_api_spec::server::ExtractAuthData); - // add authentication middlewares into the chain here - // for the purpose of this example, pretend we have authenticated a user - //chain.link_before(AllowAllMiddleware::new("cosmo")); + // authentication + chain.link_before(fatcat_api_spec::server::ExtractAuthData); + chain.link_before(fatcat::auth::OpenAuthMiddleware::new()); chain.link_after(fatcat::XClacksOverheadMiddleware); -- cgit v1.2.3 From f9408344464285870409c2209a8edc8d25fd9bfa Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Fri, 28 Dec 2018 14:52:38 -0800 Subject: start refactor of auth code Pulls auth code (which requires the persistent state of a signing keyring) into a struct. Doesn't try verify macaroon in middleware, do it in individual wrappers. --- rust/src/api_server.rs | 2 + rust/src/api_wrappers.rs | 5 +- rust/src/auth.rs | 293 +++++++++++++++++--------------------------- rust/src/bin/fatcat-auth.rs | 3 +- rust/src/bin/fatcatd.rs | 1 - rust/src/database_models.rs | 2 +- rust/src/lib.rs | 3 +- 7 files changed, 121 insertions(+), 188 deletions(-) (limited to 'rust/src/bin/fatcatd.rs') 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, - _context: &Context, + context: &Context, ) -> Box + 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, // 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, } -// impl: -// - new() -// - verify(&str) -> Result -*/ - -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 { - 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>) -> Result { + 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 { + 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 + } + }); + 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) -> Result> { - let res: Option<(String, Vec)> = match req.extensions.get::() { + 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") { - 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 { - 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)>> { - 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 = 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)>> { - 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)") + 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 = 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>) -> Result { - 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 { 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 { -- cgit v1.2.3 From e16ba3d02564121ae5f27e0784be86137f3b9386 Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Mon, 31 Dec 2018 14:57:29 -0800 Subject: rustfmt; implement role-based auth checks --- rust/src/api_helpers.rs | 17 ++-- rust/src/api_server.rs | 3 +- rust/src/api_wrappers.rs | 113 +++++++++++++++++++++----- rust/src/auth.rs | 192 ++++++++++++++++++++++++++++++++++---------- rust/src/bin/fatcat-auth.rs | 53 ++++++------ rust/src/bin/fatcatd.rs | 1 - rust/src/lib.rs | 20 +++-- rust/tests/test_auth.rs | 12 ++- 8 files changed, 304 insertions(+), 107 deletions(-) (limited to 'rust/src/bin/fatcatd.rs') 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, autoaccept: bool, ) -> Result { - 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 { +pub fn create_editor( + conn: &DbConn, + username: String, + is_admin: bool, + is_bot: bool, +) -> Result { 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, conn: &DbConn, ) -> Result> { - 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 + 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, autoaccept: Option, editgroup_id: Option, - _context: &Context, + context: &Context, ) -> Box + 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, - _context: &Context, + context: &Context, ) -> Box + 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, - _context: &Context, + context: &Context, ) -> Box + 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 = 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 + 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 + 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 + 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, // 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 { + pub fn new( + location: String, + identifier: String, + key_base64: String, + ) -> Result { 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>) -> Result { - let mut mac = Macaroon::create(&self.location, &self.key, &self.identifier).expect("Macaroon creation"); + 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))); + 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 = 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> = 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::::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) -> Result> { - + 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(); + 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"); + 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) -> Result { + 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 = editor::table - .load(conn)?; + 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{:?}", + 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 ") .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 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( " '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( - " 'base64-encoded token (macaroon)'" - ) + .args_from_usage(" '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( - " 'identifier (fcid) of editor'" - ) + .args_from_usage(" '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 { .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 { 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(); -- cgit v1.2.3 From f19288ca809d87a286336e04f8e6b46ddbef300c Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Mon, 31 Dec 2018 17:11:30 -0800 Subject: add auth middleware back in I was hoping I didn't need this middleware, but I actually do, or the swagger generated code returns unauthenticated. The middleware doesn't actually do much validation, just extracts the (string) token and does nothing with it. Acutal verification happens in user code using AuthData struct. --- rust/src/auth.rs | 97 +++++++++++++++++++++++++++++++++++++++++++++++-- rust/src/bin/fatcatd.rs | 1 + 2 files changed, 95 insertions(+), 3 deletions(-) (limited to 'rust/src/bin/fatcatd.rs') diff --git a/rust/src/auth.rs b/rust/src/auth.rs index ee3c6fb0..16fd4fe2 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -2,7 +2,8 @@ use data_encoding::BASE64; use macaroon::{Format, Macaroon, Verifier}; -use swagger::auth::AuthData; +use std::fmt; +use swagger::auth::{AuthData, Authorization, Scopes}; use api_helpers::*; use chrono::prelude::*; @@ -59,7 +60,7 @@ impl AuthContext { pub fn require_editgroup(&self, conn: &DbConn, editgroup_id: FatCatId) -> Result<()> { if self.has_role(FatcatRole::Admin) { - return Ok(()) + return Ok(()); } let editgroup: EditgroupRow = editgroup::table .find(editgroup_id.to_uuid()) @@ -74,6 +75,94 @@ impl AuthContext { } } +#[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 + } +} + +fn new_auth_ironerror(m: &str) -> iron::error::IronError { + iron::error::IronError::new( + AuthError { msg: m.to_string() }, + (iron::status::BadRequest, m.to_string()), + ) +} + +#[derive(Debug)] +pub struct OpenAuthMiddleware; + +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 { + subject: "undefined".to_string(), + scopes: Scopes::All, + issuer: None, + }); + Ok(()) + } +} + +#[derive(Debug)] +pub struct MacaroonAuthMiddleware; + +impl MacaroonAuthMiddleware { + pub fn new() -> MacaroonAuthMiddleware { + MacaroonAuthMiddleware + } +} +impl iron::middleware::BeforeMiddleware for MacaroonAuthMiddleware { + fn before(&self, req: &mut iron::Request) -> iron::IronResult<()> { + // Structure here is sorta funky because we might some day actually want to parse token + // here in some way + let token: Option = match req.extensions.get::() { + Some(AuthData::ApiKey(header)) => { + let header: Vec = + 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")); + } + Some(header[1].to_string()) + } + None => None, + _ => { + return Err(new_auth_ironerror( + "auth HTTP Header should be empty or API token", + )); + } + }; + if let Some(_token) = token { + req.extensions.insert::(Authorization { + // This is just a dummy; all actual authentication happens later + subject: "undefined".to_string(), + scopes: Scopes::All, + issuer: None, + }); + }; + Ok(()) + } +} + #[derive(Clone)] pub struct AuthConfectionary { pub location: String, @@ -88,6 +177,7 @@ impl AuthConfectionary { identifier: String, key_base64: String, ) -> Result { + macaroon::initialize().unwrap(); let key = BASE64.decode(key_base64.as_bytes())?; let mut root_keys = HashMap::new(); root_keys.insert(identifier.clone(), key.clone()); @@ -180,7 +270,8 @@ impl AuthConfectionary { )); 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 { + // allow a second of wiggle room for precision and, eg, tests + if created < (auth_epoch - chrono::Duration::seconds(1)) { return Err(ErrorKind::InvalidCredentials( "token created before current auth_epoch (was probably revoked by editor)" .to_string(), diff --git a/rust/src/bin/fatcatd.rs b/rust/src/bin/fatcatd.rs index 7d77d90b..a4f20ddb 100644 --- a/rust/src/bin/fatcatd.rs +++ b/rust/src/bin/fatcatd.rs @@ -77,6 +77,7 @@ fn main() { // authentication chain.link_before(fatcat_api_spec::server::ExtractAuthData); + chain.link_before(fatcat::auth::MacaroonAuthMiddleware::new()); chain.link_after(fatcat::XClacksOverheadMiddleware); -- cgit v1.2.3 From b930bf22d4974363934514919539149a69b15167 Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Mon, 31 Dec 2018 17:40:51 -0800 Subject: allow multiple 'alt' keys to be specified in env --- rust/src/auth.rs | 6 ++++++ rust/src/bin/fatcatd.rs | 8 ++++++++ rust/src/lib.rs | 19 ++++++++++++++++++- 3 files changed, 32 insertions(+), 1 deletion(-) (limited to 'rust/src/bin/fatcatd.rs') diff --git a/rust/src/auth.rs b/rust/src/auth.rs index 16fd4fe2..4b608a96 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -198,6 +198,12 @@ impl AuthConfectionary { .unwrap() } + pub fn add_keypair(&mut self, identifier: String, key_base64: String) -> Result<()> { + let key = BASE64.decode(key_base64.as_bytes())?; + self.root_keys.insert(identifier, key); + Ok(()) + } + pub fn create_token( &self, editor_id: FatCatId, diff --git a/rust/src/bin/fatcatd.rs b/rust/src/bin/fatcatd.rs index a4f20ddb..04f88948 100644 --- a/rust/src/bin/fatcatd.rs +++ b/rust/src/bin/fatcatd.rs @@ -39,6 +39,14 @@ fn main() { let formatter = DefaultLogFormatter; let server = fatcat::server().unwrap(); + info!( + logger, + "using primary auth key: {}", server.auth_confectionary.identifier, + ); + info!( + logger, + "all auth keys: {:?}", server.auth_confectionary.root_keys.keys().collect::>(), + ); let mut router = fatcat_api_spec::router(server); router.get("/", root_handler, "root-redirect"); diff --git a/rust/src/lib.rs b/rust/src/lib.rs index a31404da..7d00641a 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -122,7 +122,24 @@ pub fn env_confectionary() -> Result { let auth_location = env::var("AUTH_LOCATION").expect("AUTH_LOCATION must be set"); let auth_key = env::var("AUTH_SECRET_KEY").expect("AUTH_SECRET_KEY must be set"); let auth_key_ident = env::var("AUTH_KEY_IDENT").expect("AUTH_KEY_IDENT must be set"); - AuthConfectionary::new(auth_location, auth_key_ident, auth_key) + info!("Loaded primary auth key: {}", auth_key_ident); + let mut confectionary = AuthConfectionary::new(auth_location, auth_key_ident, auth_key)?; + match env::var("AUTH_ALT_KEYS") { + Ok(var) => { + for pair in var.split(",") { + let pair: Vec<&str> = pair.split(":").collect(); + if pair.len() != 2 { + println!("{:#?}", pair); + bail!("couldn't parse keypair from AUTH_ALT_KEYS (expected 'ident:key' pairs separated by commas)"); + } + info!("Loading alt auth key: {}", pair[0]); + confectionary.add_keypair(pair[0].to_string(), pair[1].to_string())?; + + } + }, + Err(_) => (), + } + Ok(confectionary) } /// Instantiate a new API server with a pooled database connection -- cgit v1.2.3 From 42ffee8c583729287aed7eaa6df4b7b121c1f7f6 Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Mon, 31 Dec 2018 18:05:24 -0800 Subject: make editor_id optional when createding editgroup The editor_id can be infered from auth metadata. --- fatcat-openapi2.yml | 2 -- rust/fatcat-api-spec/README.md | 2 +- rust/fatcat-api-spec/api.yaml | 2 -- rust/fatcat-api-spec/api/swagger.yaml | 2 -- rust/fatcat-api-spec/src/models.rs | 7 ++++--- rust/src/api_server.rs | 6 +++--- rust/src/api_wrappers.rs | 13 +++++++++++++ rust/src/bin/fatcatd.rs | 7 ++++++- rust/src/database_models.rs | 2 +- rust/src/lib.rs | 3 +-- rust/tests/test_api_server_http.rs | 33 +++++++++++++++++++++++++++++++++ rust/tests/test_old_python_tests.rs | 7 +++++-- 12 files changed, 67 insertions(+), 19 deletions(-) (limited to 'rust/src/bin/fatcatd.rs') diff --git a/fatcat-openapi2.yml b/fatcat-openapi2.yml index 98b9e4b0..80db5074 100644 --- a/fatcat-openapi2.yml +++ b/fatcat-openapi2.yml @@ -445,8 +445,6 @@ definitions: example: "zerocool93" editgroup: type: object - required: - - editor_id properties: editgroup_id: <<: *FATCATIDENT diff --git a/rust/fatcat-api-spec/README.md b/rust/fatcat-api-spec/README.md index bed47c45..7e946b16 100644 --- a/rust/fatcat-api-spec/README.md +++ b/rust/fatcat-api-spec/README.md @@ -13,7 +13,7 @@ To see how to make this your own, look here: [README](https://github.com/swagger-api/swagger-codegen/blob/master/README.md) - API version: 0.1.0 -- Build date: 2018-12-31T22:21:53.785Z +- Build date: 2019-01-01T01:45:02.795Z This autogenerated project defines an API crate `fatcat` which contains: * An `Api` trait defining the API in Rust. diff --git a/rust/fatcat-api-spec/api.yaml b/rust/fatcat-api-spec/api.yaml index 98b9e4b0..80db5074 100644 --- a/rust/fatcat-api-spec/api.yaml +++ b/rust/fatcat-api-spec/api.yaml @@ -445,8 +445,6 @@ definitions: example: "zerocool93" editgroup: type: object - required: - - editor_id properties: editgroup_id: <<: *FATCATIDENT diff --git a/rust/fatcat-api-spec/api/swagger.yaml b/rust/fatcat-api-spec/api/swagger.yaml index 670d3551..12bfe192 100644 --- a/rust/fatcat-api-spec/api/swagger.yaml +++ b/rust/fatcat-api-spec/api/swagger.yaml @@ -7598,8 +7598,6 @@ definitions: upperCaseName: "EDITOR" editgroup: type: "object" - required: - - "editor_id" properties: editgroup_id: type: "string" diff --git a/rust/fatcat-api-spec/src/models.rs b/rust/fatcat-api-spec/src/models.rs index 01b4c28e..4d7575b6 100644 --- a/rust/fatcat-api-spec/src/models.rs +++ b/rust/fatcat-api-spec/src/models.rs @@ -190,7 +190,8 @@ pub struct Editgroup { /// base32-encoded unique identifier #[serde(rename = "editor_id")] - pub editor_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub editor_id: Option, #[serde(rename = "description")] #[serde(skip_serializing_if = "Option::is_none")] @@ -206,10 +207,10 @@ pub struct Editgroup { } impl Editgroup { - pub fn new(editor_id: String) -> Editgroup { + pub fn new() -> Editgroup { Editgroup { editgroup_id: None, - editor_id: editor_id, + editor_id: None, description: None, extra: None, edits: None, diff --git a/rust/src/api_server.rs b/rust/src/api_server.rs index 853f7bc2..be9f1883 100644 --- a/rust/src/api_server.rs +++ b/rust/src/api_server.rs @@ -384,7 +384,7 @@ impl Server { ) -> Result { let row: EditgroupRow = insert_into(editgroup::table) .values(( - editgroup::editor_id.eq(FatCatId::from_str(&entity.editor_id)?.to_uuid()), + editgroup::editor_id.eq(FatCatId::from_str(&entity.editor_id.unwrap())?.to_uuid()), editgroup::description.eq(entity.description), editgroup::extra_json.eq(entity.extra), )) @@ -392,7 +392,7 @@ impl Server { Ok(Editgroup { editgroup_id: Some(uuid2fcid(&row.id)), - editor_id: uuid2fcid(&row.editor_id), + editor_id: Some(uuid2fcid(&row.editor_id)), description: row.description, edits: None, extra: row.extra_json, @@ -467,7 +467,7 @@ impl Server { let eg = Editgroup { editgroup_id: Some(uuid2fcid(&row.id)), - editor_id: uuid2fcid(&row.editor_id), + editor_id: Some(uuid2fcid(&row.editor_id)), description: row.description, edits: Some(edits), extra: row.extra_json, diff --git a/rust/src/api_wrappers.rs b/rust/src/api_wrappers.rs index ae070e02..3dec1c26 100644 --- a/rust/src/api_wrappers.rs +++ b/rust/src/api_wrappers.rs @@ -988,6 +988,19 @@ impl Api for Server { .auth_confectionary .require_auth(&conn, &context.auth_data)?; auth_context.require_role(FatcatRole::Editor)?; + let mut entity = entity.clone(); + match entity.editor_id.clone() { + Some(editor_id) => { + if !auth_context.has_role(FatcatRole::Admin) { + if editor_id != auth_context.editor_id.to_string() { + bail!("not authorized to create editgroups in others' names"); + } + } + }, + None => { + entity.editor_id = Some(auth_context.editor_id.to_string()); + } + }; self.create_editgroup_handler(entity, &conn) }) { Ok(eg) => CreateEditgroupResponse::SuccessfullyCreated(eg), diff --git a/rust/src/bin/fatcatd.rs b/rust/src/bin/fatcatd.rs index 04f88948..682f5038 100644 --- a/rust/src/bin/fatcatd.rs +++ b/rust/src/bin/fatcatd.rs @@ -45,7 +45,12 @@ fn main() { ); info!( logger, - "all auth keys: {:?}", server.auth_confectionary.root_keys.keys().collect::>(), + "all auth keys: {:?}", + server + .auth_confectionary + .root_keys + .keys() + .collect::>(), ); let mut router = fatcat_api_spec::router(server); diff --git a/rust/src/database_models.rs b/rust/src/database_models.rs index f6cca3e1..7a65f901 100644 --- a/rust/src/database_models.rs +++ b/rust/src/database_models.rs @@ -564,7 +564,7 @@ impl EditgroupRow { pub fn into_model_partial(self) -> Editgroup { Editgroup { editgroup_id: Some(uuid2fcid(&self.id)), - editor_id: uuid2fcid(&self.editor_id), + editor_id: Some(uuid2fcid(&self.editor_id)), description: self.description, extra: self.extra_json, edits: None, diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 7d00641a..b3e6c813 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -134,9 +134,8 @@ pub fn env_confectionary() -> Result { } info!("Loading alt auth key: {}", pair[0]); confectionary.add_keypair(pair[0].to_string(), pair[1].to_string())?; - } - }, + } Err(_) => (), } Ok(confectionary) diff --git a/rust/tests/test_api_server_http.rs b/rust/tests/test_api_server_http.rs index 2160a0a0..d975fe6e 100644 --- a/rust/tests/test_api_server_http.rs +++ b/rust/tests/test_api_server_http.rs @@ -1545,3 +1545,36 @@ fn test_release_types() { Some("release_type"), ); } + +#[test] +fn test_create_editgroup() { + let (headers, router, _conn) = setup_http(); + + // We're authenticated, so don't need to supply editor_id + check_http_response( + request::post( + &format!( + "http://localhost:9411/v0/editgroup", + ), + headers.clone(), + "{}", + &router, + ), + status::Created, + None, + ); + + // But can if we want to + check_http_response( + request::post( + &format!( + "http://localhost:9411/v0/editgroup", + ), + headers.clone(), + r#"{"editor_id": "aaaaaaaaaaaabkvkaaaaaaaaae"}"#, + &router, + ), + status::Created, + None, + ); +} diff --git a/rust/tests/test_old_python_tests.rs b/rust/tests/test_old_python_tests.rs index 1f91c7db..afeff55e 100644 --- a/rust/tests/test_old_python_tests.rs +++ b/rust/tests/test_old_python_tests.rs @@ -22,7 +22,8 @@ fn test_api_rich_create() { let admin_id = "aaaaaaaaaaaabkvkaaaaaaaaae".to_string(); - let mut new_eg = Editgroup::new(admin_id); + let mut new_eg = Editgroup::new(); + new_eg.editor_id = Some(admin_id); new_eg.description = Some("a unit test edit".to_string()); let resp = client.create_editgroup(new_eg).wait().unwrap(); let editgroup_id = match resp { @@ -196,8 +197,10 @@ fn test_merge_works() { let admin_id = "aaaaaaaaaaaabkvkaaaaaaaaae".to_string(); + let mut eg = Editgroup::new(); + eg.editor_id = Some(admin_id); let resp = client - .create_editgroup(Editgroup::new(admin_id)) + .create_editgroup(eg) .wait() .unwrap(); let editgroup_id = match resp { -- cgit v1.2.3