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_wrappers.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'rust/src/api_wrappers.rs') 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 }; -- 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/api_wrappers.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 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/api_wrappers.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 From b292d25b3f29407e4a6c3c093bd15027da6d8d73 Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Mon, 31 Dec 2018 18:43:51 -0800 Subject: rust fmt --- rust/src/api_wrappers.rs | 2 +- rust/tests/test_api_server_http.rs | 8 ++------ rust/tests/test_old_python_tests.rs | 5 +---- 3 files changed, 4 insertions(+), 11 deletions(-) (limited to 'rust/src/api_wrappers.rs') diff --git a/rust/src/api_wrappers.rs b/rust/src/api_wrappers.rs index 3dec1c26..6c003802 100644 --- a/rust/src/api_wrappers.rs +++ b/rust/src/api_wrappers.rs @@ -996,7 +996,7 @@ impl Api for Server { bail!("not authorized to create editgroups in others' names"); } } - }, + } None => { entity.editor_id = Some(auth_context.editor_id.to_string()); } diff --git a/rust/tests/test_api_server_http.rs b/rust/tests/test_api_server_http.rs index d975fe6e..5405c9cb 100644 --- a/rust/tests/test_api_server_http.rs +++ b/rust/tests/test_api_server_http.rs @@ -1553,9 +1553,7 @@ fn test_create_editgroup() { // We're authenticated, so don't need to supply editor_id check_http_response( request::post( - &format!( - "http://localhost:9411/v0/editgroup", - ), + &format!("http://localhost:9411/v0/editgroup",), headers.clone(), "{}", &router, @@ -1567,9 +1565,7 @@ fn test_create_editgroup() { // But can if we want to check_http_response( request::post( - &format!( - "http://localhost:9411/v0/editgroup", - ), + &format!("http://localhost:9411/v0/editgroup",), headers.clone(), r#"{"editor_id": "aaaaaaaaaaaabkvkaaaaaaaaae"}"#, &router, diff --git a/rust/tests/test_old_python_tests.rs b/rust/tests/test_old_python_tests.rs index afeff55e..4fc1ffaf 100644 --- a/rust/tests/test_old_python_tests.rs +++ b/rust/tests/test_old_python_tests.rs @@ -199,10 +199,7 @@ fn test_merge_works() { let mut eg = Editgroup::new(); eg.editor_id = Some(admin_id); - let resp = client - .create_editgroup(eg) - .wait() - .unwrap(); + let resp = client.create_editgroup(eg).wait().unwrap(); let editgroup_id = match resp { CreateEditgroupResponse::SuccessfullyCreated(eg) => eg.editgroup_id.unwrap(), _ => unreachable!(), -- cgit v1.2.3 From 39678e1410a06e99ea71655485786caaf5847e7f Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Thu, 3 Jan 2019 16:53:27 -0800 Subject: start to impl oidc auth --- rust/src/api_server.rs | 33 +++++++++++++++---- rust/src/api_wrappers.rs | 79 +++++++++++++++++++++++++++++++++++++++++++++ rust/src/database_models.rs | 28 ++++++++++++++-- rust/src/database_schema.rs | 14 ++++++++ 4 files changed, 146 insertions(+), 8 deletions(-) (limited to 'rust/src/api_wrappers.rs') diff --git a/rust/src/api_server.rs b/rust/src/api_server.rs index be9f1883..1edf739c 100644 --- a/rust/src/api_server.rs +++ b/rust/src/api_server.rs @@ -477,12 +477,7 @@ impl Server { pub fn get_editor_handler(&self, editor_id: FatCatId, conn: &DbConn) -> Result { let row: EditorRow = editor::table.find(editor_id.to_uuid()).first(conn)?; - - let ed = Editor { - editor_id: Some(uuid2fcid(&row.id)), - username: row.username, - }; - Ok(ed) + Ok(row.into_model()) } pub fn get_editor_changelog_handler( @@ -544,6 +539,32 @@ impl Server { Ok(entry) } + /// This helper either finds an Editor model by OIDC parameters (eg, remote domain and + /// identifier), or creates one and inserts the appropriate auth rows. The semantics are + /// basically an "upsert" of signup/account-creation. + /// Returns an editor model and boolean flag indicating whether a new editor was created or + /// not. + /// If this function creates an editor, it sets the username to "{iss}-{provider}"; the intent + /// is for this to be temporary but unique. Might look like "bnewbold-github", or might look + /// like "895139824-github". This is a hack to make check/creation idempotent. + pub fn auth_oidc_handler(&self, params: AuthOidc, conn: &DbConn) -> Result<(Editor, bool)> { + let existing: Vec<(EditorRow, AuthOidcRow)> = editor::table + .inner_join(auth_oidc::table) + .filter(auth_oidc::oidc_sub.eq(params.sub.clone())) + .filter(auth_oidc::oidc_iss.eq(params.iss)) + .load(conn)?; + + let (editor_row, created): (EditorRow, bool) = match existing.first() { + Some((editor, _)) => (editor.clone(), false), + None => { + let username = format!("{}-{}", params.sub, params.provider); + (create_editor(conn, username, false, false)?, true) + } + }; + + Ok((editor_row.into_model(), created)) + } + entity_batch_handler!(create_container_batch_handler, ContainerEntity); entity_batch_handler!(create_creator_batch_handler, CreatorEntity); entity_batch_handler!(create_file_batch_handler, FileEntity); diff --git a/rust/src/api_wrappers.rs b/rust/src/api_wrappers.rs index 6c003802..c6966cee 100644 --- a/rust/src/api_wrappers.rs +++ b/rust/src/api_wrappers.rs @@ -1067,4 +1067,83 @@ impl Api for Server { }; Box::new(futures::done(Ok(ret))) } + + fn auth_oidc( + &self, + params: models::AuthOidc, + 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::Admin)?; + let (editor, created) = self.auth_oidc_handler(params, &conn)?; + // create an auth token; leave it to webface to attenuate to a given duration + let token = self + .auth_confectionary + .create_token(FatCatId::from_str(&editor.editor_id.clone().unwrap())?, None)?; + let result = AuthOidcResult { editor, token }; + Ok((result, created)) + }) { + Ok((result, true)) => AuthOidcResponse::Created(result), + Ok((result, false)) => AuthOidcResponse::Found(result), + Err(Error(ErrorKind::Diesel(e), _)) => AuthOidcResponse::BadRequest(ErrorResponse { + message: e.to_string(), + }), + Err(Error(ErrorKind::Uuid(e), _)) => AuthOidcResponse::BadRequest(ErrorResponse { + message: e.to_string(), + }), + Err(Error(ErrorKind::InvalidFatcatId(e), _)) => { + AuthOidcResponse::BadRequest(ErrorResponse { + message: ErrorKind::InvalidFatcatId(e).to_string(), + }) + } + Err(Error(ErrorKind::MalformedExternalId(e), _)) => { + AuthOidcResponse::BadRequest(ErrorResponse { + message: e.to_string(), + }) + } + Err(Error(ErrorKind::MalformedChecksum(e), _)) => { + AuthOidcResponse::BadRequest(ErrorResponse { + message: e.to_string(), + }) + } + Err(Error(ErrorKind::NotInControlledVocabulary(e), _)) => { + AuthOidcResponse::BadRequest(ErrorResponse { + message: e.to_string(), + }) + } + Err(Error(ErrorKind::EditgroupAlreadyAccepted(e), _)) => { + AuthOidcResponse::BadRequest(ErrorResponse { + message: e.to_string(), + }) + } + Err(Error(ErrorKind::InvalidCredentials(e), _)) => + // TODO: why can't I NotAuthorized here? + { + AuthOidcResponse::Forbidden(ErrorResponse { + message: e.to_string(), + }) + } + Err(Error(ErrorKind::InsufficientPrivileges(e), _)) => { + AuthOidcResponse::Forbidden(ErrorResponse { + message: e.to_string(), + }) + } + Err(Error(ErrorKind::OtherBadRequest(e), _)) => { + AuthOidcResponse::BadRequest(ErrorResponse { + message: e.to_string(), + }) + } + Err(e) => { + error!("{}", e); + AuthOidcResponse::GenericError(ErrorResponse { + message: e.to_string(), + }) + } + }; + Box::new(futures::done(Ok(ret))) + } } diff --git a/rust/src/database_models.rs b/rust/src/database_models.rs index 7a65f901..5c8e17d3 100644 --- a/rust/src/database_models.rs +++ b/rust/src/database_models.rs @@ -4,7 +4,7 @@ use api_helpers::uuid2fcid; use chrono; use database_schema::*; use errors::*; -use fatcat_api_spec::models::{ChangelogEntry, Editgroup, EntityEdit}; +use fatcat_api_spec::models::{ChangelogEntry, Editgroup, Editor, EntityEdit}; use serde_json; use uuid::Uuid; @@ -559,7 +559,7 @@ pub struct EditgroupRow { } impl EditgroupRow { - /// Returns an Edigroup API model *without* the entity edits actually populated. Useful for, + /// Returns an Editgroup API model *without* the entity edits actually populated. Useful for, /// eg, entity history queries (where we already have the entity edit we want) pub fn into_model_partial(self) -> Editgroup { Editgroup { @@ -579,12 +579,36 @@ pub struct EditorRow { pub username: String, pub is_admin: bool, pub is_bot: bool, + pub is_active: bool, pub registered: chrono::NaiveDateTime, pub auth_epoch: chrono::NaiveDateTime, pub wrangler_id: Option, pub active_editgroup_id: Option, } +impl EditorRow { + pub fn into_model(self) -> Editor { + Editor { + editor_id: Some(uuid2fcid(&self.id)), + username: self.username, + is_admin: Some(self.is_admin), + is_bot: Some(self.is_bot), + is_active: Some(self.is_active), + } + } +} + +#[derive(Debug, Clone, Queryable, Associations, AsChangeset)] +#[table_name = "auth_oidc"] +pub struct AuthOidcRow { + pub id: i64, + pub created: chrono::NaiveDateTime, + pub editor_id: Uuid, + pub provider: String, + pub oidc_iss: String, + pub oidc_sub: String, +} + #[derive(Debug, Queryable, Identifiable, Associations, AsChangeset)] #[table_name = "changelog"] pub struct ChangelogRow { diff --git a/rust/src/database_schema.rs b/rust/src/database_schema.rs index c240048e..49863fc7 100644 --- a/rust/src/database_schema.rs +++ b/rust/src/database_schema.rs @@ -5,6 +5,17 @@ table! { } } +table! { + auth_oidc (id) { + id -> Int8, + created -> Timestamptz, + editor_id -> Uuid, + provider -> Text, + oidc_iss -> Text, + oidc_sub -> Text, + } +} + table! { changelog (id) { id -> Int8, @@ -98,6 +109,7 @@ table! { username -> Text, is_admin -> Bool, is_bot -> Bool, + is_active -> Bool, registered -> Timestamptz, auth_epoch -> Timestamptz, wrangler_id -> Nullable, @@ -387,6 +399,7 @@ table! { } } +joinable!(auth_oidc -> editor (editor_id)); joinable!(changelog -> editgroup (editgroup_id)); joinable!(container_edit -> editgroup (editgroup_id)); joinable!(container_ident -> container_rev (rev_id)); @@ -424,6 +437,7 @@ joinable!(work_ident -> work_rev (rev_id)); allow_tables_to_appear_in_same_query!( abstracts, + auth_oidc, changelog, container_edit, container_ident, -- cgit v1.2.3 From ec180ef211112d1ae14bf7bad06bf71686cdaf89 Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Thu, 3 Jan 2019 17:53:30 -0800 Subject: editor update --- rust/src/api_helpers.rs | 49 ++++++++++++++++++++++++++++- rust/src/api_wrappers.rs | 82 ++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 127 insertions(+), 4 deletions(-) (limited to 'rust/src/api_wrappers.rs') diff --git a/rust/src/api_helpers.rs b/rust/src/api_helpers.rs index 7478da9d..79114d4f 100644 --- a/rust/src/api_helpers.rs +++ b/rust/src/api_helpers.rs @@ -228,13 +228,13 @@ 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 { + check_username(&username)?; let ed: EditorRow = diesel::insert_into(editor::table) .values(( editor::username.eq(username), @@ -245,6 +245,19 @@ pub fn create_editor( Ok(ed) } +pub fn update_editor_username( + conn: &DbConn, + editor_id: FatCatId, + username: String, +) -> Result { + check_username(&username)?; + diesel::update(editor::table.find(editor_id.to_uuid())) + .set(editor::username.eq(username)) + .execute(conn)?; + let editor: EditorRow = editor::table.find(editor_id.to_uuid()).get_result(conn)?; + Ok(editor) +} + /// 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 @@ -344,6 +357,40 @@ pub fn uuid2fcid(id: &Uuid) -> String { BASE32_NOPAD.encode(raw).to_lowercase() } +pub fn check_username(raw: &str) -> Result<()> { + lazy_static! { + static ref RE: Regex = Regex::new(r"^[A-Za-z0-9][A-Za-z0-9._-]{2,15}$").unwrap(); + } + if RE.is_match(raw) { + Ok(()) + } else { + Err(ErrorKind::MalformedExternalId(format!( + "not a valid username: '{}' (expected, eg, 'AcidBurn')", + raw + )) + .into()) + } +} + +#[test] +fn test_check_username() { + assert!(check_username("bnewbold").is_ok()); + assert!(check_username("BNEWBOLD").is_ok()); + assert!(check_username("admin").is_ok()); + assert!(check_username("friend-bot").is_ok()); + assert!(check_username("dog").is_ok()); + assert!(check_username("g_____").is_ok()); + + assert!(check_username("").is_err()); + assert!(check_username("_").is_err()); + assert!(check_username("gg").is_err()); + assert!(check_username("adminadminadminadminadminadminadmin").is_err()); + assert!(check_username("bryan newbold").is_err()); + assert!(check_username("01234567-3456-6780").is_err()); + assert!(check_username(".admin").is_err()); + assert!(check_username("-bot").is_err()); +} + pub fn check_pmcid(raw: &str) -> Result<()> { lazy_static! { static ref RE: Regex = Regex::new(r"^PMC\d+$").unwrap(); diff --git a/rust/src/api_wrappers.rs b/rust/src/api_wrappers.rs index c6966cee..abaa2310 100644 --- a/rust/src/api_wrappers.rs +++ b/rust/src/api_wrappers.rs @@ -904,6 +904,81 @@ impl Api for Server { Box::new(futures::done(Ok(ret))) } + /// For now, only implements updating username + fn update_editor( + &self, + editor_id: String, + editor: models::Editor, + context: &Context, + ) -> Box + Send> { + let conn = self.db_pool.get().expect("db_pool error"); + let ret = match conn.transaction(|| { + if Some(editor_id.clone()) != editor.editor_id { + return Err( + ErrorKind::OtherBadRequest("editor_id doesn't match".to_string()).into(), + ); + } + let auth_context = self + .auth_confectionary + .require_auth(&conn, &context.auth_data)?; + let editor_id = FatCatId::from_str(&editor_id)?; + // DANGER! these permissions are for username updates only! + if editor_id == auth_context.editor_id { + // self edit of username allowed + auth_context.require_role(FatcatRole::Editor)?; + } else { + // admin can update any username + auth_context.require_role(FatcatRole::Admin)?; + }; + update_editor_username(&conn, editor_id, editor.username) + .map(|e| e.into_model()) + }) { + Ok(editor) => UpdateEditorResponse::UpdatedEditor(editor), + Err(Error(ErrorKind::Diesel(e), _)) => { + UpdateEditorResponse::BadRequest(ErrorResponse { + message: e.to_string(), + }) + } + Err(Error(ErrorKind::Uuid(e), _)) => UpdateEditorResponse::BadRequest(ErrorResponse { + message: e.to_string(), + }), + Err(Error(ErrorKind::InvalidFatcatId(e), _)) => { + UpdateEditorResponse::BadRequest(ErrorResponse { + message: ErrorKind::InvalidFatcatId(e).to_string(), + }) + } + Err(Error(ErrorKind::MalformedExternalId(e), _)) => { + UpdateEditorResponse::BadRequest(ErrorResponse { + message: e.to_string(), + }) + } + Err(Error(ErrorKind::InvalidCredentials(e), _)) => + // TODO: why can't I NotAuthorized here? + { + UpdateEditorResponse::Forbidden(ErrorResponse { + message: e.to_string(), + }) + } + Err(Error(ErrorKind::InsufficientPrivileges(e), _)) => { + UpdateEditorResponse::Forbidden(ErrorResponse { + message: e.to_string(), + }) + } + Err(Error(ErrorKind::OtherBadRequest(e), _)) => { + UpdateEditorResponse::BadRequest(ErrorResponse { + message: e.to_string(), + }) + } + Err(e) => { + error!("{}", e); + UpdateEditorResponse::GenericError(ErrorResponse { + message: e.to_string(), + }) + } + }; + Box::new(futures::done(Ok(ret))) + } + fn accept_editgroup( &self, editgroup_id: String, @@ -1081,9 +1156,10 @@ impl Api for Server { auth_context.require_role(FatcatRole::Admin)?; let (editor, created) = self.auth_oidc_handler(params, &conn)?; // create an auth token; leave it to webface to attenuate to a given duration - let token = self - .auth_confectionary - .create_token(FatCatId::from_str(&editor.editor_id.clone().unwrap())?, None)?; + let token = self.auth_confectionary.create_token( + FatCatId::from_str(&editor.editor_id.clone().unwrap())?, + None, + )?; let result = AuthOidcResult { editor, token }; Ok((result, created)) }) { -- cgit v1.2.3 From 94a6c7f58ba23b2c37b0a997fc4a5e2e16692599 Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Thu, 3 Jan 2019 22:09:06 -0800 Subject: fix rust side of login --- rust/src/api_server.rs | 23 +++++++++++++++++------ rust/src/api_wrappers.rs | 3 +-- 2 files changed, 18 insertions(+), 8 deletions(-) (limited to 'rust/src/api_wrappers.rs') diff --git a/rust/src/api_server.rs b/rust/src/api_server.rs index 1edf739c..349c6a27 100644 --- a/rust/src/api_server.rs +++ b/rust/src/api_server.rs @@ -544,21 +544,32 @@ impl Server { /// basically an "upsert" of signup/account-creation. /// Returns an editor model and boolean flag indicating whether a new editor was created or /// not. - /// If this function creates an editor, it sets the username to "{iss}-{provider}"; the intent - /// is for this to be temporary but unique. Might look like "bnewbold-github", or might look - /// like "895139824-github". This is a hack to make check/creation idempotent. + /// If this function creates an editor, it sets the username to + /// "{preferred_username}-{provider}"; the intent is for this to be temporary but unique. Might + /// look like "bnewbold-github", or might look like "895139824-github". This is a hack to make + /// check/creation idempotent. pub fn auth_oidc_handler(&self, params: AuthOidc, conn: &DbConn) -> Result<(Editor, bool)> { let existing: Vec<(EditorRow, AuthOidcRow)> = editor::table .inner_join(auth_oidc::table) .filter(auth_oidc::oidc_sub.eq(params.sub.clone())) - .filter(auth_oidc::oidc_iss.eq(params.iss)) + .filter(auth_oidc::oidc_iss.eq(params.iss.clone())) .load(conn)?; let (editor_row, created): (EditorRow, bool) = match existing.first() { Some((editor, _)) => (editor.clone(), false), None => { - let username = format!("{}-{}", params.sub, params.provider); - (create_editor(conn, username, false, false)?, true) + let username = format!("{}-{}", params.preferred_username, params.provider); + let editor = create_editor(conn, username, false, false)?; + // create an auth login row so the user can log back in + diesel::insert_into(auth_oidc::table) + .values(( + auth_oidc::editor_id.eq(editor.id), + auth_oidc::provider.eq(params.provider), + auth_oidc::oidc_iss.eq(params.iss), + auth_oidc::oidc_sub.eq(params.sub), + )) + .execute(conn)?; + (editor, true) } }; diff --git a/rust/src/api_wrappers.rs b/rust/src/api_wrappers.rs index abaa2310..c663c11d 100644 --- a/rust/src/api_wrappers.rs +++ b/rust/src/api_wrappers.rs @@ -930,8 +930,7 @@ impl Api for Server { // admin can update any username auth_context.require_role(FatcatRole::Admin)?; }; - update_editor_username(&conn, editor_id, editor.username) - .map(|e| e.into_model()) + update_editor_username(&conn, editor_id, editor.username).map(|e| e.into_model()) }) { Ok(editor) => UpdateEditorResponse::UpdatedEditor(editor), Err(Error(ErrorKind::Diesel(e), _)) => { -- cgit v1.2.3 From eccdd4577a54b230460de6733ed7b003b6f8f182 Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Fri, 4 Jan 2019 19:24:21 -0800 Subject: add superuser role/flag --- rust/migrations/2018-05-12-001226_init/up.sql | 15 +++++++------ rust/src/api_wrappers.rs | 20 ++++++++--------- rust/src/auth.rs | 32 ++++++++++++++++++--------- rust/src/database_models.rs | 1 + rust/src/database_schema.rs | 1 + rust/tests/test_auth.rs | 6 ++--- 6 files changed, 44 insertions(+), 31 deletions(-) (limited to 'rust/src/api_wrappers.rs') diff --git a/rust/migrations/2018-05-12-001226_init/up.sql b/rust/migrations/2018-05-12-001226_init/up.sql index 53762f40..b5b39f6f 100644 --- a/rust/migrations/2018-05-12-001226_init/up.sql +++ b/rust/migrations/2018-05-12-001226_init/up.sql @@ -17,6 +17,7 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE TABLE editor ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), username TEXT NOT NULL CHECK (username ~* '^[A-Za-z0-9][A-Za-z0-9._-]{2,15}$'), -- UNIQ below + is_superuser BOOLEAN NOT NULL DEFAULT false, is_admin BOOLEAN NOT NULL DEFAULT false, is_bot BOOLEAN NOT NULL DEFAULT false, is_active BOOLEAN NOT NULL DEFAULT true, @@ -470,10 +471,13 @@ CREATE INDEX webcapture_rev_release_target_release_idx ON webcapture_rev_release BEGIN; -INSERT INTO editor (id, username, is_admin, auth_epoch) VALUES - ('00000000-0000-0000-AAAA-000000000001', 'admin', true, '1970-01-01T01:01:01Z'), -- aaaaaaaaaaaabkvkaaaaaaaaae - ('00000000-0000-0000-AAAA-000000000002', 'demo-user', true, '1970-01-01T01:01:01Z'), -- aaaaaaaaaaaabkvkaaaaaaaaai - ('00000000-0000-0000-AAAA-000000000003', 'claire', false, default); -- aaaaaaaaaaaabkvkaaaaaaaaam +INSERT INTO editor (id, username, is_superuser, is_admin, is_bot, auth_epoch) VALUES + ('00000000-0000-0000-AAAA-000000000001', 'root', true, true, false, '1970-01-01T01:01:01Z'), -- aaaaaaaaaaaabkvkaaaaaaaaae + ('00000000-0000-0000-AAAA-000000000002', 'admin', true, true, false, '1970-01-01T01:01:01Z'), -- aaaaaaaaaaaabkvkaaaaaaaaai + ('00000000-0000-0000-AAAA-000000000003', 'demo-user', false, true, false, '1970-01-01T01:01:01Z'), -- aaaaaaaaaaaabkvkaaaaaaaaam + ('00000000-0000-0000-AAAA-000000000004', 'claire', false, false, false, default), -- aaaaaaaaaaaabkvkaaaaaaaaaq + ('00000000-0000-0000-AAAA-000000000005', 'webface-bot', true, true, true, '1970-01-01T01:01:01Z'), -- aaaaaaaaaaaabkvkaaaaaaaaau + ('00000000-0000-0000-AAAA-000000000006', 'bnewbold', false, true, false, '1970-01-01T01:01:01Z'); -- aaaaaaaaaaaabkvkaaaaaaaaay INSERT INTO editgroup (id, editor_id, description) VALUES ('00000000-0000-0000-BBBB-000000000001', '00000000-0000-0000-AAAA-000000000001', 'first edit ever!'), -- aaaaaaaaaaaabo53aaaaaaaaae @@ -483,9 +487,6 @@ INSERT INTO editgroup (id, editor_id, description) VALUES ('00000000-0000-0000-BBBB-000000000005', '00000000-0000-0000-AAAA-000000000001', 'journal edit'), -- aaaaaaaaaaaabo53aaaaaaaaau ('00000000-0000-0000-BBBB-000000000006', '00000000-0000-0000-AAAA-000000000001', 'another journal edit'); -- aaaaaaaaaaaabo53aaaaaaaaay -INSERT INTO editor (id, username, is_admin, active_editgroup_id) VALUES - ('00000000-0000-0000-AAAA-000000000004', 'bnewbold', true, '00000000-0000-0000-BBBB-000000000004'); - INSERT INTO changelog (editgroup_id) VALUES ('00000000-0000-0000-BBBB-000000000001'), ('00000000-0000-0000-BBBB-000000000002'), diff --git a/rust/src/api_wrappers.rs b/rust/src/api_wrappers.rs index c663c11d..614a0007 100644 --- a/rust/src/api_wrappers.rs +++ b/rust/src/api_wrappers.rs @@ -85,7 +85,7 @@ 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.require_auth(&conn, &context.auth_data)?; + let auth_context = self.auth_confectionary.require_auth(&conn, &context.auth_data, Some(stringify!($post_fn)))?; auth_context.require_role(FatcatRole::Editor)?; let editgroup_id = if let Some(s) = editgroup_id { let eg_id = FatCatId::from_str(&s)?; @@ -137,7 +137,7 @@ 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.require_auth(&conn, &context.auth_data)?; + let auth_context = self.auth_confectionary.require_auth(&conn, &context.auth_data, Some(stringify!($post_batch_fn)))?; auth_context.require_role(FatcatRole::Editor)?; let editgroup_id = if let Some(s) = editgroup_id { let eg_id = FatCatId::from_str(&s)?; @@ -187,7 +187,7 @@ 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.require_auth(&conn, &context.auth_data)?; + let auth_context = self.auth_confectionary.require_auth(&conn, &context.auth_data, Some(stringify!($update_fn)))?; auth_context.require_role(FatcatRole::Editor)?; let entity_id = FatCatId::from_str(&ident)?; let editgroup_id = if let Some(s) = editgroup_id { @@ -243,7 +243,7 @@ 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.require_auth(&conn, &context.auth_data)?; + let auth_context = self.auth_confectionary.require_auth(&conn, &context.auth_data, Some(stringify!($delete_fn)))?; auth_context.require_role(FatcatRole::Editor)?; let entity_id = FatCatId::from_str(&ident)?; let editgroup_id: Option = match editgroup_id { @@ -397,7 +397,7 @@ macro_rules! wrap_entity_handlers { 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)?; + let auth_context = self.auth_confectionary.require_auth(&conn, &context.auth_data, Some(stringify!($delete_edit_fn)))?; 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))?; @@ -920,7 +920,7 @@ impl Api for Server { } let auth_context = self .auth_confectionary - .require_auth(&conn, &context.auth_data)?; + .require_auth(&conn, &context.auth_data, Some("update_editor"))?; let editor_id = FatCatId::from_str(&editor_id)?; // DANGER! these permissions are for username updates only! if editor_id == auth_context.editor_id { @@ -988,7 +988,7 @@ impl Api for Server { let editgroup_id = FatCatId::from_str(&editgroup_id)?; let auth_context = self .auth_confectionary - .require_auth(&conn, &context.auth_data)?; + .require_auth(&conn, &context.auth_data, Some("accept_editgroup"))?; auth_context.require_role(FatcatRole::Admin)?; // NOTE: this is currently redundant, but zero-cost auth_context.require_editgroup(&conn, editgroup_id)?; @@ -1060,7 +1060,7 @@ impl Api for Server { let ret = match conn.transaction(|| { let auth_context = self .auth_confectionary - .require_auth(&conn, &context.auth_data)?; + .require_auth(&conn, &context.auth_data, Some("create_editgroup"))?; auth_context.require_role(FatcatRole::Editor)?; let mut entity = entity.clone(); match entity.editor_id.clone() { @@ -1151,8 +1151,8 @@ impl Api for Server { let ret = match conn.transaction(|| { let auth_context = self .auth_confectionary - .require_auth(&conn, &context.auth_data)?; - auth_context.require_role(FatcatRole::Admin)?; + .require_auth(&conn, &context.auth_data, Some("auth_oidc"))?; + auth_context.require_role(FatcatRole::Superuser)?; let (editor, created) = self.auth_oidc_handler(params, &conn)?; // create an auth token; leave it to webface to attenuate to a given duration let token = self.auth_confectionary.create_token( diff --git a/rust/src/auth.rs b/rust/src/auth.rs index 4b608a96..0160d2e8 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -18,13 +18,14 @@ use std::str::FromStr; // 32 bytes max (!) static DUMMY_KEY: &[u8] = b"dummy-key-a-one-two-three-a-la"; -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum FatcatRole { Public, Editor, Bot, Human, Admin, + Superuser, } #[derive(Clone)] @@ -35,6 +36,10 @@ pub struct AuthContext { impl AuthContext { pub fn has_role(&self, role: FatcatRole) -> bool { + if !self.editor_row.is_active { + // if account is disabled, only allow public role + return role == FatcatRole::Public; + } if self.editor_row.is_admin { return true; } @@ -44,15 +49,15 @@ impl AuthContext { FatcatRole::Bot => self.editor_row.is_bot, FatcatRole::Human => !self.editor_row.is_bot, FatcatRole::Admin => self.editor_row.is_admin, + FatcatRole::Superuser => self.editor_row.is_superuser, } } 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(), + format!("doesn't have required role: {:?}", role), ) .into()), } @@ -225,8 +230,7 @@ impl AuthConfectionary { Ok(BASE64.encode(&raw)) } - /// 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 { + pub fn parse_macaroon_token(&self, conn: &DbConn, s: &str, endpoint: Option<&str>) -> Result { let raw = BASE64.decode(s.as_bytes())?; let mac = match Macaroon::deserialize(&raw) { Ok(m) => m, @@ -258,6 +262,10 @@ impl AuthConfectionary { } let editor_id = editor_id.expect("expected an editor_id caveat"); verifier.satisfy_exact(&format!("editor_id = {}", editor_id.to_string())); + if let Some(endpoint) = endpoint { + // API endpoint + verifier.satisfy_exact(&format!("endpoint = {}", endpoint)); + } let mut created: Option> = None; for caveat in mac.first_party_caveats() { if caveat.predicate().starts_with("created = ") { @@ -329,6 +337,7 @@ impl AuthConfectionary { &self, conn: &DbConn, auth_data: &Option, + endpoint: Option<&str>, ) -> Result> { let token: Option = match auth_data { Some(AuthData::ApiKey(header)) => { @@ -355,15 +364,15 @@ impl AuthConfectionary { Some(t) => t, None => return Ok(None), }; - let editor_row = self.parse_macaroon_token(conn, &token)?; + let editor_row = self.parse_macaroon_token(conn, &token, endpoint)?; Ok(Some(AuthContext { editor_id: FatCatId::from_uuid(&editor_row.id), editor_row: editor_row, })) } - pub fn require_auth(&self, conn: &DbConn, auth_data: &Option) -> Result { - match self.parse_swagger(conn, auth_data)? { + pub fn require_auth(&self, conn: &DbConn, auth_data: &Option, endpoint: Option<&str>) -> Result { + match self.parse_swagger(conn, auth_data, endpoint)? { Some(auth) => Ok(auth), None => Err(ErrorKind::InvalidCredentials("no token supplied".to_string()).into()), } @@ -384,7 +393,7 @@ impl AuthConfectionary { for caveat in mac.first_party_caveats() { println!("caveat: {}", caveat.predicate()); } - println!("verify: {:?}", self.parse_macaroon_token(conn, token)); + println!("verify: {:?}", self.parse_macaroon_token(conn, token, None)); Ok(()) } } @@ -416,11 +425,12 @@ pub fn revoke_tokens_everyone(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)?; - println!("editor_id\t\t\tis_admin/is_bot\tauth_epoch\t\t\tusername\twrangler_id"); + println!("editor_id\t\t\tsuper/admin/bot\tauth_epoch\t\t\tusername\twrangler_id"); for e in all_editors { println!( - "{}\t{}\t{}\t{}\t{}\t{:?}", + "{}\t{}/{}/{}\t{}\t{}\t{:?}", FatCatId::from_uuid(&e.id).to_string(), + e.is_superuser, e.is_admin, e.is_bot, e.auth_epoch, diff --git a/rust/src/database_models.rs b/rust/src/database_models.rs index 5c8e17d3..59953f6b 100644 --- a/rust/src/database_models.rs +++ b/rust/src/database_models.rs @@ -577,6 +577,7 @@ impl EditgroupRow { pub struct EditorRow { pub id: Uuid, pub username: String, + pub is_superuser: bool, pub is_admin: bool, pub is_bot: bool, pub is_active: bool, diff --git a/rust/src/database_schema.rs b/rust/src/database_schema.rs index 49863fc7..0c553b40 100644 --- a/rust/src/database_schema.rs +++ b/rust/src/database_schema.rs @@ -107,6 +107,7 @@ table! { editor (id) { id -> Uuid, username -> Text, + is_superuser -> Bool, is_admin -> Bool, is_bot -> Bool, is_active -> Bool, diff --git a/rust/tests/test_auth.rs b/rust/tests/test_auth.rs index b06f3e7b..5ccbb6cb 100644 --- a/rust/tests/test_auth.rs +++ b/rust/tests/test_auth.rs @@ -35,13 +35,13 @@ fn test_auth_db() { let token = c.create_token(editor_id, None).unwrap(); // verify token - let editor_row = c.parse_macaroon_token(&conn, &token).unwrap(); + let editor_row = c.parse_macaroon_token(&conn, &token, None).unwrap(); assert_eq!(editor_row.id, editor_id.to_uuid()); // revoke token revoke_tokens(&conn, editor_id).unwrap(); // verification should fail - // XXX: one-second slop breads this - //assert!(c.parse_macaroon_token(&conn, &token).is_err()); + // XXX: one-second slop breaks this + //assert!(c.parse_macaroon_token(&conn, &token, None).is_err()); } -- cgit v1.2.3 From 35f3f55aac364373ba16191abdb3c0c585249245 Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Mon, 7 Jan 2019 18:08:09 -0800 Subject: add auth/check endpoint --- fatcat-openapi2.yml | 26 +++++ rust/HACKING.md | 9 ++ rust/fatcat-api-spec/README.md | 3 +- rust/fatcat-api-spec/api.yaml | 26 +++++ rust/fatcat-api-spec/api/swagger.yaml | 66 ++++++++++++ rust/fatcat-api-spec/examples/client.rs | 9 +- rust/fatcat-api-spec/examples/server_lib/server.rs | 8 +- rust/fatcat-api-spec/src/client.rs | 103 +++++++++++++++--- rust/fatcat-api-spec/src/lib.rs | 22 ++++ rust/fatcat-api-spec/src/mimetypes.rs | 20 ++++ rust/fatcat-api-spec/src/server.rs | 118 ++++++++++++++++++--- rust/src/api_wrappers.rs | 96 ++++++++++++++--- rust/src/auth.rs | 21 +++- 13 files changed, 480 insertions(+), 47 deletions(-) (limited to 'rust/src/api_wrappers.rs') diff --git a/fatcat-openapi2.yml b/fatcat-openapi2.yml index 501a1296..625a0143 100644 --- a/fatcat-openapi2.yml +++ b/fatcat-openapi2.yml @@ -2711,4 +2711,30 @@ paths: schema: $ref: "#/definitions/error_response" <<: *AUTHRESPONSES + /auth/check: + get: + operationId: "auth_check" + tags: # TAGLINE + security: + # required admin privs + - Bearer: [] + parameters: + - name: role + in: query + required: false + type: string + responses: + 200: + description: Success + schema: + $ref: "#/definitions/success" + 400: + description: Bad Request + schema: + $ref: "#/definitions/error_response" + 500: + description: Generic Error + schema: + $ref: "#/definitions/error_response" + <<: *AUTHRESPONSES diff --git a/rust/HACKING.md b/rust/HACKING.md index 9d161b87..b3a551fa 100644 --- a/rust/HACKING.md +++ b/rust/HACKING.md @@ -54,6 +54,15 @@ Debug SQL schema errors (if diesel commands fail): ## Direct API Interaction +First setup an auth token and check that authentication is working + + EDITOR_ID='aaaaaaaaaaaabkvkaaaaaaaaay' + AUTH_TOKEN=`./target/debug/fatcat-auth create-token $EDITOR_ID` + http get :9411/v0/auth/check "Authorization:Bearer $AUTH_TOKEN" + http get :9411/v0/auth/check?role=admin "Authorization:Bearer $AUTH_TOKEN" + +You'll need to add the `$AUTH_TOKEN` bit to all requests below. + Creating entities via API: http --json post localhost:9411/v0/container name=asdf issn=1234-5678 diff --git a/rust/fatcat-api-spec/README.md b/rust/fatcat-api-spec/README.md index f81f641a..f8a6e817 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: 2019-01-04T05:57:03.701Z +- Build date: 2019-01-08T01:49:55.777Z This autogenerated project defines an API crate `fatcat` which contains: * An `Api` trait defining the API in Rust. @@ -79,6 +79,7 @@ cargo run --example client GetCreatorReleases cargo run --example client GetCreatorRevision cargo run --example client LookupCreator cargo run --example client UpdateCreator +cargo run --example client AuthCheck cargo run --example client AuthOidc cargo run --example client GetEditor cargo run --example client GetEditorChangelog diff --git a/rust/fatcat-api-spec/api.yaml b/rust/fatcat-api-spec/api.yaml index 501a1296..625a0143 100644 --- a/rust/fatcat-api-spec/api.yaml +++ b/rust/fatcat-api-spec/api.yaml @@ -2711,4 +2711,30 @@ paths: schema: $ref: "#/definitions/error_response" <<: *AUTHRESPONSES + /auth/check: + get: + operationId: "auth_check" + tags: # TAGLINE + security: + # required admin privs + - Bearer: [] + parameters: + - name: role + in: query + required: false + type: string + responses: + 200: + description: Success + schema: + $ref: "#/definitions/success" + 400: + description: Bad Request + schema: + $ref: "#/definitions/error_response" + 500: + description: Generic Error + schema: + $ref: "#/definitions/error_response" + <<: *AUTHRESPONSES diff --git a/rust/fatcat-api-spec/api/swagger.yaml b/rust/fatcat-api-spec/api/swagger.yaml index a19d6ae1..9d4767c0 100644 --- a/rust/fatcat-api-spec/api/swagger.yaml +++ b/rust/fatcat-api-spec/api/swagger.yaml @@ -6659,6 +6659,72 @@ paths: HttpMethod: "Post" httpmethod: "post" noClientExample: true + /auth/check: + get: + operationId: "auth_check" + parameters: + - name: "role" + in: "query" + required: false + type: "string" + formatString: "{:?}" + example: "Some(\"role_example\".to_string())" + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/success" + x-responseId: "Success" + x-uppercaseResponseId: "SUCCESS" + uppercase_operation_id: "AUTH_CHECK" + uppercase_data_type: "SUCCESS" + producesJson: true + 400: + description: "Bad Request" + schema: + $ref: "#/definitions/error_response" + x-responseId: "BadRequest" + x-uppercaseResponseId: "BAD_REQUEST" + uppercase_operation_id: "AUTH_CHECK" + uppercase_data_type: "ERRORRESPONSE" + producesJson: true + 401: + description: "Not Authorized" + schema: + $ref: "#/definitions/error_response" + headers: + WWW_Authenticate: + type: "string" + x-responseId: "NotAuthorized" + x-uppercaseResponseId: "NOT_AUTHORIZED" + uppercase_operation_id: "AUTH_CHECK" + uppercase_data_type: "ERRORRESPONSE" + producesJson: true + 403: + description: "Forbidden" + schema: + $ref: "#/definitions/error_response" + x-responseId: "Forbidden" + x-uppercaseResponseId: "FORBIDDEN" + uppercase_operation_id: "AUTH_CHECK" + uppercase_data_type: "ERRORRESPONSE" + producesJson: true + 500: + description: "Generic Error" + schema: + $ref: "#/definitions/error_response" + x-responseId: "GenericError" + x-uppercaseResponseId: "GENERIC_ERROR" + uppercase_operation_id: "AUTH_CHECK" + uppercase_data_type: "ERRORRESPONSE" + producesJson: true + security: + - Bearer: [] + operation_id: "auth_check" + uppercase_operation_id: "AUTH_CHECK" + path: "/auth/check" + HttpMethod: "Get" + httpmethod: "get" securityDefinitions: Bearer: type: "apiKey" diff --git a/rust/fatcat-api-spec/examples/client.rs b/rust/fatcat-api-spec/examples/client.rs index 4eed8ae4..5a43a33c 100644 --- a/rust/fatcat-api-spec/examples/client.rs +++ b/rust/fatcat-api-spec/examples/client.rs @@ -12,8 +12,8 @@ extern crate uuid; use clap::{App, Arg}; #[allow(unused_imports)] use fatcat::{ - AcceptEditgroupResponse, ApiError, ApiNoContext, AuthOidcResponse, ContextWrapperExt, CreateContainerBatchResponse, CreateContainerResponse, CreateCreatorBatchResponse, CreateCreatorResponse, - CreateEditgroupResponse, CreateFileBatchResponse, CreateFileResponse, CreateFilesetBatchResponse, CreateFilesetResponse, CreateReleaseBatchResponse, CreateReleaseResponse, + AcceptEditgroupResponse, ApiError, ApiNoContext, AuthCheckResponse, AuthOidcResponse, ContextWrapperExt, CreateContainerBatchResponse, CreateContainerResponse, CreateCreatorBatchResponse, + CreateCreatorResponse, CreateEditgroupResponse, CreateFileBatchResponse, CreateFileResponse, CreateFilesetBatchResponse, CreateFilesetResponse, CreateReleaseBatchResponse, CreateReleaseResponse, CreateWebcaptureBatchResponse, CreateWebcaptureResponse, CreateWorkBatchResponse, CreateWorkResponse, DeleteContainerEditResponse, DeleteContainerResponse, DeleteCreatorEditResponse, DeleteCreatorResponse, DeleteFileEditResponse, DeleteFileResponse, DeleteFilesetEditResponse, DeleteFilesetResponse, DeleteReleaseEditResponse, DeleteReleaseResponse, DeleteWebcaptureEditResponse, DeleteWebcaptureResponse, DeleteWorkEditResponse, DeleteWorkResponse, GetChangelogEntryResponse, GetChangelogResponse, GetContainerEditResponse, @@ -54,6 +54,7 @@ fn main() { "GetCreatorReleases", "GetCreatorRevision", "LookupCreator", + "AuthCheck", "GetEditor", "GetEditorChangelog", "AcceptEditgroup", @@ -272,6 +273,10 @@ fn main() { // let result = client.update_creator("ident_example".to_string(), ???, Some("editgroup_id_example".to_string())).wait(); // println!("{:?} (X-Span-ID: {:?})", result, client.context().x_span_id.clone().unwrap_or(String::from(""))); // }, + Some("AuthCheck") => { + let result = client.auth_check(Some("role_example".to_string())).wait(); + println!("{:?} (X-Span-ID: {:?})", result, client.context().x_span_id.clone().unwrap_or(String::from(""))); + } // Disabled because there's no example. // Some("AuthOidc") => { diff --git a/rust/fatcat-api-spec/examples/server_lib/server.rs b/rust/fatcat-api-spec/examples/server_lib/server.rs index 5e86a10e..73917351 100644 --- a/rust/fatcat-api-spec/examples/server_lib/server.rs +++ b/rust/fatcat-api-spec/examples/server_lib/server.rs @@ -11,7 +11,7 @@ use swagger; use fatcat::models; use fatcat::{ - AcceptEditgroupResponse, Api, ApiError, AuthOidcResponse, Context, CreateContainerBatchResponse, CreateContainerResponse, CreateCreatorBatchResponse, CreateCreatorResponse, + AcceptEditgroupResponse, Api, ApiError, AuthCheckResponse, AuthOidcResponse, Context, CreateContainerBatchResponse, CreateContainerResponse, CreateCreatorBatchResponse, CreateCreatorResponse, CreateEditgroupResponse, CreateFileBatchResponse, CreateFileResponse, CreateFilesetBatchResponse, CreateFilesetResponse, CreateReleaseBatchResponse, CreateReleaseResponse, CreateWebcaptureBatchResponse, CreateWebcaptureResponse, CreateWorkBatchResponse, CreateWorkResponse, DeleteContainerEditResponse, DeleteContainerResponse, DeleteCreatorEditResponse, DeleteCreatorResponse, DeleteFileEditResponse, DeleteFileResponse, DeleteFilesetEditResponse, DeleteFilesetResponse, DeleteReleaseEditResponse, DeleteReleaseResponse, @@ -297,6 +297,12 @@ impl Api for Server { Box::new(futures::failed("Generic failure".into())) } + fn auth_check(&self, role: Option, context: &Context) -> Box + Send> { + let context = context.clone(); + println!("auth_check({:?}) - X-Span-ID: {:?}", role, context.x_span_id.unwrap_or(String::from("")).clone()); + Box::new(futures::failed("Generic failure".into())) + } + fn auth_oidc(&self, oidc_params: models::AuthOidc, context: &Context) -> Box + Send> { let context = context.clone(); println!("auth_oidc({:?}) - X-Span-ID: {:?}", oidc_params, context.x_span_id.unwrap_or(String::from("")).clone()); diff --git a/rust/fatcat-api-spec/src/client.rs b/rust/fatcat-api-spec/src/client.rs index 470a5350..7f364eb4 100644 --- a/rust/fatcat-api-spec/src/client.rs +++ b/rust/fatcat-api-spec/src/client.rs @@ -35,18 +35,19 @@ use swagger::{ApiError, Context, XSpanId}; use models; use { - AcceptEditgroupResponse, Api, AuthOidcResponse, CreateContainerBatchResponse, CreateContainerResponse, CreateCreatorBatchResponse, CreateCreatorResponse, CreateEditgroupResponse, - CreateFileBatchResponse, CreateFileResponse, CreateFilesetBatchResponse, CreateFilesetResponse, CreateReleaseBatchResponse, CreateReleaseResponse, CreateWebcaptureBatchResponse, - CreateWebcaptureResponse, CreateWorkBatchResponse, CreateWorkResponse, DeleteContainerEditResponse, DeleteContainerResponse, DeleteCreatorEditResponse, DeleteCreatorResponse, - DeleteFileEditResponse, DeleteFileResponse, DeleteFilesetEditResponse, DeleteFilesetResponse, DeleteReleaseEditResponse, DeleteReleaseResponse, DeleteWebcaptureEditResponse, - DeleteWebcaptureResponse, DeleteWorkEditResponse, DeleteWorkResponse, GetChangelogEntryResponse, GetChangelogResponse, GetContainerEditResponse, GetContainerHistoryResponse, - GetContainerRedirectsResponse, GetContainerResponse, GetContainerRevisionResponse, GetCreatorEditResponse, GetCreatorHistoryResponse, GetCreatorRedirectsResponse, GetCreatorReleasesResponse, - GetCreatorResponse, GetCreatorRevisionResponse, GetEditgroupResponse, GetEditorChangelogResponse, GetEditorResponse, GetFileEditResponse, GetFileHistoryResponse, GetFileRedirectsResponse, - GetFileResponse, GetFileRevisionResponse, GetFilesetEditResponse, GetFilesetHistoryResponse, GetFilesetRedirectsResponse, GetFilesetResponse, GetFilesetRevisionResponse, GetReleaseEditResponse, - GetReleaseFilesResponse, GetReleaseFilesetsResponse, GetReleaseHistoryResponse, GetReleaseRedirectsResponse, GetReleaseResponse, GetReleaseRevisionResponse, GetReleaseWebcapturesResponse, - GetWebcaptureEditResponse, GetWebcaptureHistoryResponse, GetWebcaptureRedirectsResponse, GetWebcaptureResponse, GetWebcaptureRevisionResponse, GetWorkEditResponse, GetWorkHistoryResponse, - GetWorkRedirectsResponse, GetWorkReleasesResponse, GetWorkResponse, GetWorkRevisionResponse, LookupContainerResponse, LookupCreatorResponse, LookupFileResponse, LookupReleaseResponse, - UpdateContainerResponse, UpdateCreatorResponse, UpdateEditorResponse, UpdateFileResponse, UpdateFilesetResponse, UpdateReleaseResponse, UpdateWebcaptureResponse, UpdateWorkResponse, + AcceptEditgroupResponse, Api, AuthCheckResponse, AuthOidcResponse, CreateContainerBatchResponse, CreateContainerResponse, CreateCreatorBatchResponse, CreateCreatorResponse, + CreateEditgroupResponse, CreateFileBatchResponse, CreateFileResponse, CreateFilesetBatchResponse, CreateFilesetResponse, CreateReleaseBatchResponse, CreateReleaseResponse, + CreateWebcaptureBatchResponse, CreateWebcaptureResponse, CreateWorkBatchResponse, CreateWorkResponse, DeleteContainerEditResponse, DeleteContainerResponse, DeleteCreatorEditResponse, + DeleteCreatorResponse, DeleteFileEditResponse, DeleteFileResponse, DeleteFilesetEditResponse, DeleteFilesetResponse, DeleteReleaseEditResponse, DeleteReleaseResponse, + DeleteWebcaptureEditResponse, DeleteWebcaptureResponse, DeleteWorkEditResponse, DeleteWorkResponse, GetChangelogEntryResponse, GetChangelogResponse, GetContainerEditResponse, + GetContainerHistoryResponse, GetContainerRedirectsResponse, GetContainerResponse, GetContainerRevisionResponse, GetCreatorEditResponse, GetCreatorHistoryResponse, GetCreatorRedirectsResponse, + GetCreatorReleasesResponse, GetCreatorResponse, GetCreatorRevisionResponse, GetEditgroupResponse, GetEditorChangelogResponse, GetEditorResponse, GetFileEditResponse, GetFileHistoryResponse, + GetFileRedirectsResponse, GetFileResponse, GetFileRevisionResponse, GetFilesetEditResponse, GetFilesetHistoryResponse, GetFilesetRedirectsResponse, GetFilesetResponse, GetFilesetRevisionResponse, + GetReleaseEditResponse, GetReleaseFilesResponse, GetReleaseFilesetsResponse, GetReleaseHistoryResponse, GetReleaseRedirectsResponse, GetReleaseResponse, GetReleaseRevisionResponse, + GetReleaseWebcapturesResponse, GetWebcaptureEditResponse, GetWebcaptureHistoryResponse, GetWebcaptureRedirectsResponse, GetWebcaptureResponse, GetWebcaptureRevisionResponse, GetWorkEditResponse, + GetWorkHistoryResponse, GetWorkRedirectsResponse, GetWorkReleasesResponse, GetWorkResponse, GetWorkRevisionResponse, LookupContainerResponse, LookupCreatorResponse, LookupFileResponse, + LookupReleaseResponse, UpdateContainerResponse, UpdateCreatorResponse, UpdateEditorResponse, UpdateFileResponse, UpdateFilesetResponse, UpdateReleaseResponse, UpdateWebcaptureResponse, + UpdateWorkResponse, }; /// Convert input into a base path, e.g. "http://example:123". Also checks the scheme as it goes. @@ -2030,6 +2031,84 @@ impl Api for Client { Box::new(futures::done(result)) } + fn auth_check(&self, param_role: Option, context: &Context) -> Box + Send> { + // Query parameters + let query_role = param_role.map_or_else(String::new, |query| format!("role={role}&", role = query.to_string())); + + let url = format!("{}/v0/auth/check?{role}", self.base_path, role = utf8_percent_encode(&query_role, QUERY_ENCODE_SET)); + + let hyper_client = (self.hyper_client)(); + let request = hyper_client.request(hyper::method::Method::Get, &url); + let mut custom_headers = hyper::header::Headers::new(); + + context.x_span_id.as_ref().map(|header| custom_headers.set(XSpanId(header.clone()))); + + let request = request.headers(custom_headers); + + // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). + fn parse_response(mut response: hyper::client::response::Response) -> Result { + match response.status.to_u16() { + 200 => { + let mut buf = String::new(); + response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; + let body = serde_json::from_str::(&buf)?; + + Ok(AuthCheckResponse::Success(body)) + } + 400 => { + let mut buf = String::new(); + response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; + let body = serde_json::from_str::(&buf)?; + + Ok(AuthCheckResponse::BadRequest(body)) + } + 401 => { + let mut buf = String::new(); + response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; + let body = serde_json::from_str::(&buf)?; + header! { (ResponseWwwAuthenticate, "WWW_Authenticate") => [String] } + let response_www_authenticate = response + .headers + .get::() + .ok_or_else(|| "Required response header WWW_Authenticate for response 401 was not found.")?; + + Ok(AuthCheckResponse::NotAuthorized { + body: body, + www_authenticate: response_www_authenticate.0.clone(), + }) + } + 403 => { + let mut buf = String::new(); + response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; + let body = serde_json::from_str::(&buf)?; + + Ok(AuthCheckResponse::Forbidden(body)) + } + 500 => { + let mut buf = String::new(); + response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; + let body = serde_json::from_str::(&buf)?; + + Ok(AuthCheckResponse::GenericError(body)) + } + code => { + let mut buf = [0; 100]; + let debug_body = match response.read(&mut buf) { + Ok(len) => match str::from_utf8(&buf[..len]) { + Ok(body) => Cow::from(body), + Err(_) => Cow::from(format!("", &buf[..len].to_vec())), + }, + Err(e) => Cow::from(format!("", e)), + }; + Err(ApiError(format!("Unexpected response code {}:\n{:?}\n\n{}", code, response.headers, debug_body))) + } + } + } + + let result = request.send().map_err(|e| ApiError(format!("No response received: {}", e))).and_then(parse_response); + Box::new(futures::done(result)) + } + fn auth_oidc(&self, param_oidc_params: models::AuthOidc, context: &Context) -> Box + Send> { let url = format!("{}/v0/auth/oidc", self.base_path); diff --git a/rust/fatcat-api-spec/src/lib.rs b/rust/fatcat-api-spec/src/lib.rs index 258b635b..17c74384 100644 --- a/rust/fatcat-api-spec/src/lib.rs +++ b/rust/fatcat-api-spec/src/lib.rs @@ -348,6 +348,20 @@ pub enum UpdateCreatorResponse { GenericError(models::ErrorResponse), } +#[derive(Debug, PartialEq)] +pub enum AuthCheckResponse { + /// Success + Success(models::Success), + /// Bad Request + BadRequest(models::ErrorResponse), + /// Not Authorized + NotAuthorized { body: models::ErrorResponse, www_authenticate: String }, + /// Forbidden + Forbidden(models::ErrorResponse), + /// Generic Error + GenericError(models::ErrorResponse), +} + #[derive(Debug, PartialEq)] pub enum AuthOidcResponse { /// Found @@ -1315,6 +1329,8 @@ pub trait Api { fn update_creator(&self, ident: String, entity: models::CreatorEntity, editgroup_id: Option, context: &Context) -> Box + Send>; + fn auth_check(&self, role: Option, context: &Context) -> Box + Send>; + fn auth_oidc(&self, oidc_params: models::AuthOidc, context: &Context) -> Box + Send>; fn get_editor(&self, editor_id: String, context: &Context) -> Box + Send>; @@ -1565,6 +1581,8 @@ pub trait ApiNoContext { fn update_creator(&self, ident: String, entity: models::CreatorEntity, editgroup_id: Option) -> Box + Send>; + fn auth_check(&self, role: Option) -> Box + Send>; + fn auth_oidc(&self, oidc_params: models::AuthOidc) -> Box + Send>; fn get_editor(&self, editor_id: String) -> Box + Send>; @@ -1852,6 +1870,10 @@ impl<'a, T: Api> ApiNoContext for ContextWrapper<'a, T> { self.api().update_creator(ident, entity, editgroup_id, &self.context()) } + fn auth_check(&self, role: Option) -> Box + Send> { + self.api().auth_check(role, &self.context()) + } + fn auth_oidc(&self, oidc_params: models::AuthOidc) -> Box + Send> { self.api().auth_oidc(oidc_params, &self.context()) } diff --git a/rust/fatcat-api-spec/src/mimetypes.rs b/rust/fatcat-api-spec/src/mimetypes.rs index cfdd357d..83add9e3 100644 --- a/rust/fatcat-api-spec/src/mimetypes.rs +++ b/rust/fatcat-api-spec/src/mimetypes.rs @@ -452,6 +452,26 @@ pub mod responses { lazy_static! { pub static ref UPDATE_CREATOR_GENERIC_ERROR: Mime = mime!(Application / Json); } + /// Create Mime objects for the response content types for AuthCheck + lazy_static! { + pub static ref AUTH_CHECK_SUCCESS: Mime = mime!(Application / Json); + } + /// Create Mime objects for the response content types for AuthCheck + lazy_static! { + pub static ref AUTH_CHECK_BAD_REQUEST: Mime = mime!(Application / Json); + } + /// Create Mime objects for the response content types for AuthCheck + lazy_static! { + pub static ref AUTH_CHECK_NOT_AUTHORIZED: Mime = mime!(Application / Json); + } + /// Create Mime objects for the response content types for AuthCheck + lazy_static! { + pub static ref AUTH_CHECK_FORBIDDEN: Mime = mime!(Application / Json); + } + /// Create Mime objects for the response content types for AuthCheck + lazy_static! { + pub static ref AUTH_CHECK_GENERIC_ERROR: Mime = mime!(Application / Json); + } /// Create Mime objects for the response content types for AuthOidc lazy_static! { pub static ref AUTH_OIDC_FOUND: Mime = mime!(Application / Json); diff --git a/rust/fatcat-api-spec/src/server.rs b/rust/fatcat-api-spec/src/server.rs index c0903676..d8fc7dc2 100644 --- a/rust/fatcat-api-spec/src/server.rs +++ b/rust/fatcat-api-spec/src/server.rs @@ -37,18 +37,19 @@ use swagger::{ApiError, Context, XSpanId}; #[allow(unused_imports)] use models; use { - AcceptEditgroupResponse, Api, AuthOidcResponse, CreateContainerBatchResponse, CreateContainerResponse, CreateCreatorBatchResponse, CreateCreatorResponse, CreateEditgroupResponse, - CreateFileBatchResponse, CreateFileResponse, CreateFilesetBatchResponse, CreateFilesetResponse, CreateReleaseBatchResponse, CreateReleaseResponse, CreateWebcaptureBatchResponse, - CreateWebcaptureResponse, CreateWorkBatchResponse, CreateWorkResponse, DeleteContainerEditResponse, DeleteContainerResponse, DeleteCreatorEditResponse, DeleteCreatorResponse, - DeleteFileEditResponse, DeleteFileResponse, DeleteFilesetEditResponse, DeleteFilesetResponse, DeleteReleaseEditResponse, DeleteReleaseResponse, DeleteWebcaptureEditResponse, - DeleteWebcaptureResponse, DeleteWorkEditResponse, DeleteWorkResponse, GetChangelogEntryResponse, GetChangelogResponse, GetContainerEditResponse, GetContainerHistoryResponse, - GetContainerRedirectsResponse, GetContainerResponse, GetContainerRevisionResponse, GetCreatorEditResponse, GetCreatorHistoryResponse, GetCreatorRedirectsResponse, GetCreatorReleasesResponse, - GetCreatorResponse, GetCreatorRevisionResponse, GetEditgroupResponse, GetEditorChangelogResponse, GetEditorResponse, GetFileEditResponse, GetFileHistoryResponse, GetFileRedirectsResponse, - GetFileResponse, GetFileRevisionResponse, GetFilesetEditResponse, GetFilesetHistoryResponse, GetFilesetRedirectsResponse, GetFilesetResponse, GetFilesetRevisionResponse, GetReleaseEditResponse, - GetReleaseFilesResponse, GetReleaseFilesetsResponse, GetReleaseHistoryResponse, GetReleaseRedirectsResponse, GetReleaseResponse, GetReleaseRevisionResponse, GetReleaseWebcapturesResponse, - GetWebcaptureEditResponse, GetWebcaptureHistoryResponse, GetWebcaptureRedirectsResponse, GetWebcaptureResponse, GetWebcaptureRevisionResponse, GetWorkEditResponse, GetWorkHistoryResponse, - GetWorkRedirectsResponse, GetWorkReleasesResponse, GetWorkResponse, GetWorkRevisionResponse, LookupContainerResponse, LookupCreatorResponse, LookupFileResponse, LookupReleaseResponse, - UpdateContainerResponse, UpdateCreatorResponse, UpdateEditorResponse, UpdateFileResponse, UpdateFilesetResponse, UpdateReleaseResponse, UpdateWebcaptureResponse, UpdateWorkResponse, + AcceptEditgroupResponse, Api, AuthCheckResponse, AuthOidcResponse, CreateContainerBatchResponse, CreateContainerResponse, CreateCreatorBatchResponse, CreateCreatorResponse, + CreateEditgroupResponse, CreateFileBatchResponse, CreateFileResponse, CreateFilesetBatchResponse, CreateFilesetResponse, CreateReleaseBatchResponse, CreateReleaseResponse, + CreateWebcaptureBatchResponse, CreateWebcaptureResponse, CreateWorkBatchResponse, CreateWorkResponse, DeleteContainerEditResponse, DeleteContainerResponse, DeleteCreatorEditResponse, + DeleteCreatorResponse, DeleteFileEditResponse, DeleteFileResponse, DeleteFilesetEditResponse, DeleteFilesetResponse, DeleteReleaseEditResponse, DeleteReleaseResponse, + DeleteWebcaptureEditResponse, DeleteWebcaptureResponse, DeleteWorkEditResponse, DeleteWorkResponse, GetChangelogEntryResponse, GetChangelogResponse, GetContainerEditResponse, + GetContainerHistoryResponse, GetContainerRedirectsResponse, GetContainerResponse, GetContainerRevisionResponse, GetCreatorEditResponse, GetCreatorHistoryResponse, GetCreatorRedirectsResponse, + GetCreatorReleasesResponse, GetCreatorResponse, GetCreatorRevisionResponse, GetEditgroupResponse, GetEditorChangelogResponse, GetEditorResponse, GetFileEditResponse, GetFileHistoryResponse, + GetFileRedirectsResponse, GetFileResponse, GetFileRevisionResponse, GetFilesetEditResponse, GetFilesetHistoryResponse, GetFilesetRedirectsResponse, GetFilesetResponse, GetFilesetRevisionResponse, + GetReleaseEditResponse, GetReleaseFilesResponse, GetReleaseFilesetsResponse, GetReleaseHistoryResponse, GetReleaseRedirectsResponse, GetReleaseResponse, GetReleaseRevisionResponse, + GetReleaseWebcapturesResponse, GetWebcaptureEditResponse, GetWebcaptureHistoryResponse, GetWebcaptureRedirectsResponse, GetWebcaptureResponse, GetWebcaptureRevisionResponse, GetWorkEditResponse, + GetWorkHistoryResponse, GetWorkRedirectsResponse, GetWorkReleasesResponse, GetWorkResponse, GetWorkRevisionResponse, LookupContainerResponse, LookupCreatorResponse, LookupFileResponse, + LookupReleaseResponse, UpdateContainerResponse, UpdateCreatorResponse, UpdateEditorResponse, UpdateFileResponse, UpdateFilesetResponse, UpdateReleaseResponse, UpdateWebcaptureResponse, + UpdateWorkResponse, }; header! { (Warning, "Warning") => [String] } @@ -2605,6 +2606,99 @@ where "UpdateCreator", ); + let api_clone = api.clone(); + router.get( + "/v0/auth/check", + move |req: &mut Request| { + let mut context = Context::default(); + + // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). + fn handle_request(req: &mut Request, api: &T, context: &mut Context) -> Result + where + T: Api, + { + context.x_span_id = Some(req.headers.get::().map(XSpanId::to_string).unwrap_or_else(|| self::uuid::Uuid::new_v4().to_string())); + context.auth_data = req.extensions.remove::(); + context.authorization = req.extensions.remove::(); + + let authorization = context.authorization.as_ref().ok_or_else(|| Response::with((status::Forbidden, "Unauthenticated".to_string())))?; + + // Query parameters (note that non-required or collection query parameters will ignore garbage values, rather than causing a 400 response) + let query_params = req.get::().unwrap_or_default(); + let param_role = query_params.get("role").and_then(|list| list.first()).and_then(|x| x.parse::().ok()); + + match api.auth_check(param_role, context).wait() { + Ok(rsp) => match rsp { + AuthCheckResponse::Success(body) => { + let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + + let mut response = Response::with((status::Status::from_u16(200), body_string)); + response.headers.set(ContentType(mimetypes::responses::AUTH_CHECK_SUCCESS.clone())); + + context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + + Ok(response) + } + AuthCheckResponse::BadRequest(body) => { + let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + + let mut response = Response::with((status::Status::from_u16(400), body_string)); + response.headers.set(ContentType(mimetypes::responses::AUTH_CHECK_BAD_REQUEST.clone())); + + context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + + Ok(response) + } + AuthCheckResponse::NotAuthorized { body, www_authenticate } => { + let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + + let mut response = Response::with((status::Status::from_u16(401), body_string)); + header! { (ResponseWwwAuthenticate, "WWW_Authenticate") => [String] } + response.headers.set(ResponseWwwAuthenticate(www_authenticate)); + + response.headers.set(ContentType(mimetypes::responses::AUTH_CHECK_NOT_AUTHORIZED.clone())); + + context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + + Ok(response) + } + AuthCheckResponse::Forbidden(body) => { + let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + + let mut response = Response::with((status::Status::from_u16(403), body_string)); + response.headers.set(ContentType(mimetypes::responses::AUTH_CHECK_FORBIDDEN.clone())); + + context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + + Ok(response) + } + AuthCheckResponse::GenericError(body) => { + let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + + let mut response = Response::with((status::Status::from_u16(500), body_string)); + response.headers.set(ContentType(mimetypes::responses::AUTH_CHECK_GENERIC_ERROR.clone())); + + context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + + Ok(response) + } + }, + Err(_) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + Err(Response::with((status::InternalServerError, "An internal error occurred".to_string()))) + } + } + } + + handle_request(req, &api_clone, &mut context).or_else(|mut response| { + context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + Ok(response) + }) + }, + "AuthCheck", + ); + let api_clone = api.clone(); router.post( "/v0/auth/oidc", diff --git a/rust/src/api_wrappers.rs b/rust/src/api_wrappers.rs index 614a0007..818a41c2 100644 --- a/rust/src/api_wrappers.rs +++ b/rust/src/api_wrappers.rs @@ -404,8 +404,7 @@ macro_rules! wrap_entity_handlers { $model::db_delete_edit(&conn, edit_id) }) { Ok(()) => - $delete_edit_resp::DeletedEdit(Success { message: format!("Successfully deleted work-in-progress {} edit: {}", stringify!($model), edit_id) } ), - Err(Error(ErrorKind::Diesel(::diesel::result::Error::NotFound), _)) => + $delete_edit_resp::DeletedEdit(Success { message: format!("Successfully deleted work-in-progress {} edit: {}", stringify!($model), edit_id) } ), Err(Error(ErrorKind::Diesel(::diesel::result::Error::NotFound), _)) => $delete_edit_resp::NotFound(ErrorResponse { message: format!("No such {} edit: {}", stringify!($model), edit_id) }), Err(Error(ErrorKind::Diesel(e), _)) => $delete_edit_resp::BadRequest(ErrorResponse { message: e.to_string() }), @@ -918,9 +917,11 @@ impl Api for Server { ErrorKind::OtherBadRequest("editor_id doesn't match".to_string()).into(), ); } - let auth_context = self - .auth_confectionary - .require_auth(&conn, &context.auth_data, Some("update_editor"))?; + let auth_context = self.auth_confectionary.require_auth( + &conn, + &context.auth_data, + Some("update_editor"), + )?; let editor_id = FatCatId::from_str(&editor_id)?; // DANGER! these permissions are for username updates only! if editor_id == auth_context.editor_id { @@ -986,9 +987,11 @@ impl Api for Server { 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, Some("accept_editgroup"))?; + let auth_context = self.auth_confectionary.require_auth( + &conn, + &context.auth_data, + Some("accept_editgroup"), + )?; auth_context.require_role(FatcatRole::Admin)?; // NOTE: this is currently redundant, but zero-cost auth_context.require_editgroup(&conn, editgroup_id)?; @@ -1058,9 +1061,11 @@ impl Api for Server { ) -> 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, Some("create_editgroup"))?; + let auth_context = self.auth_confectionary.require_auth( + &conn, + &context.auth_data, + Some("create_editgroup"), + )?; auth_context.require_role(FatcatRole::Editor)?; let mut entity = entity.clone(); match entity.editor_id.clone() { @@ -1149,9 +1154,11 @@ impl Api for Server { ) -> 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, Some("auth_oidc"))?; + let auth_context = self.auth_confectionary.require_auth( + &conn, + &context.auth_data, + Some("auth_oidc"), + )?; auth_context.require_role(FatcatRole::Superuser)?; let (editor, created) = self.auth_oidc_handler(params, &conn)?; // create an auth token; leave it to webface to attenuate to a given duration @@ -1221,4 +1228,65 @@ impl Api for Server { }; Box::new(futures::done(Ok(ret))) } + + fn auth_check( + &self, + role: Option, + 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, + Some("auth_check"), + )?; + if let Some(role) = role { + let role = match role.to_lowercase().as_ref() { + "superuser" => FatcatRole::Superuser, + "admin" => FatcatRole::Admin, + "editor" => FatcatRole::Editor, + "bot" => FatcatRole::Bot, + "human" => FatcatRole::Human, + "public" => FatcatRole::Public, + _ => bail!("unknown auth role: {}", role), + }; + auth_context.require_role(role)?; + }; + Ok(()) + }) { + Ok(()) => AuthCheckResponse::Success(Success { + message: "auth check successful!".to_string() }), + Err(Error(ErrorKind::Diesel(e), _)) => AuthCheckResponse::BadRequest(ErrorResponse { + message: e.to_string(), + }), + Err(Error(ErrorKind::Uuid(e), _)) => AuthCheckResponse::BadRequest(ErrorResponse { + message: e.to_string(), + }), + Err(Error(ErrorKind::InvalidCredentials(e), _)) => + // TODO: why can't I NotAuthorized here? + { + AuthCheckResponse::Forbidden(ErrorResponse { + message: e.to_string(), + }) + }, + Err(Error(ErrorKind::InsufficientPrivileges(e), _)) => { + AuthCheckResponse::Forbidden(ErrorResponse { + message: e.to_string(), + }) + }, + Err(Error(ErrorKind::OtherBadRequest(e), _)) => { + AuthCheckResponse::BadRequest(ErrorResponse { + message: e.to_string(), + }) + }, + Err(e) => { + error!("{}", e); + AuthCheckResponse::GenericError(ErrorResponse { + message: e.to_string(), + }) + }, + }; + Box::new(futures::done(Ok(ret))) + } } diff --git a/rust/src/auth.rs b/rust/src/auth.rs index 8894e33b..c20b9b71 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -56,9 +56,10 @@ impl AuthContext { pub fn require_role(&self, role: FatcatRole) -> Result<()> { match self.has_role(role) { true => Ok(()), - false => Err(ErrorKind::InsufficientPrivileges( - format!("doesn't have required role: {:?}", role), - ) + false => Err(ErrorKind::InsufficientPrivileges(format!( + "doesn't have required role: {:?}", + role + )) .into()), } } @@ -230,7 +231,12 @@ impl AuthConfectionary { Ok(BASE64.encode(&raw)) } - pub fn parse_macaroon_token(&self, conn: &DbConn, s: &str, endpoint: Option<&str>) -> Result { + pub fn parse_macaroon_token( + &self, + conn: &DbConn, + s: &str, + endpoint: Option<&str>, + ) -> Result { let raw = BASE64.decode(s.as_bytes())?; let mac = match Macaroon::deserialize(&raw) { Ok(m) => m, @@ -371,7 +377,12 @@ impl AuthConfectionary { })) } - pub fn require_auth(&self, conn: &DbConn, auth_data: &Option, endpoint: Option<&str>) -> Result { + pub fn require_auth( + &self, + conn: &DbConn, + auth_data: &Option, + endpoint: Option<&str>, + ) -> Result { match self.parse_swagger(conn, auth_data, endpoint)? { Some(auth) => Ok(auth), None => Err(ErrorKind::InvalidCredentials("no token supplied".to_string()).into()), -- cgit v1.2.3 From b44fd9a78c1505258178a51201e8abea9977baa5 Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Tue, 8 Jan 2019 15:27:05 -0800 Subject: /auth/oidc endpoint has 31 day limit --- rust/src/api_wrappers.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rust/src/api_wrappers.rs') diff --git a/rust/src/api_wrappers.rs b/rust/src/api_wrappers.rs index 818a41c2..22b5f857 100644 --- a/rust/src/api_wrappers.rs +++ b/rust/src/api_wrappers.rs @@ -1161,10 +1161,10 @@ impl Api for Server { )?; auth_context.require_role(FatcatRole::Superuser)?; let (editor, created) = self.auth_oidc_handler(params, &conn)?; - // create an auth token; leave it to webface to attenuate to a given duration + // create an auth token with 31 day duration let token = self.auth_confectionary.create_token( FatCatId::from_str(&editor.editor_id.clone().unwrap())?, - None, + Some(chrono::Duration::days(31)), )?; let result = AuthOidcResult { editor, token }; Ok((result, created)) -- cgit v1.2.3 From a2cbc5b4d88f3385ebed5b70e8cce19e00e81a8f Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Tue, 8 Jan 2019 16:01:13 -0800 Subject: rust fmt --- rust/src/api_wrappers.rs | 11 ++++++----- rust/src/auth.rs | 46 +++++++++++++++++++++++++--------------------- rust/tests/test_auth.rs | 3 ++- 3 files changed, 33 insertions(+), 27 deletions(-) (limited to 'rust/src/api_wrappers.rs') diff --git a/rust/src/api_wrappers.rs b/rust/src/api_wrappers.rs index 22b5f857..f03d4041 100644 --- a/rust/src/api_wrappers.rs +++ b/rust/src/api_wrappers.rs @@ -1256,7 +1256,8 @@ impl Api for Server { Ok(()) }) { Ok(()) => AuthCheckResponse::Success(Success { - message: "auth check successful!".to_string() }), + message: "auth check successful!".to_string(), + }), Err(Error(ErrorKind::Diesel(e), _)) => AuthCheckResponse::BadRequest(ErrorResponse { message: e.to_string(), }), @@ -1269,23 +1270,23 @@ impl Api for Server { AuthCheckResponse::Forbidden(ErrorResponse { message: e.to_string(), }) - }, + } Err(Error(ErrorKind::InsufficientPrivileges(e), _)) => { AuthCheckResponse::Forbidden(ErrorResponse { message: e.to_string(), }) - }, + } Err(Error(ErrorKind::OtherBadRequest(e), _)) => { AuthCheckResponse::BadRequest(ErrorResponse { message: e.to_string(), }) - }, + } Err(e) => { error!("{}", e); AuthCheckResponse::GenericError(ErrorResponse { message: e.to_string(), }) - }, + } }; Box::new(futures::done(Ok(ret))) } diff --git a/rust/src/auth.rs b/rust/src/auth.rs index acee147e..d4e03ecf 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -243,20 +243,22 @@ impl AuthConfectionary { Ok(m) => m, Err(e) => { // TODO: should be "chaining" here - return Err( - ErrorKind::InvalidCredentials( - format!("macaroon deserialize error: {:?}", e)).into(), - ); + return Err(ErrorKind::InvalidCredentials(format!( + "macaroon deserialize error: {:?}", + e + )) + .into()); } }; let mac = match mac.validate() { Ok(m) => m, Err(e) => { // TODO: should be "chaining" here - return Err( - ErrorKind::InvalidCredentials( - format!("macaroon validate error: {:?}", e)).into(), - ); + return Err(ErrorKind::InvalidCredentials(format!( + "macaroon validate error: {:?}", + e + )) + .into()); } }; let mut verifier = Verifier::new(); @@ -270,10 +272,11 @@ impl AuthConfectionary { let editor_id = match editor_id { Some(id) => id, None => { - return Err( - ErrorKind::InvalidCredentials("expected an editor_id caveat".to_string()).into(), - ); - }, + return Err(ErrorKind::InvalidCredentials( + "expected an editor_id caveat".to_string(), + ) + .into()); + } }; verifier.satisfy_exact(&format!("editor_id = {}", editor_id.to_string())); if let Some(endpoint) = endpoint { @@ -294,10 +297,11 @@ impl AuthConfectionary { let created = match created { Some(c) => c, None => { - return Err( - ErrorKind::InvalidCredentials("expected a 'created' (time >) caveat".to_string()).into(), - ); - }, + return Err(ErrorKind::InvalidCredentials( + "expected a 'created' (time >) caveat".to_string(), + ) + .into()); + } }; verifier.satisfy_exact(&format!( "time > {}", @@ -327,9 +331,10 @@ impl AuthConfectionary { let verify_key = match self.root_keys.get(mac.identifier()) { Some(key) => key, None => { - return Err(ErrorKind::InvalidCredentials( - format!("no valid auth signing key for identifier: {}", mac.identifier()) - ) + return Err(ErrorKind::InvalidCredentials(format!( + "no valid auth signing key for identifier: {}", + mac.identifier() + )) .into()); } }; @@ -344,8 +349,7 @@ impl AuthConfectionary { Err(e) => { // TODO: chain return Err( - ErrorKind::InvalidCredentials( - format!("token parsing failed: {:?}", e)).into(), + ErrorKind::InvalidCredentials(format!("token parsing failed: {:?}", e)).into(), ); } } diff --git a/rust/tests/test_auth.rs b/rust/tests/test_auth.rs index 412b67e8..1509bab4 100644 --- a/rust/tests/test_auth.rs +++ b/rust/tests/test_auth.rs @@ -18,7 +18,8 @@ fn test_macaroons() { c.create_token(editor_id, None).unwrap(); // create token w/ expiration - c.create_token(editor_id, Some(chrono::Duration::days(1))).unwrap(); + c.create_token(editor_id, Some(chrono::Duration::days(1))) + .unwrap(); } #[test] -- cgit v1.2.3