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/fatcat-auth.rs | 109 ++++++++++++++++++++++++++++++++++++++++++++ rust/src/bin/fatcatd.rs | 12 ++--- 2 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 rust/src/bin/fatcat-auth.rs (limited to 'rust/src/bin') diff --git a/rust/src/bin/fatcat-auth.rs b/rust/src/bin/fatcat-auth.rs new file mode 100644 index 00000000..7cb8af8e --- /dev/null +++ b/rust/src/bin/fatcat-auth.rs @@ -0,0 +1,109 @@ +//! JSON Export Helper + +#[macro_use] +extern crate clap; +extern crate dotenv; +#[macro_use] +extern crate error_chain; +extern crate fatcat; +#[macro_use] +extern crate log; +extern crate env_logger; +extern crate serde_json; +extern crate uuid; + +use clap::{App, Arg, SubCommand}; +use dotenv::dotenv; +use std::env; + +use fatcat::errors::*; +use fatcat::api_helpers::FatCatId; +use std::str::FromStr; +use uuid::Uuid; + +use error_chain::ChainedError; +//use std::io::{Stdout,StdoutLock}; +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( + SubCommand::with_name("create-editor") + .about("Creates a new auth token (macaroon) for the given editor") + .args_from_usage( + " 'username for editor' + --admin 'creates editor with admin privs' + --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'" + ) + ) + .subcommand( + SubCommand::with_name("inspect-token") + .about("Dumps token metadata (and whether it is valid)") + ) + .subcommand( + SubCommand::with_name("revoke-tokens") + .about("Resets auth_epoch for a single editor (invalidating all existing tokens)") + ) + .subcommand( + SubCommand::with_name("revoke-tokens-all") + .about("Resets auth_epoch for all editors (invalidating tokens for all users!)") + ) + .get_matches(); + +/* + value_t_or_exit!(subm, "magic", u32) + .after_help("Reads a ident table TSV dump from stdin (aka, ident_id, rev_id, redirect_id), \ + and outputs JSON (one entity per line). Database connection info read from environment \ + (DATABASE_URL, same as fatcatd).") +*/ + match m.subcommand() { + ("list-editors", Some(_subm)) => { + fatcat::auth::print_editors()?; + }, + ("create-editor", Some(subm)) => { + fatcat::auth::create_editor( + subm.value_of("username").unwrap().to_string(), + subm.is_present("admin"), + subm.is_present("bot"))?; + }, + ("create-token", Some(subm)) => { + let editor_id = FatCatId::from_str(subm.value_of("editor").unwrap())?; + fatcat::auth::create_token(editor_id, None)?; + }, + ("inspect-token", Some(subm)) => { + fatcat::auth::inspect_token(subm.value_of("token").unwrap())?; + }, + ("revoke-tokens", Some(subm)) => { + let editor_id = FatCatId::from_str(subm.value_of("editor").unwrap())?; + fatcat::auth::revoke_tokens(editor_id)?; + }, + ("revoke-tokens-everyone", Some(_subm)) => { + fatcat::auth::revoke_tokens_everyone()?; + }, + _ => { + println!("Missing or unimplemented command!"); + println!("{}", m.usage()); + ::std::process::exit(-1); + } + } + Ok(()) +} + +quick_main!(run); 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 946c98593cb5346fff3d1aa72c4992376ec20471 Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Thu, 27 Dec 2018 00:43:31 -0800 Subject: sql codegen and WIP on auth command --- rust/src/auth.rs | 49 ++++++++++++++++++++++++++++++++------------- rust/src/bin/fatcat-auth.rs | 40 ++++++++++++++++++++++++++---------- rust/src/database_models.rs | 3 +++ rust/src/database_schema.rs | 3 +++ 4 files changed, 70 insertions(+), 25 deletions(-) (limited to 'rust/src/bin') diff --git a/rust/src/auth.rs b/rust/src/auth.rs index 651f7979..6ded1188 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -4,17 +4,17 @@ use swagger::auth::{AuthData, Authorization, Scopes}; //use macaroon::{Macaroon, Verifier}; use std::collections::BTreeSet; -//use database_models::*; -//use database_schema::*; +use database_models::*; +use database_schema::*; use api_helpers::*; use chrono; -//use diesel; +use diesel; use iron; -//use diesel::prelude::*; +use diesel::prelude::*; use errors::*; -//use serde_json; -//use std::str::FromStr; -//use uuid::Uuid; +use serde_json; +use std::str::FromStr; +use uuid::Uuid; #[derive(Debug)] pub struct OpenAuthMiddleware; @@ -76,20 +76,41 @@ impl iron::middleware::BeforeMiddleware for MacaroonAuthMiddleware { } // DUMMY: parse macaroon +/// On success, returns Some((editor_id, scopes)), where `scopes` is a vector of strings. pub fn parse_macaroon_token(s: &str) -> Result)>> { Ok(Some(("some_editor_id".to_string(), vec![]))) } -pub fn print_editors() -> Result<()>{ - unimplemented!(); +pub fn print_editors(conn: &DbConn) -> Result<()>{ // iterate over all editors. format id, print flags, auth_epoch + let all_editors: Vec = editor::table + .load(conn)?; + println!("editor_id\t\t\tis_admin/is_bot\tauth_epoch\t\t\tusername\twrangler_id"); + for e in all_editors { + println!("{}\t{}\t{}\t{}\t{}\t{:?}", + FatCatId::from_uuid(&e.id).to_string(), + e.is_admin, + e.is_bot, + e.auth_epoch, + e.username, + e.wrangler_id, + ); + } + Ok(()) } -pub fn create_editor(username: String, is_admin: bool, is_bot: bool) -> Result<()> { // TODO: EditorRow or something - unimplemented!(); +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), + editor::is_admin.eq(is_admin), + editor::is_bot.eq(is_bot), + )) + .get_result(conn)?; + Ok(ed) } -pub fn create_token(editor_id: FatCatId, expires: Option) -> Result { +pub fn create_token(conn: &DbConn, editor_id: FatCatId, expires: Option) -> Result { unimplemented!(); } @@ -97,10 +118,10 @@ pub fn inspect_token(token: &str) -> Result<()> { unimplemented!(); } -pub fn revoke_tokens(editor_id: FatCatId) -> Result<()>{ +pub fn revoke_tokens(conn: &DbConn, editor_id: FatCatId) -> Result<()>{ unimplemented!(); } -pub fn revoke_tokens_everyone() -> Result { +pub fn revoke_tokens_everyone(conn: &DbConn) -> Result { unimplemented!(); } diff --git a/rust/src/bin/fatcat-auth.rs b/rust/src/bin/fatcat-auth.rs index 7cb8af8e..a5fedc1f 100644 --- a/rust/src/bin/fatcat-auth.rs +++ b/rust/src/bin/fatcat-auth.rs @@ -2,6 +2,7 @@ #[macro_use] extern crate clap; +extern crate diesel; extern crate dotenv; #[macro_use] extern crate error_chain; @@ -16,6 +17,9 @@ use clap::{App, Arg, SubCommand}; use dotenv::dotenv; use std::env; +use diesel::prelude::*; +use diesel::r2d2::ConnectionManager; +use fatcat::ConnectionPool; use fatcat::errors::*; use fatcat::api_helpers::FatCatId; use std::str::FromStr; @@ -27,6 +31,18 @@ use std::io::prelude::*; use std::io::{BufReader, BufWriter}; +/// Instantiate a new API server with a pooled database connection +// TODO: copypasta from fatcat-export +pub fn database_worker_pool() -> Result { + dotenv().ok(); + let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + let manager = ConnectionManager::::new(database_url); + let pool = diesel::r2d2::Pool::builder() + .build(manager) + .expect("Failed to create database pool."); + Ok(pool) +} + fn run() -> Result<()> { let m = App::new("fatcat-auth") .version(env!("CARGO_PKG_VERSION")) @@ -67,35 +83,37 @@ fn run() -> Result<()> { ) .get_matches(); -/* - value_t_or_exit!(subm, "magic", u32) - .after_help("Reads a ident table TSV dump from stdin (aka, ident_id, rev_id, redirect_id), \ - and outputs JSON (one entity per line). Database connection info read from environment \ - (DATABASE_URL, same as fatcatd).") -*/ match m.subcommand() { ("list-editors", Some(_subm)) => { - fatcat::auth::print_editors()?; + let db_conn = database_worker_pool()?.get().expect("database pool"); + fatcat::auth::print_editors(&db_conn)?; }, ("create-editor", Some(subm)) => { - fatcat::auth::create_editor( + let db_conn = database_worker_pool()?.get().expect("database pool"); + let editor = fatcat::auth::create_editor( + &db_conn, subm.value_of("username").unwrap().to_string(), subm.is_present("admin"), subm.is_present("bot"))?; + //println!("{:?}", editor); + println!("{}", FatCatId::from_uuid(&editor.id).to_string()); }, ("create-token", Some(subm)) => { + let db_conn = database_worker_pool()?.get().expect("database pool"); let editor_id = FatCatId::from_str(subm.value_of("editor").unwrap())?; - fatcat::auth::create_token(editor_id, None)?; + fatcat::auth::create_token(&db_conn, editor_id, None)?; }, ("inspect-token", Some(subm)) => { fatcat::auth::inspect_token(subm.value_of("token").unwrap())?; }, ("revoke-tokens", Some(subm)) => { + let db_conn = database_worker_pool()?.get().expect("database pool"); let editor_id = FatCatId::from_str(subm.value_of("editor").unwrap())?; - fatcat::auth::revoke_tokens(editor_id)?; + fatcat::auth::revoke_tokens(&db_conn, editor_id)?; }, ("revoke-tokens-everyone", Some(_subm)) => { - fatcat::auth::revoke_tokens_everyone()?; + let db_conn = database_worker_pool()?.get().expect("database pool"); + fatcat::auth::revoke_tokens_everyone(&db_conn)?; }, _ => { println!("Missing or unimplemented command!"); diff --git a/rust/src/database_models.rs b/rust/src/database_models.rs index fc5fc896..55ba7fb9 100644 --- a/rust/src/database_models.rs +++ b/rust/src/database_models.rs @@ -578,7 +578,10 @@ pub struct EditorRow { pub id: Uuid, pub username: String, pub is_admin: bool, + pub is_bot: bool, pub registered: chrono::NaiveDateTime, + pub auth_epoch: chrono::NaiveDateTime, + pub wrangler_id: Option, pub active_editgroup_id: Option, } diff --git a/rust/src/database_schema.rs b/rust/src/database_schema.rs index 2777696d..c240048e 100644 --- a/rust/src/database_schema.rs +++ b/rust/src/database_schema.rs @@ -97,7 +97,10 @@ table! { id -> Uuid, username -> Text, is_admin -> Bool, + is_bot -> Bool, registered -> Timestamptz, + auth_epoch -> Timestamptz, + wrangler_id -> Nullable, active_editgroup_id -> Nullable, } } -- cgit v1.2.3 From fa1892834a4650bf2e85215a3270e309d3e9f1c9 Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Thu, 27 Dec 2018 21:06:09 -0800 Subject: more basic work on auth --- rust/src/api_server.rs | 11 --- rust/src/auth.rs | 197 +++++++++++++++++++++++++++++++++++++++----- rust/src/bin/fatcat-auth.rs | 36 ++++---- rust/src/lib.rs | 3 +- 4 files changed, 200 insertions(+), 47 deletions(-) (limited to 'rust/src/bin') diff --git a/rust/src/api_server.rs b/rust/src/api_server.rs index d264afbc..af2a7e24 100644 --- a/rust/src/api_server.rs +++ b/rust/src/api_server.rs @@ -38,17 +38,6 @@ macro_rules! entity_batch_handler { } } -macro_rules! count_entity { - ($table:ident, $conn:expr) => {{ - let count: i64 = $table::table - .filter($table::is_live.eq(true)) - .filter($table::redirect_id.is_null()) - .count() - .first($conn)?; - count - }}; -} - #[derive(Clone)] pub struct Server { pub db_pool: ConnectionPool, diff --git a/rust/src/auth.rs b/rust/src/auth.rs index 6ded1188..580cde28 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -1,24 +1,68 @@ //! Editor bearer token authentication use swagger::auth::{AuthData, Authorization, Scopes}; -//use macaroon::{Macaroon, Verifier}; +use macaroon::{Format, Macaroon, Verifier}; +use data_encoding::BASE64; use std::collections::BTreeSet; +use std::fmt; use database_models::*; use database_schema::*; use api_helpers::*; -use chrono; +use chrono::prelude::*; use diesel; use iron; use diesel::prelude::*; use errors::*; -use serde_json; +//use serde_json; use std::str::FromStr; -use uuid::Uuid; +//use uuid::Uuid; + +// 32 bytes max (!) +static DUMMY_KEY: &[u8] = b"dummy-key-a-one-two-three-a-la"; #[derive(Debug)] pub struct OpenAuthMiddleware; +#[derive(Debug)] +pub struct AuthError { + msg: String, +} + +impl fmt::Display for AuthError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "AuthError: {}", &self.msg) + } +} + +impl iron::Error for AuthError { + fn description(&self) -> &str { + &self.msg + } + fn cause(&self) -> Option<&iron::Error> { + None + } +} + +/* +#[derive(Debug)] +pub struct FatcatAuthBakery { + root_key_store: bool, // hashmap + signing_key: bool, // string name +} + +// impl: +// - new() +// - verify(&str) -> Result +*/ + +fn new_auth_ironerror(m: &str) -> iron::error::IronError { + iron::error::IronError::new( + AuthError { msg: m.to_string() }, + (iron::status::BadRequest, m.to_string()) + ) +} + impl OpenAuthMiddleware { /// Create a middleware that authorizes with the configured subject. pub fn new() -> OpenAuthMiddleware { @@ -42,6 +86,7 @@ pub struct MacaroonAuthMiddleware; impl MacaroonAuthMiddleware { pub fn new() -> MacaroonAuthMiddleware { + macaroon::initialize().unwrap(); MacaroonAuthMiddleware } } @@ -52,13 +97,15 @@ impl iron::middleware::BeforeMiddleware for MacaroonAuthMiddleware { let res: Option<(String, Vec)> = match req.extensions.get::() { Some(AuthData::ApiKey(header)) => { let header: Vec = header.split_whitespace().map(|s| s.to_string()).collect(); - // TODO: error types - assert!(header.len() == 2); - assert!(header[0] == "Bearer"); - parse_macaroon_token(&header[1]).expect("valid macaroon") + if !(header.len() == 2 && header[0] == "Bearer") { + return Err(new_auth_ironerror("invalid bearer auth HTTP Header")); + } + sniff_macaroon_token(&header[1]).expect("valid macaroon") }, None => None, - _ => panic!("valid auth header, or none") + _ => { + return Err(new_auth_ironerror("auth HTTP Header should be empty or API key")); + } }; if let Some((editor_id, scopes)) = res { let mut scope_set = BTreeSet::new(); @@ -75,10 +122,87 @@ impl iron::middleware::BeforeMiddleware for MacaroonAuthMiddleware { } } -// DUMMY: parse macaroon +/// Just checks signature and expired time; can't hit database, so nothing else +pub fn sniff_macaroon_token(s: &str) -> Result)>> { + 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(s: &str) -> Result)>> { - Ok(Some(("some_editor_id".to_string(), vec![]))) +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)") + } + 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<()>{ @@ -99,6 +223,8 @@ pub fn print_editors(conn: &DbConn) -> Result<()>{ Ok(()) } +// TODO: move to api_helpers or some such +// TODO: verify username pub fn create_editor(conn: &DbConn, username: String, is_admin: bool, is_bot: bool) -> Result { let ed: EditorRow = diesel::insert_into(editor::table) .values(( @@ -110,18 +236,51 @@ pub fn create_editor(conn: &DbConn, username: String, is_admin: bool, is_bot: bo Ok(ed) } -pub fn create_token(conn: &DbConn, editor_id: FatCatId, expires: Option) -> Result { - unimplemented!(); +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(token: &str) -> Result<()> { - unimplemented!(); +pub fn inspect_token(conn: &DbConn, token: &str) -> Result<()> { + let raw = BASE64.decode(token.as_bytes())?; + let mac = match Macaroon::deserialize(&raw) { + Ok(m) => m, + Err(e) => bail!("macaroon deserialize error: {:?}", e), + }; + let now = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true); + println!("current time: {}", now); + println!("domain (location): {:?}", mac.location()); + println!("signing key name (identifier): {}", mac.identifier()); + for caveat in mac.first_party_caveats() { + println!("caveat: {}", caveat.predicate()); + } + // TODO: don't display full stacktrace on failure + println!("verify: {:?}", parse_macaroon_token(conn, token)); + Ok(()) } pub fn revoke_tokens(conn: &DbConn, editor_id: FatCatId) -> Result<()>{ - unimplemented!(); + diesel::update(editor::table.filter(editor::id.eq(&editor_id.to_uuid()))) + .set(editor::auth_epoch.eq(Utc::now())) + .execute(conn)?; + Ok(()) } -pub fn revoke_tokens_everyone(conn: &DbConn) -> Result { - unimplemented!(); +pub fn revoke_tokens_everyone(conn: &DbConn) -> Result<()> { + diesel::update(editor::table) + .set(editor::auth_epoch.eq(Utc::now())) + .execute(conn)?; + Ok(()) } diff --git a/rust/src/bin/fatcat-auth.rs b/rust/src/bin/fatcat-auth.rs index a5fedc1f..9b4550d8 100644 --- a/rust/src/bin/fatcat-auth.rs +++ b/rust/src/bin/fatcat-auth.rs @@ -1,13 +1,13 @@ //! JSON Export Helper -#[macro_use] +//#[macro_use] extern crate clap; extern crate diesel; extern crate dotenv; #[macro_use] extern crate error_chain; extern crate fatcat; -#[macro_use] +//#[macro_use] extern crate log; extern crate env_logger; extern crate serde_json; @@ -23,12 +23,12 @@ use fatcat::ConnectionPool; use fatcat::errors::*; use fatcat::api_helpers::FatCatId; use std::str::FromStr; -use uuid::Uuid; +//use uuid::Uuid; -use error_chain::ChainedError; +//use error_chain::ChainedError; //use std::io::{Stdout,StdoutLock}; -use std::io::prelude::*; -use std::io::{BufReader, BufWriter}; +//use std::io::prelude::*; +//use std::io::{BufReader, BufWriter}; /// Instantiate a new API server with a pooled database connection @@ -72,24 +72,29 @@ fn run() -> Result<()> { .subcommand( SubCommand::with_name("inspect-token") .about("Dumps token metadata (and whether it is valid)") + .args_from_usage( + " 'base64-encoded token (macaroon)'" + ) ) .subcommand( SubCommand::with_name("revoke-tokens") .about("Resets auth_epoch for a single editor (invalidating all existing tokens)") + .args_from_usage( + " 'identifier (fcid) of editor'" + ) ) .subcommand( - SubCommand::with_name("revoke-tokens-all") + SubCommand::with_name("revoke-tokens-everyone") .about("Resets auth_epoch for all editors (invalidating tokens for all users!)") ) .get_matches(); + let db_conn = database_worker_pool()?.get().expect("database pool"); match m.subcommand() { ("list-editors", Some(_subm)) => { - let db_conn = database_worker_pool()?.get().expect("database pool"); fatcat::auth::print_editors(&db_conn)?; }, ("create-editor", Some(subm)) => { - let db_conn = database_worker_pool()?.get().expect("database pool"); let editor = fatcat::auth::create_editor( &db_conn, subm.value_of("username").unwrap().to_string(), @@ -99,21 +104,20 @@ fn run() -> Result<()> { println!("{}", FatCatId::from_uuid(&editor.id).to_string()); }, ("create-token", Some(subm)) => { - let db_conn = database_worker_pool()?.get().expect("database pool"); - let editor_id = FatCatId::from_str(subm.value_of("editor").unwrap())?; - fatcat::auth::create_token(&db_conn, editor_id, None)?; + let editor_id = FatCatId::from_str(subm.value_of("editor-id").unwrap())?; + println!("{}", fatcat::auth::create_token(&db_conn, editor_id, None)?); }, ("inspect-token", Some(subm)) => { - fatcat::auth::inspect_token(subm.value_of("token").unwrap())?; + fatcat::auth::inspect_token(&db_conn, subm.value_of("token").unwrap())?; }, ("revoke-tokens", Some(subm)) => { - let db_conn = database_worker_pool()?.get().expect("database pool"); - let editor_id = FatCatId::from_str(subm.value_of("editor").unwrap())?; + let editor_id = FatCatId::from_str(subm.value_of("editor-id").unwrap())?; fatcat::auth::revoke_tokens(&db_conn, editor_id)?; + println!("success!"); }, ("revoke-tokens-everyone", Some(_subm)) => { - let db_conn = database_worker_pool()?.get().expect("database pool"); fatcat::auth::revoke_tokens_everyone(&db_conn)?; + println!("success!"); }, _ => { println!("Missing or unimplemented command!"); diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 2a6c7203..f081be25 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -15,7 +15,6 @@ extern crate swagger; #[macro_use] extern crate error_chain; extern crate iron; -#[macro_use] extern crate serde_json; #[macro_use] extern crate log; @@ -43,6 +42,8 @@ pub mod errors { Uuid(::uuid::ParseError); Io(::std::io::Error) #[cfg(unix)]; Serde(::serde_json::Error); + Utf8Decode(::std::string::FromUtf8Error); + StringDecode(::data_encoding::DecodeError); } errors { InvalidFatcatId(id: String) { -- 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') 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 d50f7729cbc86c62dba9bd4db80786f07b44a7c0 Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Fri, 28 Dec 2018 22:04:45 -0800 Subject: more auth refactoring --- rust/src/api_helpers.rs | 12 +++++ rust/src/auth.rs | 113 +++++++++++++++++++++++--------------------- rust/src/bin/fatcat-auth.rs | 8 ++-- rust/src/lib.rs | 2 +- 4 files changed, 76 insertions(+), 59 deletions(-) (limited to 'rust/src/bin') diff --git a/rust/src/api_helpers.rs b/rust/src/api_helpers.rs index ff164bef..da208c0a 100644 --- a/rust/src/api_helpers.rs +++ b/rust/src/api_helpers.rs @@ -228,6 +228,18 @@ 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 { + let ed: EditorRow = diesel::insert_into(editor::table) + .values(( + editor::username.eq(username), + editor::is_admin.eq(is_admin), + editor::is_bot.eq(is_bot), + )) + .get_result(conn)?; + Ok(ed) +} + /// This function should always be run within a transaction pub fn get_or_create_editgroup(editor_id: Uuid, conn: &DbConn) -> Result { // check for current active diff --git a/rust/src/auth.rs b/rust/src/auth.rs index c188233a..8e9a6309 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -4,7 +4,7 @@ use swagger::auth::AuthData; use macaroon::{Format, Macaroon, Verifier}; use data_encoding::BASE64; -use std::collections::{BTreeSet,HashMap}; +use std::collections::HashMap; use database_models::*; use database_schema::*; use api_helpers::*; @@ -33,21 +33,36 @@ impl AuthContext { #[derive(Clone)] pub struct AuthConfectionary { - pub root_keys: HashMap, + pub location: String, + pub identifier: String, + pub key: Vec, + pub root_keys: HashMap>, } impl AuthConfectionary { - pub fn new() -> AuthConfectionary { + pub fn new(location: String, identifier: String, key: Vec) -> AuthConfectionary { + let mut root_keys = HashMap::new(); + root_keys.insert(identifier.clone(), key.clone()); AuthConfectionary { - root_keys: HashMap::new(), + location: location, + identifier: identifier, + key: key, + root_keys: root_keys, } } + pub fn new_dummy() -> AuthConfectionary { + AuthConfectionary::new( + "test.fatcat.wiki".to_string(), + "dummy".to_string(), + DUMMY_KEY.to_vec()) + } + pub fn create_token(&self, 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"); + 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); @@ -110,10 +125,11 @@ impl AuthConfectionary { false } }); - if !mac.verify_signature(DUMMY_KEY) { - bail!("token signature verification failed"); + let verify_key = match self.root_keys.get(mac.identifier()) { + Some(key) => key, + None => bail!("key not found for identifier: {}", mac.identifier()), }; - match mac.verify(DUMMY_KEY, &mut verifier) { + match mac.verify(verify_key, &mut verifier) { Ok(true) => (), Ok(false) => bail!("token overall verification failed"), Err(e) => bail!("token parsing failed: {:?}", e), @@ -146,8 +162,43 @@ impl AuthConfectionary { roles: roles, })) } + + // TODO: refactor out of this file? + /// Only used from CLI tool + pub fn inspect_token(&self, conn: &DbConn, token: &str) -> Result<()> { + let raw = BASE64.decode(token.as_bytes())?; + let mac = match Macaroon::deserialize(&raw) { + Ok(m) => m, + Err(e) => bail!("macaroon deserialize error: {:?}", e), + }; + let now = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true); + println!("current time: {}", now); + println!("domain (location): {:?}", mac.location()); + println!("signing key name (identifier): {}", mac.identifier()); + for caveat in mac.first_party_caveats() { + println!("caveat: {}", caveat.predicate()); + } + println!("verify: {:?}", self.parse_macaroon_token(conn, token)); + Ok(()) + } } +pub fn revoke_tokens(conn: &DbConn, editor_id: FatCatId) -> Result<()>{ + diesel::update(editor::table.filter(editor::id.eq(&editor_id.to_uuid()))) + .set(editor::auth_epoch.eq(Utc::now())) + .execute(conn)?; + Ok(()) +} + +pub fn revoke_tokens_everyone(conn: &DbConn) -> Result<()> { + diesel::update(editor::table) + .set(editor::auth_epoch.eq(Utc::now())) + .execute(conn)?; + Ok(()) +} + +// TODO: refactor out of this file? +/// Only used from CLI tool pub fn print_editors(conn: &DbConn) -> Result<()>{ // iterate over all editors. format id, print flags, auth_epoch let all_editors: Vec = editor::table @@ -165,49 +216,3 @@ pub fn print_editors(conn: &DbConn) -> Result<()>{ } Ok(()) } - -// TODO: move to api_helpers or some such -// TODO: verify username -pub fn create_editor(conn: &DbConn, username: String, is_admin: bool, is_bot: bool) -> Result { - let ed: EditorRow = diesel::insert_into(editor::table) - .values(( - editor::username.eq(username), - editor::is_admin.eq(is_admin), - editor::is_bot.eq(is_bot), - )) - .get_result(conn)?; - Ok(ed) -} - -pub fn inspect_token(conn: &DbConn, token: &str) -> Result<()> { - let raw = BASE64.decode(token.as_bytes())?; - let mac = match Macaroon::deserialize(&raw) { - Ok(m) => m, - Err(e) => bail!("macaroon deserialize error: {:?}", e), - }; - let now = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true); - println!("current time: {}", now); - println!("domain (location): {:?}", mac.location()); - println!("signing key name (identifier): {}", mac.identifier()); - for caveat in mac.first_party_caveats() { - println!("caveat: {}", caveat.predicate()); - } - let ac = AuthConfectionary::new(); - // TODO: don't display full stacktrace on failure - println!("verify: {:?}", ac.parse_macaroon_token(conn, token)); - Ok(()) -} - -pub fn revoke_tokens(conn: &DbConn, editor_id: FatCatId) -> Result<()>{ - diesel::update(editor::table.filter(editor::id.eq(&editor_id.to_uuid()))) - .set(editor::auth_epoch.eq(Utc::now())) - .execute(conn)?; - Ok(()) -} - -pub fn revoke_tokens_everyone(conn: &DbConn) -> Result<()> { - diesel::update(editor::table) - .set(editor::auth_epoch.eq(Utc::now())) - .execute(conn)?; - Ok(()) -} diff --git a/rust/src/bin/fatcat-auth.rs b/rust/src/bin/fatcat-auth.rs index f11f7a67..4b90da74 100644 --- a/rust/src/bin/fatcat-auth.rs +++ b/rust/src/bin/fatcat-auth.rs @@ -13,7 +13,7 @@ extern crate env_logger; extern crate serde_json; extern crate uuid; -use clap::{App, Arg, SubCommand}; +use clap::{App, SubCommand}; use dotenv::dotenv; use std::env; @@ -90,13 +90,13 @@ fn run() -> Result<()> { .get_matches(); let db_conn = database_worker_pool()?.get().expect("database pool"); - let confectionary = fatcat::auth::AuthConfectionary::new(); + let confectionary = fatcat::auth::AuthConfectionary::new_dummy(); match m.subcommand() { ("list-editors", Some(_subm)) => { fatcat::auth::print_editors(&db_conn)?; }, ("create-editor", Some(subm)) => { - let editor = fatcat::auth::create_editor( + let editor = fatcat::api_helpers::create_editor( &db_conn, subm.value_of("username").unwrap().to_string(), subm.is_present("admin"), @@ -109,7 +109,7 @@ fn run() -> Result<()> { println!("{}", confectionary.create_token(&db_conn, editor_id, None)?); }, ("inspect-token", Some(subm)) => { - fatcat::auth::inspect_token(&db_conn, subm.value_of("token").unwrap())?; + 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())?; diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 43911868..983645d8 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -117,7 +117,7 @@ pub fn server() -> Result { let pool = diesel::r2d2::Pool::builder() .build(manager) .expect("Failed to create database pool."); - let confectionary = auth::AuthConfectionary::new(); + let confectionary = auth::AuthConfectionary::new_dummy(); Ok(api_server::Server { db_pool: pool, auth_confectionary: confectionary }) } -- cgit v1.2.3 From 156b10220a50f6f441e7484235e227316f26761e Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Fri, 28 Dec 2018 22:50:47 -0800 Subject: basic auth unittests --- rust/src/api_helpers.rs | 2 +- rust/src/auth.rs | 5 +---- rust/src/bin/fatcat-auth.rs | 6 +++++- rust/tests/test_auth.rs | 48 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 rust/tests/test_auth.rs (limited to 'rust/src/bin') diff --git a/rust/src/api_helpers.rs b/rust/src/api_helpers.rs index da208c0a..3c5a2e17 100644 --- a/rust/src/api_helpers.rs +++ b/rust/src/api_helpers.rs @@ -294,7 +294,7 @@ pub fn accept_editgroup(editgroup_id: FatCatId, conn: &DbConn) -> Result>) -> Result { - let _ed: EditorRow = editor::table - .find(&editor_id.to_uuid()) - .get_result(conn)?; + 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? diff --git a/rust/src/bin/fatcat-auth.rs b/rust/src/bin/fatcat-auth.rs index 4b90da74..5a8f0f98 100644 --- a/rust/src/bin/fatcat-auth.rs +++ b/rust/src/bin/fatcat-auth.rs @@ -106,7 +106,11 @@ fn run() -> Result<()> { }, ("create-token", Some(subm)) => { let editor_id = FatCatId::from_str(subm.value_of("editor-id").unwrap())?; - println!("{}", confectionary.create_token(&db_conn, editor_id, None)?); + // check that editor exists + let _ed: fatcat::database_models::EditorRow = fatcat::database_schema::editor::table + .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())?; diff --git a/rust/tests/test_auth.rs b/rust/tests/test_auth.rs new file mode 100644 index 00000000..45956036 --- /dev/null +++ b/rust/tests/test_auth.rs @@ -0,0 +1,48 @@ + +extern crate fatcat; +extern crate uuid; +extern crate chrono; + +use std::str::FromStr; +use chrono::prelude::*; +use fatcat::auth::*; +use fatcat::api_helpers::*; + +#[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(); + + // create token w/o expiration + c.create_token(editor_id, None).unwrap(); + + // create token w/ expiration + let tomorrow = Utc::now() + chrono::Duration::days(1); + c.create_token(editor_id, Some(tomorrow)).unwrap(); +} + + +#[test] +fn test_auth_db() { + // Test things that require database + + let server = fatcat::test_server().unwrap(); + let conn = server.db_pool.get().expect("db_pool error"); + let c = fatcat::auth::AuthConfectionary::new_dummy(); + let editor_id = FatCatId::from_str("aaaaaaaaaaaabkvkaaaaaaaaae").unwrap(); + + // create token + let token = c.create_token(editor_id, None).unwrap(); + + // 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); + + // verification should fail + assert!(c.parse_macaroon_token(&conn, &token).is_err()); +} -- cgit v1.2.3 From f9c15a4fc22cb87179e459a283146769e612a92b Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Mon, 31 Dec 2018 13:20:02 -0800 Subject: wire-up auth config via ENV --- rust/Cargo.lock | 1 + rust/Cargo.toml | 1 + rust/src/auth.rs | 20 +++++++++++++++----- rust/src/bin/fatcat-auth.rs | 39 ++++++++++++++++++++------------------- rust/src/bin/fatcat-export.rs | 18 +----------------- rust/src/lib.rs | 26 ++++++++++++++++++-------- 6 files changed, 56 insertions(+), 49 deletions(-) (limited to 'rust/src/bin') diff --git a/rust/Cargo.lock b/rust/Cargo.lock index b76367b0..817697be 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -380,6 +380,7 @@ dependencies = [ "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "macaroon 0.1.1 (git+https://github.com/bnewbold/libmacaroon-rs?branch=bnewbold-broken)", "num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", "sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index a20118d2..190177a6 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -22,6 +22,7 @@ regex = "1" lazy_static = "1.0" sha1 = { version = "0.6", features = ["std"] } macaroon = { git = "https://github.com/bnewbold/libmacaroon-rs", branch = "bnewbold-broken" } +rand = "*" # API server chrono = { version = "0.4", features = ["serde"] } diff --git a/rust/src/auth.rs b/rust/src/auth.rs index 0fe21ebe..450a19d6 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -40,22 +40,24 @@ pub struct AuthConfectionary { } impl AuthConfectionary { - pub fn new(location: String, identifier: String, key: Vec) -> AuthConfectionary { + 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()); - AuthConfectionary { + Ok(AuthConfectionary { location: location, identifier: identifier, key: key, root_keys: root_keys, - } + }) } pub fn new_dummy() -> AuthConfectionary { AuthConfectionary::new( "test.fatcat.wiki".to_string(), "dummy".to_string(), - DUMMY_KEY.to_vec()) + BASE64.encode(DUMMY_KEY), + ).unwrap() } pub fn create_token(&self, editor_id: FatCatId, expires: Option>) -> Result { @@ -180,7 +182,15 @@ impl AuthConfectionary { } } -pub fn revoke_tokens(conn: &DbConn, editor_id: FatCatId) -> Result<()>{ +pub fn create_key() -> String { + let mut key: Vec = vec![0; 32]; + for v in key.iter_mut() { + *v = rand::random() + } + BASE64.encode(&key) +} + +pub fn revoke_tokens(conn: &DbConn, editor_id: FatCatId) -> Result<()> { diesel::update(editor::table.filter(editor::id.eq(&editor_id.to_uuid()))) .set(editor::auth_epoch.eq(Utc::now())) .execute(conn)?; diff --git a/rust/src/bin/fatcat-auth.rs b/rust/src/bin/fatcat-auth.rs index 5a8f0f98..3240964f 100644 --- a/rust/src/bin/fatcat-auth.rs +++ b/rust/src/bin/fatcat-auth.rs @@ -14,12 +14,8 @@ extern crate serde_json; extern crate uuid; use clap::{App, SubCommand}; -use dotenv::dotenv; -use std::env; use diesel::prelude::*; -use diesel::r2d2::ConnectionManager; -use fatcat::ConnectionPool; use fatcat::errors::*; use fatcat::api_helpers::FatCatId; use std::str::FromStr; @@ -31,18 +27,6 @@ use std::str::FromStr; //use std::io::{BufReader, BufWriter}; -/// Instantiate a new API server with a pooled database connection -// TODO: copypasta from fatcat-export -pub fn database_worker_pool() -> Result { - dotenv().ok(); - let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); - let manager = ConnectionManager::::new(database_url); - let pool = diesel::r2d2::Pool::builder() - .build(manager) - .expect("Failed to create database pool."); - Ok(pool) -} - fn run() -> Result<()> { let m = App::new("fatcat-auth") .version(env!("CARGO_PKG_VERSION")) @@ -66,7 +50,7 @@ fn run() -> Result<()> { .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'" + --env-format 'outputs in a format that shells can source'" // TODO ) ) .subcommand( @@ -76,6 +60,13 @@ fn run() -> Result<()> { " '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 + ) + ) .subcommand( SubCommand::with_name("revoke-tokens") .about("Resets auth_epoch for a single editor (invalidating all existing tokens)") @@ -89,8 +80,18 @@ fn run() -> Result<()> { ) .get_matches(); - let db_conn = database_worker_pool()?.get().expect("database pool"); - let confectionary = fatcat::auth::AuthConfectionary::new_dummy(); + // First, the commands with no db or confectionary needed + match m.subcommand() { + ("create-key", Some(_subm)) => { + println!("{}", fatcat::auth::create_key()); + return Ok(()) + }, + _ => (), + } + + // Then the ones that do + 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)?; diff --git a/rust/src/bin/fatcat-export.rs b/rust/src/bin/fatcat-export.rs index ec66ed4c..e1b930fc 100644 --- a/rust/src/bin/fatcat-export.rs +++ b/rust/src/bin/fatcat-export.rs @@ -17,15 +17,10 @@ extern crate serde_json; extern crate uuid; use clap::{App, Arg}; -use dotenv::dotenv; -use std::env; -use diesel::prelude::*; -use diesel::r2d2::ConnectionManager; use fatcat::api_entity_crud::*; use fatcat::api_helpers::*; use fatcat::errors::*; -use fatcat::ConnectionPool; use fatcat_api_spec::models::*; use std::str::FromStr; use uuid::Uuid; @@ -59,17 +54,6 @@ struct IdentRow { redirect_id: Option, } -/// Instantiate a new API server with a pooled database connection -pub fn database_worker_pool() -> Result { - dotenv().ok(); - let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); - let manager = ConnectionManager::::new(database_url); - let pool = diesel::r2d2::Pool::builder() - .build(manager) - .expect("Failed to create database pool."); - Ok(pool) -} - macro_rules! generic_loop_work { ($fn_name:ident, $entity_model:ident) => { fn $fn_name( @@ -183,7 +167,7 @@ pub fn do_export( entity_type: ExportEntityType, redirects: bool, ) -> Result<()> { - let db_pool = database_worker_pool()?; + let db_pool = fatcat::database_worker_pool()?; let buf_input = BufReader::new(std::io::stdin()); let (row_sender, row_receiver) = channel::bounded(CHANNEL_BUFFER_LEN); let (output_sender, output_receiver) = channel::bounded(CHANNEL_BUFFER_LEN); diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 983645d8..233f8642 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -93,20 +93,29 @@ use dotenv::dotenv; use iron::middleware::AfterMiddleware; use iron::{Request, Response}; use std::env; +use auth::AuthConfectionary; #[cfg(feature = "postgres")] embed_migrations!("../migrations/"); pub type ConnectionPool = diesel::r2d2::Pool>; -/// Establish a direct database connection. Not currently used, but could be helpful for -/// single-threaded tests or utilities. -pub fn establish_connection() -> PgConnection { +/// Instantiate a new API server with a pooled database connection +pub fn database_worker_pool() -> Result { dotenv().ok(); - let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); - PgConnection::establish(&database_url) - .unwrap_or_else(|_| panic!("Error connecting to {}", database_url)) + let manager = ConnectionManager::::new(database_url); + let pool = diesel::r2d2::Pool::builder() + .build(manager) + .expect("Failed to create database pool."); + Ok(pool) +} + +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) } /// Instantiate a new API server with a pooled database connection @@ -117,7 +126,7 @@ pub fn server() -> Result { let pool = diesel::r2d2::Pool::builder() .build(manager) .expect("Failed to create database pool."); - let confectionary = auth::AuthConfectionary::new_dummy(); + let confectionary = env_confectionary()?; Ok(api_server::Server { db_pool: pool, auth_confectionary: confectionary }) } @@ -126,7 +135,8 @@ pub fn test_server() -> Result { let database_url = env::var("TEST_DATABASE_URL").expect("TEST_DATABASE_URL must be set"); env::set_var("DATABASE_URL", database_url); - let server = server()?; + let mut server = server()?; + server.auth_confectionary = AuthConfectionary::new_dummy(); let conn = server.db_pool.get().expect("db_pool error"); // run migrations; revert latest (dummy data); re-run latest -- 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') 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') 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') 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') 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