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/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'rust/src/lib.rs') diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 0bed3471..2a6c7203 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -11,7 +11,7 @@ extern crate futures; extern crate uuid; #[macro_use] extern crate hyper; -//extern crate swagger; +extern crate swagger; #[macro_use] extern crate error_chain; extern crate iron; @@ -24,6 +24,7 @@ extern crate regex; #[macro_use] extern crate lazy_static; extern crate sha1; +extern crate macaroon; pub mod api_entity_crud; pub mod api_helpers; @@ -31,6 +32,7 @@ pub mod api_server; pub mod api_wrappers; pub mod database_models; pub mod database_schema; +pub mod auth; pub mod errors { // Create the Error, ErrorKind, ResultExt, and Result types -- 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/lib.rs') 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/lib.rs') diff --git a/rust/src/api_server.rs b/rust/src/api_server.rs index af2a7e24..cbf5be21 100644 --- a/rust/src/api_server.rs +++ b/rust/src/api_server.rs @@ -2,6 +2,7 @@ use api_entity_crud::EntityCrud; use api_helpers::*; +use auth::*; use chrono; use database_models::*; use database_schema::*; @@ -41,6 +42,7 @@ macro_rules! entity_batch_handler { #[derive(Clone)] pub struct Server { pub db_pool: ConnectionPool, + pub auth_confectionary: AuthConfectionary, } pub fn get_release_files( diff --git a/rust/src/api_wrappers.rs b/rust/src/api_wrappers.rs index cf696d15..25b4fab1 100644 --- a/rust/src/api_wrappers.rs +++ b/rust/src/api_wrappers.rs @@ -2,6 +2,7 @@ use api_entity_crud::EntityCrud; use api_helpers::*; +use auth::*; use api_server::Server; use database_models::EntityEditRow; use diesel::Connection; @@ -80,10 +81,12 @@ macro_rules! wrap_entity_handlers { &self, entity: models::$model, editgroup_id: Option, - _context: &Context, + context: &Context, ) -> Box + Send> { let conn = self.db_pool.get().expect("db_pool error"); let ret = match conn.transaction(|| { + let auth_context = self.auth_confectionary.parse_swagger(&conn, &context.auth_data)?; + // XXX: auth_context.expect("not authorized"); let editgroup_id = if let Some(s) = editgroup_id { Some(FatCatId::from_str(&s)?) } else { None }; diff --git a/rust/src/auth.rs b/rust/src/auth.rs index 580cde28..c188233a 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -1,208 +1,151 @@ //! Editor bearer token authentication -use swagger::auth::{AuthData, Authorization, Scopes}; +use swagger::auth::AuthData; use macaroon::{Format, Macaroon, Verifier}; use data_encoding::BASE64; -use std::collections::BTreeSet; -use std::fmt; +use std::collections::{BTreeSet,HashMap}; use database_models::*; use database_schema::*; use api_helpers::*; use chrono::prelude::*; use diesel; -use iron; use diesel::prelude::*; use errors::*; -//use serde_json; use std::str::FromStr; -//use uuid::Uuid; // 32 bytes max (!) static DUMMY_KEY: &[u8] = b"dummy-key-a-one-two-three-a-la"; -#[derive(Debug)] -pub struct OpenAuthMiddleware; - -#[derive(Debug)] -pub struct AuthError { - msg: String, +#[derive(Clone)] +pub struct AuthContext { + pub editor_id: FatCatId, + editor_row: EditorRow, + roles: Vec, // TODO: BTreeSet } -impl fmt::Display for AuthError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "AuthError: {}", &self.msg) - } -} +impl AuthContext { -impl iron::Error for AuthError { - fn description(&self) -> &str { - &self.msg - } - fn cause(&self) -> Option<&iron::Error> { - None + pub fn has_role(&self, role: &str) -> bool { + self.roles.contains(&role.to_string()) || self.roles.contains(&"admin".to_string()) } } -/* -#[derive(Debug)] -pub struct FatcatAuthBakery { - root_key_store: bool, // hashmap - signing_key: bool, // string name +#[derive(Clone)] +pub struct AuthConfectionary { + pub root_keys: HashMap, } -// impl: -// - new() -// - verify(&str) -> Result -*/ - -fn new_auth_ironerror(m: &str) -> iron::error::IronError { - iron::error::IronError::new( - AuthError { msg: m.to_string() }, - (iron::status::BadRequest, m.to_string()) - ) -} - -impl OpenAuthMiddleware { - /// Create a middleware that authorizes with the configured subject. - pub fn new() -> OpenAuthMiddleware { - OpenAuthMiddleware - } -} -impl iron::middleware::BeforeMiddleware for OpenAuthMiddleware { - fn before(&self, req: &mut iron::Request) -> iron::IronResult<()> { - req.extensions.insert::(Authorization { - subject: "undefined".to_string(), - scopes: Scopes::All, - issuer: None, - }); - Ok(()) +impl AuthConfectionary { + pub fn new() -> AuthConfectionary { + AuthConfectionary { + root_keys: HashMap::new(), + } } -} -#[derive(Debug)] -pub struct MacaroonAuthMiddleware; - -impl MacaroonAuthMiddleware { + pub fn create_token(&self, conn: &DbConn, editor_id: FatCatId, expires: Option>) -> Result { + let _ed: EditorRow = editor::table + .find(&editor_id.to_uuid()) + .get_result(conn)?; + let mut mac = Macaroon::create("fatcat.wiki", DUMMY_KEY, "dummy-key").expect("Macaroon creation"); + mac.add_first_party_caveat(&format!("editor_id = {}", editor_id.to_string())); + // TODO: put created one second in the past to prevent timing synchronization glitches? + let now = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true); + mac.add_first_party_caveat(&format!("created = {}", now)); + if let Some(expires) = expires { + mac.add_first_party_caveat(&format!("expires = {:?}", + &expires.to_rfc3339_opts(SecondsFormat::Secs, true))); + }; + let raw = mac.serialize(Format::V2).expect("macaroon serialization"); + Ok(BASE64.encode(&raw)) + } - pub fn new() -> MacaroonAuthMiddleware { - macaroon::initialize().unwrap(); - MacaroonAuthMiddleware + /// On success, returns Some((editor_id, scopes)), where `scopes` is a vector of strings. + pub fn parse_macaroon_token(&self, conn: &DbConn, s: &str) -> Result { + let raw = BASE64.decode(s.as_bytes())?; + let mac = match Macaroon::deserialize(&raw) { + Ok(m) => m, + Err(e) => bail!("macaroon deserialize error: {:?}", e), + }; + let mac = match mac.validate() { + Ok(m) => m, + Err(e) => bail!("macaroon validate error: {:?}", e), + }; + let mut verifier = Verifier::new(); + let mut editor_id: Option = None; + for caveat in mac.first_party_caveats() { + if caveat.predicate().starts_with("editor_id = ") { + editor_id = Some(FatCatId::from_str(caveat.predicate().get(12..).unwrap())?); + break + } + } + let editor_id = editor_id.expect("expected an editor_id caveat"); + verifier.satisfy_exact(&format!("editor_id = {}", editor_id.to_string())); + let mut created: Option> = None; + for caveat in mac.first_party_caveats() { + if caveat.predicate().starts_with("created = ") { + created = Some(DateTime::parse_from_rfc3339(caveat.predicate().get(10..).unwrap()) + .unwrap() + .with_timezone(&Utc)); + break + } + } + let created = created.expect("expected a 'created' caveat"); + verifier.satisfy_exact(&format!("created = {}", created.to_rfc3339_opts(SecondsFormat::Secs, true))); + let editor: EditorRow = editor::table + .find(&editor_id.to_uuid()) + .get_result(conn)?; + let auth_epoch = DateTime::::from_utc(editor.auth_epoch, Utc); + if created < auth_epoch { + bail!("token created before current auth_epoch (was probably revoked by editor)") + } + verifier.satisfy_general(|p: &str| -> bool { + // not expired (based on expires) + if p.starts_with("expires = ") { + let expires: DateTime = DateTime::parse_from_rfc3339(p.get(12..).unwrap()) + .unwrap() + .with_timezone(&Utc); + expires < Utc::now() + } else { + false + } + }); + if !mac.verify_signature(DUMMY_KEY) { + bail!("token signature verification failed"); + }; + match mac.verify(DUMMY_KEY, &mut verifier) { + Ok(true) => (), + Ok(false) => bail!("token overall verification failed"), + Err(e) => bail!("token parsing failed: {:?}", e), + } + Ok(editor) } -} -impl iron::middleware::BeforeMiddleware for MacaroonAuthMiddleware { - fn before(&self, req: &mut iron::Request) -> iron::IronResult<()> { + pub fn parse_swagger(&self, conn: &DbConn, auth_data: &Option) -> Result> { - let res: Option<(String, Vec)> = match req.extensions.get::() { + let token: Option = match auth_data { Some(AuthData::ApiKey(header)) => { let header: Vec = header.split_whitespace().map(|s| s.to_string()).collect(); if !(header.len() == 2 && header[0] == "Bearer") { - return Err(new_auth_ironerror("invalid bearer auth HTTP Header")); + bail!("invalid Bearer Auth HTTP header"); } - sniff_macaroon_token(&header[1]).expect("valid macaroon") + Some(header[1].clone()) }, None => None, - _ => { - return Err(new_auth_ironerror("auth HTTP Header should be empty or API key")); - } + _ => bail!("Authentication HTTP Header should either be empty or a Beaerer API key"), }; - if let Some((editor_id, scopes)) = res { - let mut scope_set = BTreeSet::new(); - for s in scopes { - scope_set.insert(s); - } - req.extensions.insert::(Authorization { - subject: editor_id, - scopes: Scopes::Some(scope_set), - issuer: None, - }); + let token = match token { + Some(t) => t, + None => return Ok(None), }; - Ok(()) - } -} - -/// Just checks signature and expired time; can't hit database, so nothing else -pub fn sniff_macaroon_token(s: &str) -> Result)>> { - let raw = BASE64.decode(s.as_bytes())?; - let mac = match Macaroon::deserialize(&raw) { - Ok(m) => m, - Err(e) => bail!("macaroon deserialize error: {:?}", e), - }; - let mac = match mac.validate() { - Ok(m) => m, - Err(e) => bail!("macaroon validate error: {:?}", e), - }; - let mut editor_id: Option = None; - for caveat in mac.first_party_caveats() { - if caveat.predicate().starts_with("editor_id = ") { - editor_id = Some(FatCatId::from_str(caveat.predicate().get(12..).unwrap())?); - break - } - } - let editor_id = editor_id.expect("expected an editor_id caveat"); - Ok(Some((editor_id.to_string(), vec![]))) -} - -/// On success, returns Some((editor_id, scopes)), where `scopes` is a vector of strings. -pub fn parse_macaroon_token(conn: &DbConn, s: &str) -> Result)>> { - let raw = BASE64.decode(s.as_bytes())?; - let mac = match Macaroon::deserialize(&raw) { - Ok(m) => m, - Err(e) => bail!("macaroon deserialize error: {:?}", e), - }; - let mac = match mac.validate() { - Ok(m) => m, - Err(e) => bail!("macaroon validate error: {:?}", e), - }; - let mut verifier = Verifier::new(); - let mut editor_id: Option = None; - for caveat in mac.first_party_caveats() { - if caveat.predicate().starts_with("editor_id = ") { - editor_id = Some(FatCatId::from_str(caveat.predicate().get(12..).unwrap())?); - break - } - } - let editor_id = editor_id.expect("expected an editor_id caveat"); - verifier.satisfy_exact(&format!("editor_id = {}", editor_id.to_string())); - let mut created: Option> = None; - for caveat in mac.first_party_caveats() { - if caveat.predicate().starts_with("created = ") { - created = Some(DateTime::parse_from_rfc3339(caveat.predicate().get(10..).unwrap()) - .unwrap() - .with_timezone(&Utc)); - break - } - } - let created = created.expect("expected a 'created' caveat"); - verifier.satisfy_exact(&format!("created = {}", created.to_rfc3339_opts(SecondsFormat::Secs, true))); - let editor: EditorRow = editor::table - .find(&editor_id.to_uuid()) - .get_result(conn)?; - let auth_epoch = DateTime::::from_utc(editor.auth_epoch, Utc); - if created < auth_epoch { - bail!("token created before current auth_epoch (was probably revoked by editor)") + let editor_row = self.parse_macaroon_token(conn, &token)?; + let roles = if editor_row.is_admin { vec!["admin".to_string()] } else { vec![] }; + Ok(Some(AuthContext { + editor_id: FatCatId::from_uuid(&editor_row.id), + editor_row: editor_row, + roles: roles, + })) } - verifier.satisfy_general(|p: &str| -> bool { - // not expired (based on expires) - if p.starts_with("expires = ") { - let expires: DateTime = DateTime::parse_from_rfc3339(p.get(12..).unwrap()) - .unwrap() - .with_timezone(&Utc); - expires < Utc::now() - } else { - false - } - }); - if !mac.verify_signature(DUMMY_KEY) { - bail!("token signature verification failed"); - }; - match mac.verify(DUMMY_KEY, &mut verifier) { - Ok(true) => (), - Ok(false) => bail!("token overall verification failed"), - Err(e) => bail!("token parsing failed: {:?}", e), - } - Ok(Some((editor_id.to_string(), vec![]))) } pub fn print_editors(conn: &DbConn) -> Result<()>{ @@ -236,23 +179,6 @@ pub fn create_editor(conn: &DbConn, username: String, is_admin: bool, is_bot: bo Ok(ed) } -pub fn create_token(conn: &DbConn, editor_id: FatCatId, expires: Option>) -> Result { - let _ed: EditorRow = editor::table - .find(&editor_id.to_uuid()) - .get_result(conn)?; - let mut mac = Macaroon::create("fatcat.wiki", DUMMY_KEY, "dummy-key").expect("Macaroon creation"); - mac.add_first_party_caveat(&format!("editor_id = {}", editor_id.to_string())); - // TODO: put created one second in the past to prevent timing synchronization glitches? - let now = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true); - mac.add_first_party_caveat(&format!("created = {}", now)); - if let Some(expires) = expires { - mac.add_first_party_caveat(&format!("expires = {:?}", - &expires.to_rfc3339_opts(SecondsFormat::Secs, true))); - }; - let raw = mac.serialize(Format::V2).expect("macaroon serialization"); - Ok(BASE64.encode(&raw)) -} - pub fn inspect_token(conn: &DbConn, token: &str) -> Result<()> { let raw = BASE64.decode(token.as_bytes())?; let mac = match Macaroon::deserialize(&raw) { @@ -266,8 +192,9 @@ pub fn inspect_token(conn: &DbConn, token: &str) -> Result<()> { for caveat in mac.first_party_caveats() { println!("caveat: {}", caveat.predicate()); } + let ac = AuthConfectionary::new(); // TODO: don't display full stacktrace on failure - println!("verify: {:?}", parse_macaroon_token(conn, token)); + println!("verify: {:?}", ac.parse_macaroon_token(conn, token)); Ok(()) } diff --git a/rust/src/bin/fatcat-auth.rs b/rust/src/bin/fatcat-auth.rs index 9b4550d8..f11f7a67 100644 --- a/rust/src/bin/fatcat-auth.rs +++ b/rust/src/bin/fatcat-auth.rs @@ -90,6 +90,7 @@ fn run() -> Result<()> { .get_matches(); let db_conn = database_worker_pool()?.get().expect("database pool"); + let confectionary = fatcat::auth::AuthConfectionary::new(); match m.subcommand() { ("list-editors", Some(_subm)) => { fatcat::auth::print_editors(&db_conn)?; @@ -105,7 +106,7 @@ fn run() -> Result<()> { }, ("create-token", Some(subm)) => { let editor_id = FatCatId::from_str(subm.value_of("editor-id").unwrap())?; - println!("{}", fatcat::auth::create_token(&db_conn, editor_id, None)?); + println!("{}", confectionary.create_token(&db_conn, editor_id, None)?); }, ("inspect-token", Some(subm)) => { fatcat::auth::inspect_token(&db_conn, subm.value_of("token").unwrap())?; diff --git a/rust/src/bin/fatcatd.rs b/rust/src/bin/fatcatd.rs index e14296da..7def7f66 100644 --- a/rust/src/bin/fatcatd.rs +++ b/rust/src/bin/fatcatd.rs @@ -78,7 +78,6 @@ fn main() { // authentication chain.link_before(fatcat_api_spec::server::ExtractAuthData); - chain.link_before(fatcat::auth::OpenAuthMiddleware::new()); chain.link_after(fatcat::XClacksOverheadMiddleware); diff --git a/rust/src/database_models.rs b/rust/src/database_models.rs index 55ba7fb9..f6cca3e1 100644 --- a/rust/src/database_models.rs +++ b/rust/src/database_models.rs @@ -572,7 +572,7 @@ impl EditgroupRow { } } -#[derive(Debug, Queryable, Identifiable, Associations, AsChangeset)] +#[derive(Debug, Clone, Queryable, Identifiable, Associations, AsChangeset)] #[table_name = "editor"] pub struct EditorRow { pub id: Uuid, diff --git a/rust/src/lib.rs b/rust/src/lib.rs index f081be25..43911868 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -117,7 +117,8 @@ pub fn server() -> Result { let pool = diesel::r2d2::Pool::builder() .build(manager) .expect("Failed to create database pool."); - Ok(api_server::Server { db_pool: pool }) + let confectionary = auth::AuthConfectionary::new(); + Ok(api_server::Server { db_pool: pool, auth_confectionary: confectionary }) } pub fn test_server() -> Result { -- cgit v1.2.3 From 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/lib.rs') 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 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/lib.rs') 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/lib.rs') diff --git a/rust/src/api_helpers.rs b/rust/src/api_helpers.rs index 3c5a2e17..7478da9d 100644 --- a/rust/src/api_helpers.rs +++ b/rust/src/api_helpers.rs @@ -205,23 +205,23 @@ fn test_hide_flags() { pub fn make_edit_context( conn: &DbConn, + editor_id: FatCatId, editgroup_id: Option, autoaccept: bool, ) -> Result { - let editor_id = Uuid::parse_str("00000000-0000-0000-AAAA-000000000001")?; // TODO: auth let editgroup_id: FatCatId = match (editgroup_id, autoaccept) { (Some(eg), _) => eg, // If autoaccept and no editgroup_id passed, always create a new one for this transaction (None, true) => { let eg_row: EditgroupRow = diesel::insert_into(editgroup::table) - .values((editgroup::editor_id.eq(editor_id),)) + .values((editgroup::editor_id.eq(editor_id.to_uuid()),)) .get_result(conn)?; FatCatId::from_uuid(&eg_row.id) } - (None, false) => FatCatId::from_uuid(&get_or_create_editgroup(editor_id, conn)?), + (None, false) => FatCatId::from_uuid(&get_or_create_editgroup(editor_id.to_uuid(), conn)?), }; Ok(EditContext { - editor_id: FatCatId::from_uuid(&editor_id), + editor_id: editor_id, editgroup_id: editgroup_id, extra_json: None, autoaccept: autoaccept, @@ -229,7 +229,12 @@ pub fn make_edit_context( } // TODO: verify username (alphanum, etc) -pub fn create_editor(conn: &DbConn, username: String, is_admin: bool, is_bot: bool) -> Result { +pub fn create_editor( + conn: &DbConn, + username: String, + is_admin: bool, + is_bot: bool, +) -> Result { let ed: EditorRow = diesel::insert_into(editor::table) .values(( editor::username.eq(username), @@ -237,7 +242,7 @@ pub fn create_editor(conn: &DbConn, username: String, is_admin: bool, is_bot: bo editor::is_bot.eq(is_bot), )) .get_result(conn)?; - Ok(ed) + Ok(ed) } /// This function should always be run within a transaction diff --git a/rust/src/api_server.rs b/rust/src/api_server.rs index cbf5be21..853f7bc2 100644 --- a/rust/src/api_server.rs +++ b/rust/src/api_server.rs @@ -20,11 +20,12 @@ macro_rules! entity_batch_handler { &self, entity_list: &[models::$model], autoaccept: bool, + editor_id: FatCatId, editgroup_id: Option, conn: &DbConn, ) -> Result> { - let edit_context = make_edit_context(conn, editgroup_id, autoaccept)?; + let edit_context = make_edit_context(conn, editor_id, editgroup_id, autoaccept)?; edit_context.check(&conn)?; let model_list: Vec<&models::$model> = entity_list.iter().map(|e| e).collect(); let edits = $model::db_create_batch(conn, &edit_context, model_list.as_slice())?; diff --git a/rust/src/api_wrappers.rs b/rust/src/api_wrappers.rs index 25b4fab1..ae070e02 100644 --- a/rust/src/api_wrappers.rs +++ b/rust/src/api_wrappers.rs @@ -2,8 +2,8 @@ use api_entity_crud::EntityCrud; use api_helpers::*; -use auth::*; use api_server::Server; +use auth::*; use database_models::EntityEditRow; use diesel::Connection; use errors::*; @@ -85,12 +85,14 @@ macro_rules! wrap_entity_handlers { ) -> Box + Send> { let conn = self.db_pool.get().expect("db_pool error"); let ret = match conn.transaction(|| { - let auth_context = self.auth_confectionary.parse_swagger(&conn, &context.auth_data)?; - // XXX: auth_context.expect("not authorized"); + let auth_context = self.auth_confectionary.require_auth(&conn, &context.auth_data)?; + auth_context.require_role(FatcatRole::Editor)?; let editgroup_id = if let Some(s) = editgroup_id { - Some(FatCatId::from_str(&s)?) + let eg_id = FatCatId::from_str(&s)?; + auth_context.require_editgroup(&conn, eg_id)?; + Some(eg_id) } else { None }; - let edit_context = make_edit_context(&conn, editgroup_id, false)?; + let edit_context = make_edit_context(&conn, auth_context.editor_id, editgroup_id, false)?; edit_context.check(&conn)?; entity.db_create(&conn, &edit_context)?.into_model() }) { @@ -111,6 +113,11 @@ macro_rules! wrap_entity_handlers { $post_resp::BadRequest(ErrorResponse { message: e.to_string() }), Err(Error(ErrorKind::EditgroupAlreadyAccepted(e), _)) => $post_resp::BadRequest(ErrorResponse { message: e.to_string() }), + Err(Error(ErrorKind::InvalidCredentials(e), _)) => + // TODO: why can't I NotAuthorized here? + $post_resp::Forbidden(ErrorResponse { message: e.to_string() }), + Err(Error(ErrorKind::InsufficientPrivileges(e), _)) => + $post_resp::Forbidden(ErrorResponse { message: e.to_string() }), Err(Error(ErrorKind::OtherBadRequest(e), _)) => $post_resp::BadRequest(ErrorResponse { message: e.to_string() }), Err(e) => { @@ -126,14 +133,18 @@ macro_rules! wrap_entity_handlers { entity_list: &Vec, autoaccept: Option, editgroup_id: Option, - _context: &Context, + context: &Context, ) -> Box + Send> { let conn = self.db_pool.get().expect("db_pool error"); let ret = match conn.transaction(|| { + let auth_context = self.auth_confectionary.require_auth(&conn, &context.auth_data)?; + auth_context.require_role(FatcatRole::Editor)?; let editgroup_id = if let Some(s) = editgroup_id { - Some(FatCatId::from_str(&s)?) + let eg_id = FatCatId::from_str(&s)?; + auth_context.require_editgroup(&conn, eg_id)?; + Some(eg_id) } else { None }; - self.$post_batch_handler(entity_list, autoaccept.unwrap_or(false), editgroup_id, &conn) + self.$post_batch_handler(entity_list, autoaccept.unwrap_or(false), auth_context.editor_id, editgroup_id, &conn) }) { Ok(edit) => $post_batch_resp::CreatedEntities(edit), @@ -152,6 +163,11 @@ macro_rules! wrap_entity_handlers { $post_batch_resp::BadRequest(ErrorResponse { message: e.to_string() }), Err(Error(ErrorKind::EditgroupAlreadyAccepted(e), _)) => $post_batch_resp::BadRequest(ErrorResponse { message: e.to_string() }), + Err(Error(ErrorKind::InvalidCredentials(e), _)) => + // TODO: why can't I NotAuthorized here? + $post_batch_resp::Forbidden(ErrorResponse { message: e.to_string() }), + Err(Error(ErrorKind::InsufficientPrivileges(e), _)) => + $post_batch_resp::Forbidden(ErrorResponse { message: e.to_string() }), Err(Error(ErrorKind::OtherBadRequest(e), _)) => $post_batch_resp::BadRequest(ErrorResponse { message: e.to_string() }), Err(e) => { @@ -167,15 +183,19 @@ macro_rules! wrap_entity_handlers { ident: String, entity: models::$model, editgroup_id: Option, - _context: &Context, + context: &Context, ) -> Box + Send> { let conn = self.db_pool.get().expect("db_pool error"); let ret = match conn.transaction(|| { + let auth_context = self.auth_confectionary.require_auth(&conn, &context.auth_data)?; + auth_context.require_role(FatcatRole::Editor)?; let entity_id = FatCatId::from_str(&ident)?; let editgroup_id = if let Some(s) = editgroup_id { - Some(FatCatId::from_str(&s)?) + let eg_id = FatCatId::from_str(&s)?; + auth_context.require_editgroup(&conn, eg_id)?; + Some(eg_id) } else { None }; - let edit_context = make_edit_context(&conn, editgroup_id, false)?; + let edit_context = make_edit_context(&conn, auth_context.editor_id, editgroup_id, false)?; edit_context.check(&conn)?; entity.db_update(&conn, &edit_context, entity_id)?.into_model() }) { @@ -202,6 +222,11 @@ macro_rules! wrap_entity_handlers { $update_resp::BadRequest(ErrorResponse { message: e.to_string() }), Err(Error(ErrorKind::OtherBadRequest(e), _)) => $update_resp::BadRequest(ErrorResponse { message: e.to_string() }), + Err(Error(ErrorKind::InvalidCredentials(e), _)) => + // TODO: why can't I NotAuthorized here? + $update_resp::Forbidden(ErrorResponse { message: e.to_string() }), + Err(Error(ErrorKind::InsufficientPrivileges(e), _)) => + $update_resp::Forbidden(ErrorResponse { message: e.to_string() }), Err(e) => { error!("{}", e); $update_resp::GenericError(ErrorResponse { message: e.to_string() }) @@ -214,16 +239,22 @@ macro_rules! wrap_entity_handlers { &self, ident: String, editgroup_id: Option, - _context: &Context, + context: &Context, ) -> Box + Send> { let conn = self.db_pool.get().expect("db_pool error"); let ret = match conn.transaction(|| { + let auth_context = self.auth_confectionary.require_auth(&conn, &context.auth_data)?; + auth_context.require_role(FatcatRole::Editor)?; let entity_id = FatCatId::from_str(&ident)?; let editgroup_id: Option = match editgroup_id { - Some(s) => Some(FatCatId::from_str(&s)?), + Some(s) => { + let editgroup_id = FatCatId::from_str(&s)?; + auth_context.require_editgroup(&conn, editgroup_id)?; + Some(editgroup_id) + }, None => None, }; - let edit_context = make_edit_context(&conn, editgroup_id, false)?; + let edit_context = make_edit_context(&conn, auth_context.editor_id, editgroup_id, false)?; edit_context.check(&conn)?; $model::db_delete(&conn, &edit_context, entity_id)?.into_model() }) { @@ -246,6 +277,11 @@ macro_rules! wrap_entity_handlers { $delete_resp::BadRequest(ErrorResponse { message: e.to_string() }), Err(Error(ErrorKind::OtherBadRequest(e), _)) => $delete_resp::BadRequest(ErrorResponse { message: e.to_string() }), + Err(Error(ErrorKind::InvalidCredentials(e), _)) => + // TODO: why can't I NotAuthorized here? + $delete_resp::Forbidden(ErrorResponse { message: e.to_string() }), + Err(Error(ErrorKind::InsufficientPrivileges(e), _)) => + $delete_resp::Forbidden(ErrorResponse { message: e.to_string() }), Err(e) => { error!("{}", e); $delete_resp::GenericError(ErrorResponse { message: e.to_string() }) @@ -356,11 +392,15 @@ macro_rules! wrap_entity_handlers { fn $delete_edit_fn( &self, edit_id: String, - _context: &Context, + context: &Context, ) -> Box + Send> { let conn = self.db_pool.get().expect("db_pool error"); let ret = match conn.transaction(|| { let edit_id = Uuid::from_str(&edit_id)?; + let auth_context = self.auth_confectionary.require_auth(&conn, &context.auth_data)?; + auth_context.require_role(FatcatRole::Editor)?; + let edit = $model::db_get_edit(&conn, edit_id)?; + auth_context.require_editgroup(&conn, FatCatId::from_uuid(&edit.editgroup_id))?; $model::db_delete_edit(&conn, edit_id) }) { Ok(()) => @@ -373,6 +413,11 @@ macro_rules! wrap_entity_handlers { $delete_edit_resp::BadRequest(ErrorResponse { message: e.to_string() }), Err(Error(ErrorKind::OtherBadRequest(e), _)) => $delete_edit_resp::BadRequest(ErrorResponse { message: e.to_string() }), + Err(Error(ErrorKind::InvalidCredentials(e), _)) => + // TODO: why can't I NotAuthorized here? + $delete_edit_resp::Forbidden(ErrorResponse { message: e.to_string() }), + Err(Error(ErrorKind::InsufficientPrivileges(e), _)) => + $delete_edit_resp::Forbidden(ErrorResponse { message: e.to_string() }), Err(e) => { error!("{}", e); $delete_edit_resp::GenericError(ErrorResponse { message: e.to_string() }) @@ -862,11 +907,17 @@ impl Api for Server { fn accept_editgroup( &self, editgroup_id: String, - _context: &Context, + context: &Context, ) -> Box + Send> { let conn = self.db_pool.get().expect("db_pool error"); let ret = match conn.transaction(|| { let editgroup_id = FatCatId::from_str(&editgroup_id)?; + let auth_context = self + .auth_confectionary + .require_auth(&conn, &context.auth_data)?; + auth_context.require_role(FatcatRole::Admin)?; + // NOTE: this is currently redundant, but zero-cost + auth_context.require_editgroup(&conn, editgroup_id)?; self.accept_editgroup_handler(editgroup_id, &conn) }) { Ok(()) => AcceptEditgroupResponse::MergedSuccessfully(Success { @@ -882,6 +933,16 @@ impl Api for Server { message: ErrorKind::EditgroupAlreadyAccepted(e).to_string(), }) } + Err(Error(ErrorKind::InvalidCredentials(e), _)) => { + AcceptEditgroupResponse::Forbidden(ErrorResponse { + message: e.to_string(), + }) + } + Err(Error(ErrorKind::InsufficientPrivileges(e), _)) => { + AcceptEditgroupResponse::Forbidden(ErrorResponse { + message: e.to_string(), + }) + } Err(e) => AcceptEditgroupResponse::GenericError(ErrorResponse { message: e.to_string(), }), @@ -919,11 +980,27 @@ impl Api for Server { fn create_editgroup( &self, entity: models::Editgroup, - _context: &Context, + context: &Context, ) -> Box + Send> { let conn = self.db_pool.get().expect("db_pool error"); - let ret = match conn.transaction(|| self.create_editgroup_handler(entity, &conn)) { + let ret = match conn.transaction(|| { + let auth_context = self + .auth_confectionary + .require_auth(&conn, &context.auth_data)?; + auth_context.require_role(FatcatRole::Editor)?; + self.create_editgroup_handler(entity, &conn) + }) { Ok(eg) => CreateEditgroupResponse::SuccessfullyCreated(eg), + Err(Error(ErrorKind::InvalidCredentials(e), _)) => { + CreateEditgroupResponse::Forbidden(ErrorResponse { + message: e.to_string(), + }) + } + Err(Error(ErrorKind::InsufficientPrivileges(e), _)) => { + CreateEditgroupResponse::Forbidden(ErrorResponse { + message: e.to_string(), + }) + } Err(e) => // TODO: dig in to error type here { diff --git a/rust/src/auth.rs b/rust/src/auth.rs index 450a19d6..ee3c6fb0 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -1,33 +1,76 @@ //! Editor bearer token authentication -use swagger::auth::AuthData; -use macaroon::{Format, Macaroon, Verifier}; use data_encoding::BASE64; +use macaroon::{Format, Macaroon, Verifier}; +use swagger::auth::AuthData; -use std::collections::HashMap; -use database_models::*; -use database_schema::*; use api_helpers::*; use chrono::prelude::*; +use database_models::*; +use database_schema::*; use diesel; use diesel::prelude::*; use errors::*; +use std::collections::HashMap; use std::str::FromStr; // 32 bytes max (!) static DUMMY_KEY: &[u8] = b"dummy-key-a-one-two-three-a-la"; +#[derive(Clone, Copy, Debug)] +pub enum FatcatRole { + Public, + Editor, + Bot, + Human, + Admin, +} + #[derive(Clone)] pub struct AuthContext { pub editor_id: FatCatId, editor_row: EditorRow, - roles: Vec, // TODO: BTreeSet } impl AuthContext { + pub fn has_role(&self, role: FatcatRole) -> bool { + if self.editor_row.is_admin { + return true; + } + match role { + FatcatRole::Public => true, + FatcatRole::Editor => true, + FatcatRole::Bot => self.editor_row.is_bot, + FatcatRole::Human => !self.editor_row.is_bot, + FatcatRole::Admin => self.editor_row.is_admin, + } + } - pub fn has_role(&self, role: &str) -> bool { - self.roles.contains(&role.to_string()) || self.roles.contains(&"admin".to_string()) + pub fn require_role(&self, role: FatcatRole) -> Result<()> { + match self.has_role(role) { + true => Ok(()), + // TODO: better message + false => Err(ErrorKind::InsufficientPrivileges( + "doesn't have required role".to_string(), + ) + .into()), + } + } + + pub fn require_editgroup(&self, conn: &DbConn, editgroup_id: FatCatId) -> Result<()> { + if self.has_role(FatcatRole::Admin) { + return Ok(()) + } + let editgroup: EditgroupRow = editgroup::table + .find(editgroup_id.to_uuid()) + .get_result(conn)?; + match editgroup.editor_id == self.editor_id.to_uuid() { + true => Ok(()), + false => Err(ErrorKind::InsufficientPrivileges( + "editor does not own this editgroup".to_string(), + ) + .into()), + } } } @@ -40,7 +83,11 @@ pub struct AuthConfectionary { } impl AuthConfectionary { - pub fn new(location: String, identifier: String, key_base64: String) -> Result { + pub fn new( + location: String, + identifier: String, + key_base64: String, + ) -> Result { let key = BASE64.decode(key_base64.as_bytes())?; let mut root_keys = HashMap::new(); root_keys.insert(identifier.clone(), key.clone()); @@ -57,18 +104,26 @@ impl AuthConfectionary { "test.fatcat.wiki".to_string(), "dummy".to_string(), BASE64.encode(DUMMY_KEY), - ).unwrap() + ) + .unwrap() } - pub fn create_token(&self, editor_id: FatCatId, expires: Option>) -> Result { - let mut mac = Macaroon::create(&self.location, &self.key, &self.identifier).expect("Macaroon creation"); + pub fn create_token( + &self, + editor_id: FatCatId, + expires: Option>, + ) -> Result { + let mut mac = Macaroon::create(&self.location, &self.key, &self.identifier) + .expect("Macaroon creation"); mac.add_first_party_caveat(&format!("editor_id = {}", editor_id.to_string())); // TODO: put created one second in the past to prevent timing synchronization glitches? let now = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true); mac.add_first_party_caveat(&format!("created = {}", now)); if let Some(expires) = expires { - mac.add_first_party_caveat(&format!("expires = {:?}", - &expires.to_rfc3339_opts(SecondsFormat::Secs, true))); + mac.add_first_party_caveat(&format!( + "expires = {:?}", + &expires.to_rfc3339_opts(SecondsFormat::Secs, true) + )); }; let raw = mac.serialize(Format::V2).expect("macaroon serialization"); Ok(BASE64.encode(&raw)) @@ -79,18 +134,30 @@ impl AuthConfectionary { let raw = BASE64.decode(s.as_bytes())?; let mac = match Macaroon::deserialize(&raw) { Ok(m) => m, - Err(e) => bail!("macaroon deserialize error: {:?}", e), + Err(_e) => { + // TODO: should be "chaining" here + //bail!("macaroon deserialize error: {:?}", e), + return Err( + ErrorKind::InvalidCredentials("macaroon deserialize error".to_string()).into(), + ); + } }; let mac = match mac.validate() { Ok(m) => m, - Err(e) => bail!("macaroon validate error: {:?}", e), + Err(_e) => { + // TODO: should be "chaining" here + //bail!("macaroon validate error: {:?}", e), + return Err( + ErrorKind::InvalidCredentials("macaroon validate error".to_string()).into(), + ); + } }; let mut verifier = Verifier::new(); let mut editor_id: Option = None; for caveat in mac.first_party_caveats() { if caveat.predicate().starts_with("editor_id = ") { editor_id = Some(FatCatId::from_str(caveat.predicate().get(12..).unwrap())?); - break + break; } } let editor_id = editor_id.expect("expected an editor_id caveat"); @@ -98,20 +165,27 @@ impl AuthConfectionary { let mut created: Option> = None; for caveat in mac.first_party_caveats() { if caveat.predicate().starts_with("created = ") { - created = Some(DateTime::parse_from_rfc3339(caveat.predicate().get(10..).unwrap()) - .unwrap() - .with_timezone(&Utc)); - break + created = Some( + DateTime::parse_from_rfc3339(caveat.predicate().get(10..).unwrap()) + .unwrap() + .with_timezone(&Utc), + ); + break; } } let created = created.expect("expected a 'created' caveat"); - verifier.satisfy_exact(&format!("created = {}", created.to_rfc3339_opts(SecondsFormat::Secs, true))); - let editor: EditorRow = editor::table - .find(&editor_id.to_uuid()) - .get_result(conn)?; + verifier.satisfy_exact(&format!( + "created = {}", + created.to_rfc3339_opts(SecondsFormat::Secs, true) + )); + let editor: EditorRow = editor::table.find(&editor_id.to_uuid()).get_result(conn)?; let auth_epoch = DateTime::::from_utc(editor.auth_epoch, Utc); if created < auth_epoch { - bail!("token created before current auth_epoch (was probably revoked by editor)") + return Err(ErrorKind::InvalidCredentials( + "token created before current auth_epoch (was probably revoked by editor)" + .to_string(), + ) + .into()); } verifier.satisfy_general(|p: &str| -> bool { // not expired (based on expires) @@ -126,42 +200,78 @@ impl AuthConfectionary { }); let verify_key = match self.root_keys.get(mac.identifier()) { Some(key) => key, - None => bail!("key not found for identifier: {}", mac.identifier()), + None => { + // TODO: better message + //bail!("key not found for identifier: {}", mac.identifier()), + return Err(ErrorKind::InvalidCredentials( + "key not found for identifier".to_string(), + ) + .into()); + } }; match mac.verify(verify_key, &mut verifier) { Ok(true) => (), - Ok(false) => bail!("token overall verification failed"), - Err(e) => bail!("token parsing failed: {:?}", e), + Ok(false) => { + return Err(ErrorKind::InvalidCredentials( + "token overall verification failed".to_string(), + ) + .into()); + } + Err(_e) => { + // TODO: chain + //bail!("token parsing failed: {:?}", e), + return Err( + ErrorKind::InvalidCredentials("token parsing failed".to_string()).into(), + ); + } } Ok(editor) } - pub fn parse_swagger(&self, conn: &DbConn, auth_data: &Option) -> Result> { - + pub fn parse_swagger( + &self, + conn: &DbConn, + auth_data: &Option, + ) -> Result> { let token: Option = match auth_data { Some(AuthData::ApiKey(header)) => { - let header: Vec = header.split_whitespace().map(|s| s.to_string()).collect(); + let header: Vec = + header.split_whitespace().map(|s| s.to_string()).collect(); if !(header.len() == 2 && header[0] == "Bearer") { - bail!("invalid Bearer Auth HTTP header"); + return Err(ErrorKind::InvalidCredentials( + "invalid Bearer Auth HTTP header".to_string(), + ) + .into()); } Some(header[1].clone()) - }, + } None => None, - _ => bail!("Authentication HTTP Header should either be empty or a Beaerer API key"), + _ => { + return Err(ErrorKind::InvalidCredentials( + "Authentication HTTP Header should either be empty or a Beaerer API key" + .to_string(), + ) + .into()); + } }; let token = match token { Some(t) => t, None => return Ok(None), }; let editor_row = self.parse_macaroon_token(conn, &token)?; - let roles = if editor_row.is_admin { vec!["admin".to_string()] } else { vec![] }; Ok(Some(AuthContext { editor_id: FatCatId::from_uuid(&editor_row.id), editor_row: editor_row, - roles: roles, })) } + pub fn require_auth(&self, conn: &DbConn, auth_data: &Option) -> Result { + match self.parse_swagger(conn, auth_data)? { + Some(auth) => Ok(auth), + None => Err(ErrorKind::InvalidCredentials("no token supplied".to_string()).into()), + } + } + // TODO: refactor out of this file? /// Only used from CLI tool pub fn inspect_token(&self, conn: &DbConn, token: &str) -> Result<()> { @@ -206,13 +316,13 @@ pub fn revoke_tokens_everyone(conn: &DbConn) -> Result<()> { // TODO: refactor out of this file? /// Only used from CLI tool -pub fn print_editors(conn: &DbConn) -> Result<()>{ +pub fn print_editors(conn: &DbConn) -> Result<()> { // iterate over all editors. format id, print flags, auth_epoch - let all_editors: Vec = editor::table - .load(conn)?; + let all_editors: Vec = editor::table.load(conn)?; println!("editor_id\t\t\tis_admin/is_bot\tauth_epoch\t\t\tusername\twrangler_id"); for e in all_editors { - println!("{}\t{}\t{}\t{}\t{}\t{:?}", + println!( + "{}\t{}\t{}\t{}\t{}\t{:?}", FatCatId::from_uuid(&e.id).to_string(), e.is_admin, e.is_bot, diff --git a/rust/src/bin/fatcat-auth.rs b/rust/src/bin/fatcat-auth.rs index 3240964f..addd2b66 100644 --- a/rust/src/bin/fatcat-auth.rs +++ b/rust/src/bin/fatcat-auth.rs @@ -8,16 +8,16 @@ extern crate dotenv; extern crate error_chain; extern crate fatcat; //#[macro_use] -extern crate log; extern crate env_logger; +extern crate log; extern crate serde_json; extern crate uuid; use clap::{App, SubCommand}; use diesel::prelude::*; -use fatcat::errors::*; use fatcat::api_helpers::FatCatId; +use fatcat::errors::*; use std::str::FromStr; //use uuid::Uuid; @@ -26,15 +26,13 @@ use std::str::FromStr; //use std::io::prelude::*; //use std::io::{BufReader, BufWriter}; - fn run() -> Result<()> { let m = App::new("fatcat-auth") .version(env!("CARGO_PKG_VERSION")) .author("Bryan Newbold ") .about("Editor authentication admin tool") .subcommand( - SubCommand::with_name("list-editors") - .about("Prints all currently registered editors") + SubCommand::with_name("list-editors").about("Prints all currently registered editors"), ) .subcommand( SubCommand::with_name("create-editor") @@ -42,41 +40,37 @@ fn run() -> Result<()> { .args_from_usage( " 'username for editor' --admin 'creates editor with admin privs' - --bot 'this editor is a bot'" - ) + --bot 'this editor is a bot'", + ), ) .subcommand( SubCommand::with_name("create-token") .about("Creates a new auth token (macaroon) for the given editor") .args_from_usage( " 'id of the editor (fatcatid, not username)' - --env-format 'outputs in a format that shells can source'" // TODO - ) + --env-format 'outputs in a format that shells can source'", // TODO + ), ) .subcommand( SubCommand::with_name("inspect-token") .about("Dumps token metadata (and whether it is valid)") - .args_from_usage( - " 'base64-encoded token (macaroon)'" - ) + .args_from_usage(" 'base64-encoded token (macaroon)'"), ) .subcommand( SubCommand::with_name("create-key") .about("Creates a new auth secret key (aka, root/signing key for tokens)") .args_from_usage( - "--env-format 'outputs in a format that shells can source'" // TODO - ) + "--env-format 'outputs in a format that shells can source'", // TODO + ), ) .subcommand( SubCommand::with_name("revoke-tokens") .about("Resets auth_epoch for a single editor (invalidating all existing tokens)") - .args_from_usage( - " 'identifier (fcid) of editor'" - ) + .args_from_usage(" 'identifier (fcid) of editor'"), ) .subcommand( SubCommand::with_name("revoke-tokens-everyone") - .about("Resets auth_epoch for all editors (invalidating tokens for all users!)") + .about("Resets auth_epoch for all editors (invalidating tokens for all users!)"), ) .get_matches(); @@ -84,27 +78,30 @@ fn run() -> Result<()> { match m.subcommand() { ("create-key", Some(_subm)) => { println!("{}", fatcat::auth::create_key()); - return Ok(()) - }, + return Ok(()); + } _ => (), } // Then the ones that do - let db_conn = fatcat::database_worker_pool()?.get().expect("database pool"); + let db_conn = fatcat::database_worker_pool()? + .get() + .expect("database pool"); let confectionary = fatcat::env_confectionary()?; match m.subcommand() { ("list-editors", Some(_subm)) => { fatcat::auth::print_editors(&db_conn)?; - }, + } ("create-editor", Some(subm)) => { let editor = fatcat::api_helpers::create_editor( &db_conn, subm.value_of("username").unwrap().to_string(), subm.is_present("admin"), - subm.is_present("bot"))?; + subm.is_present("bot"), + )?; //println!("{:?}", editor); println!("{}", FatCatId::from_uuid(&editor.id).to_string()); - }, + } ("create-token", Some(subm)) => { let editor_id = FatCatId::from_str(subm.value_of("editor-id").unwrap())?; // check that editor exists @@ -112,19 +109,19 @@ fn run() -> Result<()> { .find(&editor_id.to_uuid()) .get_result(&db_conn)?; println!("{}", confectionary.create_token(editor_id, None)?); - }, + } ("inspect-token", Some(subm)) => { confectionary.inspect_token(&db_conn, subm.value_of("token").unwrap())?; - }, + } ("revoke-tokens", Some(subm)) => { let editor_id = FatCatId::from_str(subm.value_of("editor-id").unwrap())?; fatcat::auth::revoke_tokens(&db_conn, editor_id)?; println!("success!"); - }, + } ("revoke-tokens-everyone", Some(_subm)) => { fatcat::auth::revoke_tokens_everyone(&db_conn)?; println!("success!"); - }, + } _ => { println!("Missing or unimplemented command!"); println!("{}", m.usage()); diff --git a/rust/src/bin/fatcatd.rs b/rust/src/bin/fatcatd.rs index 7def7f66..7d77d90b 100644 --- a/rust/src/bin/fatcatd.rs +++ b/rust/src/bin/fatcatd.rs @@ -21,7 +21,6 @@ use iron::{status, Chain, Iron, IronResult, Request, Response}; use iron_slog::{DefaultLogFormatter, LoggerMiddleware}; use slog::{Drain, Logger}; - /// Create custom server, wire it to the autogenerated router, /// and pass it to the web server. fn main() { diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 233f8642..a31404da 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -22,16 +22,16 @@ extern crate data_encoding; extern crate regex; #[macro_use] extern crate lazy_static; -extern crate sha1; extern crate macaroon; +extern crate sha1; pub mod api_entity_crud; pub mod api_helpers; pub mod api_server; pub mod api_wrappers; +pub mod auth; pub mod database_models; pub mod database_schema; -pub mod auth; pub mod errors { // Create the Error, ErrorKind, ResultExt, and Result types @@ -74,6 +74,14 @@ pub mod errors { description("Invalid Entity State Transform") display("tried to mutate an entity which was not in an appropriate state: {}", message) } + InvalidCredentials(message: String) { + description("auth token was missing, expired, revoked, or corrupt") + display("auth token was missing, expired, revoked, or corrupt: {}", message) + } + InsufficientPrivileges(message: String) { + description("editor account doesn't have authorization") + display("editor account doesn't have authorization: {}", message) + } OtherBadRequest(message: String) { description("catch-all error for bad or unallowed requests") display("broke a constraint or made an otherwise invalid request: {}", message) @@ -86,14 +94,13 @@ pub mod errors { pub use errors::*; pub use self::errors::*; +use auth::AuthConfectionary; use diesel::pg::PgConnection; -use diesel::prelude::*; use diesel::r2d2::ConnectionManager; use dotenv::dotenv; use iron::middleware::AfterMiddleware; use iron::{Request, Response}; use std::env; -use auth::AuthConfectionary; #[cfg(feature = "postgres")] embed_migrations!("../migrations/"); @@ -127,7 +134,10 @@ pub fn server() -> Result { .build(manager) .expect("Failed to create database pool."); let confectionary = env_confectionary()?; - Ok(api_server::Server { db_pool: pool, auth_confectionary: confectionary }) + Ok(api_server::Server { + db_pool: pool, + auth_confectionary: confectionary, + }) } pub fn test_server() -> Result { diff --git a/rust/tests/test_auth.rs b/rust/tests/test_auth.rs index 5b04d595..8d20dafd 100644 --- a/rust/tests/test_auth.rs +++ b/rust/tests/test_auth.rs @@ -1,17 +1,16 @@ - +extern crate chrono; extern crate fatcat; extern crate uuid; -extern crate chrono; -use std::str::FromStr; use chrono::prelude::*; -use fatcat::auth::*; use fatcat::api_helpers::*; +use fatcat::auth::*; +use std::str::FromStr; #[test] fn test_macaroons() { // Test everything we can without connecting to database - + let c = fatcat::auth::AuthConfectionary::new_dummy(); let editor_id = FatCatId::from_str("q3nouwy3nnbsvo3h5klxsx4a7y").unwrap(); @@ -23,7 +22,6 @@ fn test_macaroons() { c.create_token(editor_id, Some(tomorrow)).unwrap(); } - #[test] fn test_auth_db() { // Test things that require database @@ -39,7 +37,7 @@ fn test_auth_db() { // verify token let editor_row = c.parse_macaroon_token(&conn, &token).unwrap(); assert_eq!(editor_row.id, editor_id.to_uuid()); - + // revoke token revoke_tokens(&conn, editor_id).unwrap(); -- cgit v1.2.3 From 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/lib.rs') diff --git a/rust/src/auth.rs b/rust/src/auth.rs index 16fd4fe2..4b608a96 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -198,6 +198,12 @@ impl AuthConfectionary { .unwrap() } + pub fn add_keypair(&mut self, identifier: String, key_base64: String) -> Result<()> { + let key = BASE64.decode(key_base64.as_bytes())?; + self.root_keys.insert(identifier, key); + Ok(()) + } + pub fn create_token( &self, editor_id: FatCatId, diff --git a/rust/src/bin/fatcatd.rs b/rust/src/bin/fatcatd.rs index a4f20ddb..04f88948 100644 --- a/rust/src/bin/fatcatd.rs +++ b/rust/src/bin/fatcatd.rs @@ -39,6 +39,14 @@ fn main() { let formatter = DefaultLogFormatter; let server = fatcat::server().unwrap(); + info!( + logger, + "using primary auth key: {}", server.auth_confectionary.identifier, + ); + info!( + logger, + "all auth keys: {:?}", server.auth_confectionary.root_keys.keys().collect::>(), + ); let mut router = fatcat_api_spec::router(server); router.get("/", root_handler, "root-redirect"); diff --git a/rust/src/lib.rs b/rust/src/lib.rs index a31404da..7d00641a 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -122,7 +122,24 @@ pub fn env_confectionary() -> Result { let auth_location = env::var("AUTH_LOCATION").expect("AUTH_LOCATION must be set"); let auth_key = env::var("AUTH_SECRET_KEY").expect("AUTH_SECRET_KEY must be set"); let auth_key_ident = env::var("AUTH_KEY_IDENT").expect("AUTH_KEY_IDENT must be set"); - AuthConfectionary::new(auth_location, auth_key_ident, auth_key) + info!("Loaded primary auth key: {}", auth_key_ident); + let mut confectionary = AuthConfectionary::new(auth_location, auth_key_ident, auth_key)?; + match env::var("AUTH_ALT_KEYS") { + Ok(var) => { + for pair in var.split(",") { + let pair: Vec<&str> = pair.split(":").collect(); + if pair.len() != 2 { + println!("{:#?}", pair); + bail!("couldn't parse keypair from AUTH_ALT_KEYS (expected 'ident:key' pairs separated by commas)"); + } + info!("Loading alt auth key: {}", pair[0]); + confectionary.add_keypair(pair[0].to_string(), pair[1].to_string())?; + + } + }, + Err(_) => (), + } + Ok(confectionary) } /// Instantiate a new API server with a pooled database connection -- cgit v1.2.3 From 42ffee8c583729287aed7eaa6df4b7b121c1f7f6 Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Mon, 31 Dec 2018 18:05:24 -0800 Subject: make editor_id optional when createding editgroup The editor_id can be infered from auth metadata. --- fatcat-openapi2.yml | 2 -- rust/fatcat-api-spec/README.md | 2 +- rust/fatcat-api-spec/api.yaml | 2 -- rust/fatcat-api-spec/api/swagger.yaml | 2 -- rust/fatcat-api-spec/src/models.rs | 7 ++++--- rust/src/api_server.rs | 6 +++--- rust/src/api_wrappers.rs | 13 +++++++++++++ rust/src/bin/fatcatd.rs | 7 ++++++- rust/src/database_models.rs | 2 +- rust/src/lib.rs | 3 +-- rust/tests/test_api_server_http.rs | 33 +++++++++++++++++++++++++++++++++ rust/tests/test_old_python_tests.rs | 7 +++++-- 12 files changed, 67 insertions(+), 19 deletions(-) (limited to 'rust/src/lib.rs') diff --git a/fatcat-openapi2.yml b/fatcat-openapi2.yml index 98b9e4b0..80db5074 100644 --- a/fatcat-openapi2.yml +++ b/fatcat-openapi2.yml @@ -445,8 +445,6 @@ definitions: example: "zerocool93" editgroup: type: object - required: - - editor_id properties: editgroup_id: <<: *FATCATIDENT diff --git a/rust/fatcat-api-spec/README.md b/rust/fatcat-api-spec/README.md index bed47c45..7e946b16 100644 --- a/rust/fatcat-api-spec/README.md +++ b/rust/fatcat-api-spec/README.md @@ -13,7 +13,7 @@ To see how to make this your own, look here: [README](https://github.com/swagger-api/swagger-codegen/blob/master/README.md) - API version: 0.1.0 -- Build date: 2018-12-31T22:21:53.785Z +- Build date: 2019-01-01T01:45:02.795Z This autogenerated project defines an API crate `fatcat` which contains: * An `Api` trait defining the API in Rust. diff --git a/rust/fatcat-api-spec/api.yaml b/rust/fatcat-api-spec/api.yaml index 98b9e4b0..80db5074 100644 --- a/rust/fatcat-api-spec/api.yaml +++ b/rust/fatcat-api-spec/api.yaml @@ -445,8 +445,6 @@ definitions: example: "zerocool93" editgroup: type: object - required: - - editor_id properties: editgroup_id: <<: *FATCATIDENT diff --git a/rust/fatcat-api-spec/api/swagger.yaml b/rust/fatcat-api-spec/api/swagger.yaml index 670d3551..12bfe192 100644 --- a/rust/fatcat-api-spec/api/swagger.yaml +++ b/rust/fatcat-api-spec/api/swagger.yaml @@ -7598,8 +7598,6 @@ definitions: upperCaseName: "EDITOR" editgroup: type: "object" - required: - - "editor_id" properties: editgroup_id: type: "string" diff --git a/rust/fatcat-api-spec/src/models.rs b/rust/fatcat-api-spec/src/models.rs index 01b4c28e..4d7575b6 100644 --- a/rust/fatcat-api-spec/src/models.rs +++ b/rust/fatcat-api-spec/src/models.rs @@ -190,7 +190,8 @@ pub struct Editgroup { /// base32-encoded unique identifier #[serde(rename = "editor_id")] - pub editor_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub editor_id: Option, #[serde(rename = "description")] #[serde(skip_serializing_if = "Option::is_none")] @@ -206,10 +207,10 @@ pub struct Editgroup { } impl Editgroup { - pub fn new(editor_id: String) -> Editgroup { + pub fn new() -> Editgroup { Editgroup { editgroup_id: None, - editor_id: editor_id, + editor_id: None, description: None, extra: None, edits: None, diff --git a/rust/src/api_server.rs b/rust/src/api_server.rs index 853f7bc2..be9f1883 100644 --- a/rust/src/api_server.rs +++ b/rust/src/api_server.rs @@ -384,7 +384,7 @@ impl Server { ) -> Result { let row: EditgroupRow = insert_into(editgroup::table) .values(( - editgroup::editor_id.eq(FatCatId::from_str(&entity.editor_id)?.to_uuid()), + editgroup::editor_id.eq(FatCatId::from_str(&entity.editor_id.unwrap())?.to_uuid()), editgroup::description.eq(entity.description), editgroup::extra_json.eq(entity.extra), )) @@ -392,7 +392,7 @@ impl Server { Ok(Editgroup { editgroup_id: Some(uuid2fcid(&row.id)), - editor_id: uuid2fcid(&row.editor_id), + editor_id: Some(uuid2fcid(&row.editor_id)), description: row.description, edits: None, extra: row.extra_json, @@ -467,7 +467,7 @@ impl Server { let eg = Editgroup { editgroup_id: Some(uuid2fcid(&row.id)), - editor_id: uuid2fcid(&row.editor_id), + editor_id: Some(uuid2fcid(&row.editor_id)), description: row.description, edits: Some(edits), extra: row.extra_json, diff --git a/rust/src/api_wrappers.rs b/rust/src/api_wrappers.rs index ae070e02..3dec1c26 100644 --- a/rust/src/api_wrappers.rs +++ b/rust/src/api_wrappers.rs @@ -988,6 +988,19 @@ impl Api for Server { .auth_confectionary .require_auth(&conn, &context.auth_data)?; auth_context.require_role(FatcatRole::Editor)?; + let mut entity = entity.clone(); + match entity.editor_id.clone() { + Some(editor_id) => { + if !auth_context.has_role(FatcatRole::Admin) { + if editor_id != auth_context.editor_id.to_string() { + bail!("not authorized to create editgroups in others' names"); + } + } + }, + None => { + entity.editor_id = Some(auth_context.editor_id.to_string()); + } + }; self.create_editgroup_handler(entity, &conn) }) { Ok(eg) => CreateEditgroupResponse::SuccessfullyCreated(eg), diff --git a/rust/src/bin/fatcatd.rs b/rust/src/bin/fatcatd.rs index 04f88948..682f5038 100644 --- a/rust/src/bin/fatcatd.rs +++ b/rust/src/bin/fatcatd.rs @@ -45,7 +45,12 @@ fn main() { ); info!( logger, - "all auth keys: {:?}", server.auth_confectionary.root_keys.keys().collect::>(), + "all auth keys: {:?}", + server + .auth_confectionary + .root_keys + .keys() + .collect::>(), ); let mut router = fatcat_api_spec::router(server); diff --git a/rust/src/database_models.rs b/rust/src/database_models.rs index f6cca3e1..7a65f901 100644 --- a/rust/src/database_models.rs +++ b/rust/src/database_models.rs @@ -564,7 +564,7 @@ impl EditgroupRow { pub fn into_model_partial(self) -> Editgroup { Editgroup { editgroup_id: Some(uuid2fcid(&self.id)), - editor_id: uuid2fcid(&self.editor_id), + editor_id: Some(uuid2fcid(&self.editor_id)), description: self.description, extra: self.extra_json, edits: None, diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 7d00641a..b3e6c813 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -134,9 +134,8 @@ pub fn env_confectionary() -> Result { } info!("Loading alt auth key: {}", pair[0]); confectionary.add_keypair(pair[0].to_string(), pair[1].to_string())?; - } - }, + } Err(_) => (), } Ok(confectionary) diff --git a/rust/tests/test_api_server_http.rs b/rust/tests/test_api_server_http.rs index 2160a0a0..d975fe6e 100644 --- a/rust/tests/test_api_server_http.rs +++ b/rust/tests/test_api_server_http.rs @@ -1545,3 +1545,36 @@ fn test_release_types() { Some("release_type"), ); } + +#[test] +fn test_create_editgroup() { + let (headers, router, _conn) = setup_http(); + + // We're authenticated, so don't need to supply editor_id + check_http_response( + request::post( + &format!( + "http://localhost:9411/v0/editgroup", + ), + headers.clone(), + "{}", + &router, + ), + status::Created, + None, + ); + + // But can if we want to + check_http_response( + request::post( + &format!( + "http://localhost:9411/v0/editgroup", + ), + headers.clone(), + r#"{"editor_id": "aaaaaaaaaaaabkvkaaaaaaaaae"}"#, + &router, + ), + status::Created, + None, + ); +} diff --git a/rust/tests/test_old_python_tests.rs b/rust/tests/test_old_python_tests.rs index 1f91c7db..afeff55e 100644 --- a/rust/tests/test_old_python_tests.rs +++ b/rust/tests/test_old_python_tests.rs @@ -22,7 +22,8 @@ fn test_api_rich_create() { let admin_id = "aaaaaaaaaaaabkvkaaaaaaaaae".to_string(); - let mut new_eg = Editgroup::new(admin_id); + let mut new_eg = Editgroup::new(); + new_eg.editor_id = Some(admin_id); new_eg.description = Some("a unit test edit".to_string()); let resp = client.create_editgroup(new_eg).wait().unwrap(); let editgroup_id = match resp { @@ -196,8 +197,10 @@ fn test_merge_works() { let admin_id = "aaaaaaaaaaaabkvkaaaaaaaaae".to_string(); + let mut eg = Editgroup::new(); + eg.editor_id = Some(admin_id); let resp = client - .create_editgroup(Editgroup::new(admin_id)) + .create_editgroup(eg) .wait() .unwrap(); let editgroup_id = match resp { -- cgit v1.2.3