//! API server endpoint request/response wrappers //! //! These mostly deal with type conversion between internal function signatures and API-defined //! response types (mapping to HTTP statuses. Some contain actual endpoint implementations, but //! most implementation lives in the server module. // using closures as a Result/? hack #![allow(clippy::redundant_closure_call)] use crate::auth::FatcatRole; use crate::database_models::EntityEditRow; use crate::editing::*; use crate::editing_crud::{EditgroupAnnotationCrud, EditgroupCrud, EditorCrud}; use crate::entity_crud::{EntityCrud, ExpandFlags, HideFlags}; use crate::errors::*; use crate::identifiers::FatcatId; use crate::server::*; use cadence::prelude::*; use diesel::Connection; use fatcat_openapi::models; use fatcat_openapi::models::*; use fatcat_openapi::*; use futures::{self, Future}; use sentry::integrations::failure::capture_fail; use std::cmp; use std::str::FromStr; use uuid::{self, Uuid}; // This makes response matching below *much* more terse use crate::errors::FatcatError::*; // I heard you liked macros, so I use macros in your macros macro_rules! generic_auth_err_responses { ($val:ident, $resp_type:ident) => { //use crate::errors::FatcatError::*; match $val { NotFound(_, _) | DatabaseRowNotFound => $resp_type::NotFound($val.into()), InvalidCredentials(_) | InsufficientPrivileges(_) => $resp_type::Forbidden($val.into()), DatabaseError(_) | InternalError(_) => { error!("{}", $val); capture_fail(&$val); $resp_type::GenericError($val.into()) } _ => $resp_type::BadRequest($val.into()), } }; } macro_rules! generic_err_responses { ($val:ident, $resp_type:ident) => { //use crate::errors::FatcatError::*; match $val { NotFound(_, _) | DatabaseRowNotFound => $resp_type::NotFound($val.into()), DatabaseError(_) | InternalError(_) => { error!("{}", $val); capture_fail(&$val); $resp_type::GenericError($val.into()) } _ => $resp_type::BadRequest($val.into()), } }; } /// Helper for generating wrappers (which return "Box::new(futures::done(Ok(BLAH)))" like the /// codegen fatcat-openapi code wants) that call through to actual helpers (which have simple /// Result<> return types) macro_rules! wrap_entity_handlers { // Would much rather just have entity ident, then generate the other fields from that, but Rust // stable doesn't have a mechanism to "concat" or generate new identifiers in macros, at least // in the context of defining new functions. // The only stable approach I know of would be: https://github.com/dtolnay/mashup ($get_fn:ident, $get_resp:ident, $post_fn:ident, $post_resp:ident, $auto_batch_type:ident, $post_auto_batch_fn:ident, $post_auto_batch_handler:ident, $post_auto_batch_resp:ident, $update_fn:ident, $update_resp:ident, $delete_fn:ident, $delete_resp:ident, $get_history_fn:ident, $get_history_resp:ident, $get_edit_fn:ident, $get_edit_resp:ident, $delete_edit_fn:ident, $delete_edit_resp:ident, $get_rev_fn:ident, $get_rev_resp:ident, $get_redirects_fn:ident, $get_redirects_resp:ident, $model:ident) => { fn $get_fn( &self, ident: String, expand: Option<String>, hide: Option<String>, _context: &Context, ) -> Box<dyn Future<Item = $get_resp, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); // No transaction for GET let ret = match (|| { let entity_id = FatcatId::from_str(&ident)?; let hide_flags = match hide { None => HideFlags::none(), Some(param) => HideFlags::from_str(¶m)?, }; match expand { None => $model::db_get(&conn, entity_id, hide_flags), Some(param) => { let expand_flags = ExpandFlags::from_str(¶m)?; let mut entity = $model::db_get(&conn, entity_id, hide_flags)?; entity.db_expand(&conn, expand_flags)?; Ok(entity) }, } })().map_err(|e| FatcatError::from(e)) { Ok(entity) => $get_resp::FoundEntity(entity), Err(fe) => generic_err_responses!(fe, $get_resp), }; Box::new(futures::done(Ok(ret))) } fn $post_fn( &self, editgroup_id: String, entity: models::$model, context: &Context, ) -> Box<dyn Future<Item = $post_resp, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); let ret = match conn.transaction(|| { let editgroup_id = FatcatId::from_str(&editgroup_id)?; let auth_context = self.auth_confectionary.require_auth(&conn, &context.auth_data, Some(stringify!($post_fn)))?; auth_context.require_role(FatcatRole::Editor)?; auth_context.require_editgroup(&conn, editgroup_id)?; let edit_context = make_edit_context(auth_context.editor_id, editgroup_id, false)?; edit_context.check(&conn)?; entity.db_create(&conn, &edit_context)?.into_model() }).map_err(|e| FatcatError::from(e)) { Ok(edit) => { self.metrics.incr("entities.created").ok(); $post_resp::CreatedEntity(edit) }, Err(fe) => generic_auth_err_responses!(fe, $post_resp), }; Box::new(futures::done(Ok(ret))) } fn $post_auto_batch_fn( &self, auto_batch: $auto_batch_type, context: &Context, ) -> Box<dyn Future<Item = $post_auto_batch_resp, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); let ret = match conn.transaction(|| { let auth_context = self.auth_confectionary.require_auth(&conn, &context.auth_data, Some(stringify!($post_auto_batch_fn)))?; auth_context.require_role(FatcatRole::Admin)?; let mut editgroup = auto_batch.editgroup.clone(); // TODO: this is duplicated code with create_editgroup() match editgroup.editor_id.clone() { Some(editor_id) => { if editor_id != auth_context.editor_id.to_string() && !auth_context.has_role(FatcatRole::Admin) { return Err(FatcatError::InsufficientPrivileges( "not authorized to create editgroups in others' names".to_string() )) } } None => { editgroup.editor_id = Some(auth_context.editor_id.to_string()); } }; self.$post_auto_batch_handler(&conn, editgroup, &auto_batch.entity_list, auth_context.editor_id) .map_err(|e| FatcatError::from(e)) }) { Ok(editgroup) => { // TODO: need a count helper on editgroup //self.metrics.count("entities.created", count as i64).ok(); self.metrics.incr("editgroup.created").ok(); self.metrics.incr("editgroup.accepted").ok(); $post_auto_batch_resp::CreatedEditgroup(editgroup) }, Err(fe) => generic_auth_err_responses!(fe, $post_auto_batch_resp), }; Box::new(futures::done(Ok(ret))) } fn $update_fn( &self, editgroup_id: String, ident: String, entity: models::$model, context: &Context, ) -> Box<dyn Future<Item = $update_resp, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); let ret = match conn.transaction(|| { let editgroup_id = FatcatId::from_str(&editgroup_id)?; let auth_context = self.auth_confectionary.require_auth(&conn, &context.auth_data, Some(stringify!($update_fn)))?; auth_context.require_role(FatcatRole::Editor)?; let entity_id = FatcatId::from_str(&ident)?; auth_context.require_editgroup(&conn, editgroup_id)?; let edit_context = make_edit_context(auth_context.editor_id, editgroup_id, false)?; edit_context.check(&conn)?; entity.db_update(&conn, &edit_context, entity_id)?.into_model() }).map_err(|e| FatcatError::from(e)) { Ok(edit) => { self.metrics.incr("entities.updated").ok(); $update_resp::UpdatedEntity(edit) }, Err(fe) => generic_auth_err_responses!(fe, $update_resp), }; Box::new(futures::done(Ok(ret))) } fn $delete_fn( &self, editgroup_id: String, ident: String, context: &Context, ) -> Box<dyn Future<Item = $delete_resp, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); let ret = match conn.transaction(|| { let editgroup_id = FatcatId::from_str(&editgroup_id)?; let auth_context = self.auth_confectionary.require_auth(&conn, &context.auth_data, Some(stringify!($delete_fn)))?; auth_context.require_role(FatcatRole::Editor)?; let entity_id = FatcatId::from_str(&ident)?; auth_context.require_editgroup(&conn, editgroup_id)?; let edit_context = make_edit_context(auth_context.editor_id, editgroup_id, false)?; edit_context.check(&conn)?; $model::db_delete(&conn, &edit_context, entity_id)?.into_model() }).map_err(|e| FatcatError::from(e)) { Ok(edit) => { self.metrics.incr("entities.deleted").ok(); $delete_resp::DeletedEntity(edit) }, Err(fe) => generic_auth_err_responses!(fe, $delete_resp), }; Box::new(futures::done(Ok(ret))) } fn $get_history_fn( &self, ident: String, limit: Option<i64>, _context: &Context, ) -> Box<dyn Future<Item = $get_history_resp, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); // No transaction for GET? let ret = match (|| { let entity_id = FatcatId::from_str(&ident)?; $model::db_get_history(&conn, entity_id, limit) })().map_err(|e| FatcatError::from(e)) { Ok(history) => $get_history_resp::FoundEntityHistory(history), Err(fe) => generic_err_responses!(fe, $get_history_resp), }; Box::new(futures::done(Ok(ret))) } fn $get_rev_fn( &self, rev_id: String, expand: Option<String>, hide: Option<String>, _context: &Context, ) -> Box<dyn Future<Item = $get_rev_resp, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); // No transaction for GET? let ret = match (|| { let rev_id = Uuid::from_str(&rev_id)?; let hide_flags = match hide { None => HideFlags::none(), Some(param) => HideFlags::from_str(¶m)?, }; match expand { None => $model::db_get_rev(&conn, rev_id, hide_flags), Some(param) => { let expand_flags = ExpandFlags::from_str(¶m)?; let mut entity = $model::db_get_rev(&conn, rev_id, hide_flags)?; entity.db_expand(&conn, expand_flags)?; Ok(entity) }, } })().map_err(|e| FatcatError::from(e)) { Ok(entity) => $get_rev_resp::FoundEntityRevision(entity), Err(fe) => generic_err_responses!(fe, $get_rev_resp), }; Box::new(futures::done(Ok(ret))) } fn $get_edit_fn( &self, edit_id: String, _context: &Context, ) -> Box<dyn Future<Item = $get_edit_resp, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); // No transaction for GET? let ret = match (|| { let edit_id = Uuid::from_str(&edit_id)?; $model::db_get_edit(&conn, edit_id)?.into_model() })().map_err(|e| FatcatError::from(e)) { Ok(edit) => $get_edit_resp::FoundEdit(edit), Err(fe) => generic_err_responses!(fe, $get_edit_resp), }; Box::new(futures::done(Ok(ret))) } fn $delete_edit_fn( &self, editgroup_id: String, edit_id: String, context: &Context, ) -> Box<dyn Future<Item = $delete_edit_resp, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); let ret = match conn.transaction(|| { let editgroup_id = FatcatId::from_str(&editgroup_id)?; let edit_id = Uuid::from_str(&edit_id)?; 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)?; if !(edit.editgroup_id == editgroup_id.to_uuid()) { return Err(FatcatError::BadRequest( "editgroup_id parameter didn't match that of the edit".to_string() )) } auth_context.require_editgroup(&conn, editgroup_id)?; // check for editgroup being deleted happens in db_delete_edit() $model::db_delete_edit(&conn, edit_id) .map_err(|e| FatcatError::from(e)) }) { Ok(()) => $delete_edit_resp::DeletedEdit(Success { success: true, message: format!("Successfully deleted work-in-progress {} edit: {}", stringify!($model), edit_id) }), Err(fe) => generic_auth_err_responses!(fe, $delete_edit_resp), }; Box::new(futures::done(Ok(ret))) } fn $get_redirects_fn( &self, ident: String, _context: &Context, ) -> Box<dyn Future<Item = $get_redirects_resp, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); // No transaction for GET? let ret = match (|| { let entity_id = FatcatId::from_str(&ident)?; let redirects: Vec<FatcatId> = $model::db_get_redirects(&conn, entity_id)?; Ok(redirects.into_iter().map(|fcid| fcid.to_string()).collect()) })().map_err(|e: Error| FatcatError::from(e)) { Ok(redirects) => $get_redirects_resp::FoundEntityRedirects(redirects), Err(fe) => generic_err_responses!(fe, $get_redirects_resp), }; Box::new(futures::done(Ok(ret))) } } } macro_rules! wrap_lookup_handler { ($get_fn:ident, $get_handler:ident, $get_resp:ident, $idname:ident) => { fn $get_fn( &self, $idname: Option<String>, wikidata_qid: Option<String>, expand: Option<String>, hide: Option<String>, _context: &Context, ) -> Box<dyn Future<Item = $get_resp, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); let expand_flags = match expand { None => ExpandFlags::none(), Some(param) => ExpandFlags::from_str(¶m).unwrap(), }; let hide_flags = match hide { None => HideFlags::none(), Some(param) => HideFlags::from_str(¶m).unwrap(), }; // No transaction for GET let ret = match self.$get_handler(&conn, &$idname, &wikidata_qid, expand_flags, hide_flags).map_err(|e| FatcatError::from(e)) { Ok(entity) => $get_resp::FoundEntity(entity), Err(fe) => generic_err_responses!(fe, $get_resp), }; Box::new(futures::done(Ok(ret))) } } } macro_rules! wrap_fcid_handler { ($get_fn:ident, $get_handler:ident, $get_resp:ident) => { fn $get_fn( &self, id: String, _context: &Context, ) -> Box<dyn Future<Item = $get_resp, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); // No transaction for GET let ret = match (|| { let fcid = FatcatId::from_str(&id)?; self.$get_handler(&conn, fcid) })().map_err(|e| FatcatError::from(e)) { Ok(entity) => $get_resp::Found(entity), Err(fe) => generic_err_responses!(fe, $get_resp), }; Box::new(futures::done(Ok(ret))) } } } macro_rules! wrap_fcid_hide_handler { ($get_fn:ident, $get_handler:ident, $get_resp:ident) => { fn $get_fn( &self, id: String, hide: Option<String>, _context: &Context, ) -> Box<dyn Future<Item = $get_resp, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); // No transaction for GET let ret = match (|| { let fcid = FatcatId::from_str(&id)?; let hide_flags = match hide { None => HideFlags::none(), Some(param) => HideFlags::from_str(¶m)?, }; self.$get_handler(&conn, fcid, hide_flags) })().map_err(|e| FatcatError::from(e)) { Ok(entity) => $get_resp::Found(entity), Err(fe) => generic_err_responses!(fe, $get_resp), }; Box::new(futures::done(Ok(ret))) } } } impl Api for Server { wrap_entity_handlers!( get_container, GetContainerResponse, create_container, CreateContainerResponse, ContainerAutoBatch, create_container_auto_batch, create_container_auto_batch_handler, CreateContainerAutoBatchResponse, update_container, UpdateContainerResponse, delete_container, DeleteContainerResponse, get_container_history, GetContainerHistoryResponse, get_container_edit, GetContainerEditResponse, delete_container_edit, DeleteContainerEditResponse, get_container_revision, GetContainerRevisionResponse, get_container_redirects, GetContainerRedirectsResponse, ContainerEntity ); wrap_entity_handlers!( get_creator, GetCreatorResponse, create_creator, CreateCreatorResponse, CreatorAutoBatch, create_creator_auto_batch, create_creator_auto_batch_handler, CreateCreatorAutoBatchResponse, update_creator, UpdateCreatorResponse, delete_creator, DeleteCreatorResponse, get_creator_history, GetCreatorHistoryResponse, get_creator_edit, GetCreatorEditResponse, delete_creator_edit, DeleteCreatorEditResponse, get_creator_revision, GetCreatorRevisionResponse, get_creator_redirects, GetCreatorRedirectsResponse, CreatorEntity ); wrap_entity_handlers!( get_file, GetFileResponse, create_file, CreateFileResponse, FileAutoBatch, create_file_auto_batch, create_file_auto_batch_handler, CreateFileAutoBatchResponse, update_file, UpdateFileResponse, delete_file, DeleteFileResponse, get_file_history, GetFileHistoryResponse, get_file_edit, GetFileEditResponse, delete_file_edit, DeleteFileEditResponse, get_file_revision, GetFileRevisionResponse, get_file_redirects, GetFileRedirectsResponse, FileEntity ); wrap_entity_handlers!( get_fileset, GetFilesetResponse, create_fileset, CreateFilesetResponse, FilesetAutoBatch, create_fileset_auto_batch, create_fileset_auto_batch_handler, CreateFilesetAutoBatchResponse, update_fileset, UpdateFilesetResponse, delete_fileset, DeleteFilesetResponse, get_fileset_history, GetFilesetHistoryResponse, get_fileset_edit, GetFilesetEditResponse, delete_fileset_edit, DeleteFilesetEditResponse, get_fileset_revision, GetFilesetRevisionResponse, get_fileset_redirects, GetFilesetRedirectsResponse, FilesetEntity ); wrap_entity_handlers!( get_webcapture, GetWebcaptureResponse, create_webcapture, CreateWebcaptureResponse, WebcaptureAutoBatch, create_webcapture_auto_batch, create_webcapture_auto_batch_handler, CreateWebcaptureAutoBatchResponse, update_webcapture, UpdateWebcaptureResponse, delete_webcapture, DeleteWebcaptureResponse, get_webcapture_history, GetWebcaptureHistoryResponse, get_webcapture_edit, GetWebcaptureEditResponse, delete_webcapture_edit, DeleteWebcaptureEditResponse, get_webcapture_revision, GetWebcaptureRevisionResponse, get_webcapture_redirects, GetWebcaptureRedirectsResponse, WebcaptureEntity ); wrap_entity_handlers!( get_release, GetReleaseResponse, create_release, CreateReleaseResponse, ReleaseAutoBatch, create_release_auto_batch, create_release_auto_batch_handler, CreateReleaseAutoBatchResponse, update_release, UpdateReleaseResponse, delete_release, DeleteReleaseResponse, get_release_history, GetReleaseHistoryResponse, get_release_edit, GetReleaseEditResponse, delete_release_edit, DeleteReleaseEditResponse, get_release_revision, GetReleaseRevisionResponse, get_release_redirects, GetReleaseRedirectsResponse, ReleaseEntity ); wrap_entity_handlers!( get_work, GetWorkResponse, create_work, CreateWorkResponse, WorkAutoBatch, create_work_auto_batch, create_work_auto_batch_handler, CreateWorkAutoBatchResponse, update_work, UpdateWorkResponse, delete_work, DeleteWorkResponse, get_work_history, GetWorkHistoryResponse, get_work_edit, GetWorkEditResponse, delete_work_edit, DeleteWorkEditResponse, get_work_revision, GetWorkRevisionResponse, get_work_redirects, GetWorkRedirectsResponse, WorkEntity ); wrap_lookup_handler!( lookup_container, lookup_container_handler, LookupContainerResponse, issnl ); wrap_lookup_handler!( lookup_creator, lookup_creator_handler, LookupCreatorResponse, orcid ); wrap_fcid_hide_handler!( get_release_files, get_release_files_handler, GetReleaseFilesResponse ); wrap_fcid_hide_handler!( get_release_filesets, get_release_filesets_handler, GetReleaseFilesetsResponse ); wrap_fcid_hide_handler!( get_release_webcaptures, get_release_webcaptures_handler, GetReleaseWebcapturesResponse ); wrap_fcid_hide_handler!( get_work_releases, get_work_releases_handler, GetWorkReleasesResponse ); wrap_fcid_hide_handler!( get_creator_releases, get_creator_releases_handler, GetCreatorReleasesResponse ); wrap_fcid_handler!(get_editor, get_editor_handler, GetEditorResponse); fn lookup_file( &self, md5: Option<String>, sha1: Option<String>, sha256: Option<String>, expand: Option<String>, hide: Option<String>, _context: &Context, ) -> Box<dyn Future<Item = LookupFileResponse, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); let expand_flags = match expand { None => ExpandFlags::none(), Some(param) => ExpandFlags::from_str(¶m).unwrap(), }; let hide_flags = match hide { None => HideFlags::none(), Some(param) => HideFlags::from_str(¶m).unwrap(), }; // No transaction for GET let ret = match self .lookup_file_handler(&conn, &md5, &sha1, &sha256, expand_flags, hide_flags) .map_err(|e| FatcatError::from(e)) { Ok(entity) => LookupFileResponse::FoundEntity(entity), Err(fe) => generic_err_responses!(fe, LookupFileResponse), }; Box::new(futures::done(Ok(ret))) } fn lookup_release( &self, doi: Option<String>, wikidata_qid: Option<String>, isbn13: Option<String>, pmid: Option<String>, pmcid: Option<String>, core: Option<String>, arxiv: Option<String>, jstor: Option<String>, ark: Option<String>, mag: Option<String>, expand: Option<String>, hide: Option<String>, _context: &Context, ) -> Box<dyn Future<Item = LookupReleaseResponse, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); let expand_flags = match expand { None => ExpandFlags::none(), Some(param) => ExpandFlags::from_str(¶m).unwrap(), }; let hide_flags = match hide { None => HideFlags::none(), Some(param) => HideFlags::from_str(¶m).unwrap(), }; // No transaction for GET let ret = match self .lookup_release_handler( &conn, &doi, &wikidata_qid, &isbn13, &pmid, &pmcid, &core, &arxiv, &jstor, &ark, &mag, expand_flags, hide_flags, ) .map_err(|e| FatcatError::from(e)) { Ok(entity) => LookupReleaseResponse::FoundEntity(entity), // TODO: ensure good 'Not Found" error message here // (was: "Not found: {:?} / {:?} / {:?} / {:?} / {:?} / {:?}", doi, wikidata_qid, isbn13, pmid, pmcid, core Err(fe) => generic_err_responses!(fe, LookupReleaseResponse), }; 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<dyn Future<Item = UpdateEditorResponse, Error = ApiError> + 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( FatcatError::BadRequest("editor_id doesn't match".to_string()).into(), ); } 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 { // self edit of username allowed auth_context.require_role(FatcatRole::Editor)?; } else { // admin can update any username auth_context.require_role(FatcatRole::Admin)?; }; // only update fixed set of fields (username) let mut existing = Editor::db_get(&conn, editor_id)?.into_model(); existing.username = editor.username; existing .db_update_username(&conn, editor_id) .map(|e| e.into_model()) }) .map_err(|e| FatcatError::from(e)) { Ok(editor) => UpdateEditorResponse::UpdatedEditor(editor), Err(fe) => generic_err_responses!(fe, UpdateEditorResponse), }; Box::new(futures::done(Ok(ret))) } fn get_editor_editgroups( &self, editor_id: String, limit: Option<i64>, before: Option<chrono::DateTime<chrono::Utc>>, since: Option<chrono::DateTime<chrono::Utc>>, _context: &Context, ) -> Box<dyn Future<Item = GetEditorEditgroupsResponse, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); let ret = match conn .transaction(|| { let editor_id = FatcatId::from_str(&editor_id)?; let limit = cmp::min(100, limit.unwrap_or(20)) as u64; let rows = Editgroup::db_get_range_for_editor(&conn, editor_id, limit, since, before)?; let editor = Editor::db_get(&conn, editor_id)?.into_model(); Ok(rows .into_iter() .map(|(eg, cl)| eg.into_model_partial(cl.map(|v| v.id), Some(editor.clone()))) .collect()) }) .map_err(|e: Error| FatcatError::from(e)) { Ok(editgroups) => GetEditorEditgroupsResponse::Found(editgroups), Err(fe) => generic_err_responses!(fe, GetEditorEditgroupsResponse), }; Box::new(futures::done(Ok(ret))) } fn get_editor_annotations( &self, editor_id: String, limit: Option<i64>, before: Option<chrono::DateTime<chrono::Utc>>, since: Option<chrono::DateTime<chrono::Utc>>, _context: &Context, ) -> Box<dyn Future<Item = GetEditorAnnotationsResponse, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); let ret = match conn .transaction(|| { let editor_id = FatcatId::from_str(&editor_id)?; let limit = cmp::min(100, limit.unwrap_or(20)) as u64; let annotations = EditgroupAnnotation::db_get_range_for_editor( &conn, editor_id, limit, since, before, )?; Ok(annotations.into_iter().map(|a| a.into_model()).collect()) }) .map_err(|e: Error| FatcatError::from(e)) { Ok(annotations) => GetEditorAnnotationsResponse::Success(annotations), Err(fe) => generic_err_responses!(fe, GetEditorAnnotationsResponse), }; Box::new(futures::done(Ok(ret))) } fn accept_editgroup( &self, editgroup_id: String, context: &Context, ) -> Box<dyn Future<Item = AcceptEditgroupResponse, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); let ret = match conn .transaction(|| { let editgroup_id = FatcatId::from_str(&editgroup_id)?; let auth_context = self.auth_confectionary.require_auth( &conn, &context.auth_data, Some("accept_editgroup"), )?; 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(&conn, editgroup_id) }) .map_err(|e| FatcatError::from(e)) { Ok(()) => { self.metrics.incr("editgroup.accepted").ok(); AcceptEditgroupResponse::MergedSuccessfully(Success { success: true, message: "horray!".to_string(), }) } Err(fe) => generic_auth_err_responses!(fe, AcceptEditgroupResponse), }; Box::new(futures::done(Ok(ret))) } fn get_editgroup( &self, editgroup_id: String, _context: &Context, ) -> Box<dyn Future<Item = GetEditgroupResponse, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); let ret = match conn .transaction(|| { let editgroup_id = FatcatId::from_str(&editgroup_id)?; self.get_editgroup_handler(&conn, editgroup_id) }) .map_err(|e| FatcatError::from(e)) { Ok(entity) => GetEditgroupResponse::Found(entity), Err(fe) => generic_err_responses!(fe, GetEditgroupResponse), }; Box::new(futures::done(Ok(ret))) } fn get_editgroup_annotations( &self, editgroup_id: String, expand: Option<String>, _context: &Context, ) -> Box<dyn Future<Item = GetEditgroupAnnotationsResponse, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); let ret = match conn .transaction(|| { let editgroup_id = FatcatId::from_str(&editgroup_id)?; let limit: u64 = 1000; // TODO: controllable expansion... for now always expands editors let annotations = EditgroupAnnotation::db_get_range_for_editgroup( &conn, editgroup_id, limit, None, None, )?; let mut annotations: Vec<EditgroupAnnotation> = annotations.into_iter().map(|a| a.into_model()).collect(); if let Some(expand) = expand { let expand = ExpandFlags::from_str(&expand)?; for a in annotations.iter_mut() { a.db_expand(&conn, expand)?; } }; Ok(annotations) }) .map_err(|e: Error| FatcatError::from(e)) { Ok(annotations) => GetEditgroupAnnotationsResponse::Success(annotations), Err(fe) => generic_err_responses!(fe, GetEditgroupAnnotationsResponse), }; Box::new(futures::done(Ok(ret))) } fn get_editgroups_reviewable( &self, expand: Option<String>, limit: Option<i64>, before: Option<chrono::DateTime<chrono::Utc>>, since: Option<chrono::DateTime<chrono::Utc>>, _context: &Context, ) -> Box<dyn Future<Item = GetEditgroupsReviewableResponse, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); let ret = match conn .transaction(|| { let limit = cmp::min(100, limit.unwrap_or(20)) as u64; let row = Editgroup::db_get_range_reviewable(&conn, limit, since, before)?; let mut editgroups: Vec<Editgroup> = row .into_iter() .map(|eg| eg.into_model_partial(None, None)) .collect(); if let Some(expand) = expand { let expand = ExpandFlags::from_str(&expand)?; for eg in editgroups.iter_mut() { eg.db_expand(&conn, expand)?; } }; Ok(editgroups) }) .map_err(|e: Error| FatcatError::from(e)) { Ok(editgroups) => GetEditgroupsReviewableResponse::Found(editgroups), Err(fe) => generic_err_responses!(fe, GetEditgroupsReviewableResponse), }; Box::new(futures::done(Ok(ret))) } fn create_editgroup( &self, entity: models::Editgroup, context: &Context, ) -> Box<dyn Future<Item = CreateEditgroupResponse, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); let ret = match conn.transaction(|| { let auth_context = self.auth_confectionary.require_auth( &conn, &context.auth_data, Some("create_editgroup"), )?; auth_context.require_role(FatcatRole::Editor)?; let mut entity = entity.clone(); match entity.editor_id.clone() { Some(editor_id) => { if editor_id != auth_context.editor_id.to_string() && !auth_context.has_role(FatcatRole::Admin) { return Err(FatcatError::InsufficientPrivileges( "not authorized to create editgroups in others' names".to_string(), )); } } None => { entity.editor_id = Some(auth_context.editor_id.to_string()); } }; self.create_editgroup_handler(&conn, entity) .map_err(|e| FatcatError::from(e)) }) { Ok(eg) => { self.metrics.incr("editgroup.created").ok(); CreateEditgroupResponse::SuccessfullyCreated(eg) } Err(fe) => match fe { NotFound(_, _) | DatabaseRowNotFound => { CreateEditgroupResponse::NotFound(fe.into()) } DatabaseError(_) | InternalError(_) => { error!("{}", fe); capture_fail(&fe); CreateEditgroupResponse::GenericError(fe.into()) } _ => CreateEditgroupResponse::BadRequest(fe.into()), }, }; Box::new(futures::done(Ok(ret))) } /// Note: this currently won't return the full (hydrated) editgroup including all edits, just /// the partial model. fn update_editgroup( &self, editgroup_id: String, editgroup: models::Editgroup, submit: Option<bool>, context: &Context, ) -> Box<dyn Future<Item = UpdateEditgroupResponse, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); let ret = match conn .transaction(|| { let editgroup_id = FatcatId::from_str(&editgroup_id)?; if Some(editgroup_id.to_string()) != editgroup.editgroup_id { return Err( FatcatError::BadRequest("editgroup_id doesn't match".to_string()).into(), ); } let auth_context = self.auth_confectionary.require_auth( &conn, &context.auth_data, Some("update_editgroup"), )?; let existing = Editgroup::db_get(&conn, editgroup_id)?; if existing.editor_id == auth_context.editor_id.to_uuid() { // self edit of editgroup allowed auth_context.require_role(FatcatRole::Editor)?; } else { // admin can update any editgroup auth_context.require_role(FatcatRole::Admin)?; }; editgroup .db_update(&conn, editgroup_id, submit) .map(|eg| eg.into_model_partial(None, None)) // can't update an accepted editgroup }) .map_err(|e: Error| FatcatError::from(e)) { Ok(editgroup) => UpdateEditgroupResponse::UpdatedEditgroup(editgroup), Err(fe) => generic_err_responses!(fe, UpdateEditgroupResponse), }; Box::new(futures::done(Ok(ret))) } fn create_editgroup_annotation( &self, editgroup_id: String, annotation: models::EditgroupAnnotation, context: &Context, ) -> Box<dyn Future<Item = CreateEditgroupAnnotationResponse, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); let ret = match conn.transaction(|| { let auth_context = self.auth_confectionary.require_auth( &conn, &context.auth_data, Some("create_editgroup_annotation"), )?; auth_context.require_role(FatcatRole::Editor)?; let editgroup_id = FatcatId::from_str(&editgroup_id)?; let mut annotation = annotation.clone(); annotation.editgroup_id = Some(editgroup_id.to_string()); match annotation.editor_id.clone() { Some(editor_id) => { if editor_id != auth_context.editor_id.to_string() && !auth_context.has_role(FatcatRole::Superuser) { return Err(FatcatError::BadRequest( "not authorized to annotate in others' names".to_string(), )); } } None => { annotation.editor_id = Some(auth_context.editor_id.to_string()); } }; annotation .db_create(&conn) .map(|a| a.into_model()) .map_err(|e: Error| FatcatError::from(e)) }) { Ok(annotation) => CreateEditgroupAnnotationResponse::Created(annotation), Err(fe) => generic_auth_err_responses!(fe, CreateEditgroupAnnotationResponse), }; Box::new(futures::done(Ok(ret))) } fn get_changelog( &self, limit: Option<i64>, _context: &Context, ) -> Box<dyn Future<Item = GetChangelogResponse, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); // No transaction for GET let ret = match self .get_changelog_handler(&conn, limit) .map_err(|e| FatcatError::from(e)) { Ok(changelog) => GetChangelogResponse::Success(changelog), Err(fe) => match fe { DatabaseError(_) | InternalError(_) => { error!("{}", fe); capture_fail(&fe); GetChangelogResponse::GenericError(fe.into()) } _ => GetChangelogResponse::BadRequest(fe.into()), }, }; Box::new(futures::done(Ok(ret))) } fn get_changelog_entry( &self, id: i64, _context: &Context, ) -> Box<dyn Future<Item = GetChangelogEntryResponse, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); // No transaction for GET let ret = match self .get_changelog_entry_handler(&conn, id) .map_err(|e| FatcatError::from(e)) { Ok(entry) => GetChangelogEntryResponse::FoundChangelogEntry(entry), Err(fe) => generic_err_responses!(fe, GetChangelogEntryResponse), }; Box::new(futures::done(Ok(ret))) } fn auth_oidc( &self, params: models::AuthOidc, context: &Context, ) -> Box<dyn Future<Item = AuthOidcResponse, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); let ret = match conn .transaction(|| { let auth_context = self.auth_confectionary.require_auth( &conn, &context.auth_data, Some("auth_oidc"), )?; auth_context.require_role(FatcatRole::Superuser)?; let (editor, created) = self.auth_oidc_handler(&conn, params)?; // create an auth token with 31 day duration let token = self.auth_confectionary.create_token( FatcatId::from_str(&editor.editor_id.clone().unwrap())?, Some(chrono::Duration::days(31)), )?; let result = AuthOidcResult { editor, token }; Ok((result, created)) }) .map_err(|e: Error| FatcatError::from(e)) { Ok((result, true)) => { self.metrics.incr("account.signup").ok(); AuthOidcResponse::Created(result) } Ok((result, false)) => { self.metrics.incr("account.login").ok(); AuthOidcResponse::Found(result) } Err(fe) => match fe { InvalidCredentials(_) | InsufficientPrivileges(_) => { AuthOidcResponse::Forbidden(fe.into()) } DatabaseError(_) | InternalError(_) => { error!("{}", fe); capture_fail(&fe); AuthOidcResponse::GenericError(fe.into()) } _ => AuthOidcResponse::BadRequest(fe.into()), }, }; Box::new(futures::done(Ok(ret))) } fn auth_check( &self, role: Option<String>, context: &Context, ) -> Box<dyn Future<Item = AuthCheckResponse, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); let ret = match conn .transaction(|| { let auth_context = self.auth_confectionary.require_auth( &conn, &context.auth_data, 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(()) }) .map_err(|e| FatcatError::from(e)) { Ok(()) => AuthCheckResponse::Success(Success { success: true, message: "auth check successful!".to_string(), }), Err(fe) => match fe { InvalidCredentials(_) | InsufficientPrivileges(_) => { AuthCheckResponse::Forbidden(fe.into()) } DatabaseError(_) | InternalError(_) => { error!("{}", fe); capture_fail(&fe); AuthCheckResponse::GenericError(fe.into()) } _ => AuthCheckResponse::BadRequest(fe.into()), }, }; Box::new(futures::done(Ok(ret))) } fn create_auth_token( &self, editor_id: String, duration_seconds: Option<i32>, context: &Context, ) -> Box<dyn Future<Item = CreateAuthTokenResponse, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); let ret = match conn .transaction(|| { let auth_context = self.auth_confectionary.require_auth( &conn, &context.auth_data, Some("create_auth_token"), )?; auth_context.require_role(FatcatRole::Superuser)?; // create an auth token. default to 31 day duration let duration = match duration_seconds { Some(seconds) => { assert!(seconds >= 1); chrono::Duration::seconds(seconds.into()) } None => chrono::Duration::days(31), }; // TODO: does logic checking if account is locked happen elsewhere? let token = self .auth_confectionary .create_token(FatcatId::from_str(&editor_id)?, Some(duration))?; let result = AuthTokenResult { token }; Ok(result) }) .map_err(|e: Error| FatcatError::from(e)) { Ok(result) => { self.metrics.incr("account.create_token").ok(); CreateAuthTokenResponse::Success(result) } Err(fe) => match fe { InvalidCredentials(_) | InsufficientPrivileges(_) => { CreateAuthTokenResponse::Forbidden(fe.into()) } DatabaseError(_) | InternalError(_) => { error!("{}", fe); capture_fail(&fe); CreateAuthTokenResponse::GenericError(fe.into()) } _ => CreateAuthTokenResponse::BadRequest(fe.into()), }, }; Box::new(futures::done(Ok(ret))) } }