diff --git a/src/api/sudo.rs b/src/api/sudo.rs index 9a737a9..b00f317 100644 --- a/src/api/sudo.rs +++ b/src/api/sudo.rs @@ -41,6 +41,13 @@ struct LimitOffsetQuery { s: i64, } +#[derive(Deserialize)] +pub struct TransferMemberShip { + group_name: String, + old_user_uuid: Uuid, + new_user_uuid: Uuid, +} + fn default_groups_list_size() -> i64 { 20 } @@ -288,8 +295,44 @@ async fn delete_user( .map_err(Into::into) } +#[guard(Staff, Admin, Medium)] +async fn transfer_membership( + pool: web::Data, + transfer: web::Json, + scope_and_user: ScopeAndUser, + cis_client: web::Data, +) -> Result { + operations::members::transfer( + &pool, + &scope_and_user, + &transfer.group_name, + &User { + user_uuid: transfer.old_user_uuid, + }, + &User { + user_uuid: transfer.new_user_uuid, + }, + Arc::clone(&*cis_client), + ) + .await + .map(|_| HttpResponse::Ok().json("")) + .map_err(Into::into) +} + +#[guard(Staff, Admin, Medium)] +async fn raw_data( + pool: web::Data, + scope_and_user: ScopeAndUser, + user_uuid: web::Path, +) -> Result { + operations::raws::raw_user_data(&pool, &scope_and_user, Some(user_uuid.into_inner())) + .map(|data| HttpResponse::Ok().json(data)) + .map_err(Into::into) +} + pub fn sudo_app() -> impl HttpServiceFactory { web::scope("/sudo") + .service(web::resource("/transfer").route(web::post().to(transfer_membership::))) .service(web::resource("/groups/reserve/{group_name}").route(web::post().to(reserve_group))) .service( web::resource("/groups/inactive/{group_name}") @@ -304,6 +347,7 @@ pub fn sudo_app() -> impl HttpServiceFactory { .route(web::delete().to(remove_member::)), ) .service(web::resource("/member/{group_name}").route(web::post().to(add_member::))) + .service(web::resource("/user/data/{user_uuid}").route(web::get().to(raw_data))) .service(web::resource("/user/uuids/staff").route(web::get().to(all_staff_uuids))) .service(web::resource("/user/uuids/members").route(web::get().to(all_member_uuids))) .service( diff --git a/src/db/internal/member.rs b/src/db/internal/member.rs index 36579b6..d018512 100644 --- a/src/db/internal/member.rs +++ b/src/db/internal/member.rs @@ -311,6 +311,8 @@ scoped_members_and_host_for!(users_public, hosts_public, public_scoped_members_a membership_and_scoped_host_for!(hosts_staff, membership_and_staff_host); membership_and_scoped_host_for!(hosts_ndaed, membership_and_ndaed_host); +membership_and_scoped_host_for!(hosts_vouched, membership_and_vouched_host); +membership_and_scoped_host_for!(hosts_authenticated, membership_and_authenticated_host); pub fn add_member_role( host_uuid: &Uuid, @@ -430,6 +432,44 @@ pub fn add_to_group( .map_err(Into::into) } +pub fn transfer_membership( + connection: &PgConnection, + group_name: &str, + host: &User, + old_member: &User, + new_member: &User, +) -> Result<(), Error> { + let group = internal::group::get_group(connection, group_name)?; + let log_ctx_old = LogContext::with(group.id, host.user_uuid).with_user(old_member.user_uuid); + let log_ctx_new = LogContext::with(group.id, host.user_uuid).with_user(new_member.user_uuid); + diesel::update( + schema::memberships::table.filter( + schema::memberships::group_id + .eq(group.id) + .and(schema::memberships::user_uuid.eq(old_member.user_uuid)), + ), + ) + .set(schema::memberships::user_uuid.eq(new_member.user_uuid)) + .execute(connection) + .map(|_| { + internal::log::db_log( + connection, + &log_ctx_old, + LogTargetType::Membership, + LogOperationType::Updated, + log_comment_body(&format!("moved to {}", new_member.user_uuid)), + ); + internal::log::db_log( + connection, + &log_ctx_new, + LogTargetType::Membership, + LogOperationType::Updated, + log_comment_body(&format!("moved from {}", old_member.user_uuid)), + ); + }) + .map_err(Into::into) +} + pub fn renew( host_uuid: &Uuid, connection: &PgConnection, diff --git a/src/db/internal/mod.rs b/src/db/internal/mod.rs index 805561c..48133f5 100644 --- a/src/db/internal/mod.rs +++ b/src/db/internal/mod.rs @@ -4,6 +4,7 @@ pub mod group; pub mod invitation; pub mod log; pub mod member; +pub mod raw; pub mod request; pub mod terms; pub mod user; diff --git a/src/db/internal/raw.rs b/src/db/internal/raw.rs new file mode 100644 index 0000000..4e3a7b6 --- /dev/null +++ b/src/db/internal/raw.rs @@ -0,0 +1,67 @@ +use crate::db::logs::Log; +use crate::db::model::*; +use crate::db::schema; +use crate::db::users::UserProfile; +use crate::db::users::UserProfileValue; +use diesel::prelude::*; +use failure::Error; +use std::convert::TryInto; +use uuid::Uuid; + +pub fn raw_memberships_for_user( + connection: &PgConnection, + user_uuid: &Uuid, +) -> Result, Error> { + use schema::memberships as m; + + m::table + .filter(m::user_uuid.eq(user_uuid)) + .get_results(connection) + .map_err(Into::into) +} + +pub fn raw_invitations_for_user( + connection: &PgConnection, + user_uuid: &Uuid, +) -> Result, Error> { + use schema::invitations as i; + + i::table + .filter(i::user_uuid.eq(user_uuid).or(i::added_by.eq(user_uuid))) + .get_results(connection) + .map_err(Into::into) +} + +pub fn raw_requests_for_user( + connection: &PgConnection, + user_uuid: &Uuid, +) -> Result, Error> { + use schema::requests as r; + + r::table + .filter(r::user_uuid.eq(user_uuid)) + .get_results(connection) + .map_err(Into::into) +} + +pub fn raw_user_for_user( + connection: &PgConnection, + user_uuid: &Uuid, +) -> Result { + use schema::profiles as p; + + p::table + .filter(p::user_uuid.eq(user_uuid)) + .get_result::(connection) + .map_err(Error::from) + .and_then(TryInto::try_into) +} + +pub fn raw_logs_for_user(connection: &PgConnection, user_uuid: &Uuid) -> Result, Error> { + use schema::logs as l; + + l::table + .filter(l::user_uuid.eq(user_uuid).or(l::host_uuid.eq(user_uuid))) + .get_results(connection) + .map_err(Into::into) +} diff --git a/src/db/model.rs b/src/db/model.rs index bf2ddb6..90fd102 100644 --- a/src/db/model.rs +++ b/src/db/model.rs @@ -37,7 +37,7 @@ pub struct Role { pub permissions: Vec, } -#[derive(Queryable, Associations, PartialEq, Debug, Insertable, AsChangeset)] +#[derive(Serialize, Queryable, Associations, PartialEq, Debug, Insertable, AsChangeset)] #[belongs_to(Group)] #[primary_key(group_id, user_uuid)] pub struct Membership { @@ -49,7 +49,9 @@ pub struct Membership { pub added_ts: NaiveDateTime, } -#[derive(Identifiable, Queryable, Associations, PartialEq, Debug, Insertable, AsChangeset)] +#[derive( + Serialize, Identifiable, Queryable, Associations, PartialEq, Debug, Insertable, AsChangeset, +)] #[belongs_to(Group)] #[primary_key(group_id, user_uuid)] pub struct Invitation { @@ -68,7 +70,9 @@ pub struct Invitationtext { pub body: String, } -#[derive(Identifiable, Queryable, Associations, PartialEq, Debug, Insertable, AsChangeset)] +#[derive( + Serialize, Identifiable, Queryable, Associations, PartialEq, Debug, Insertable, AsChangeset, +)] #[belongs_to(Group)] #[primary_key(group_id, user_uuid)] pub struct Request { diff --git a/src/db/operations/members.rs b/src/db/operations/members.rs index bbbdc99..1b71fdf 100644 --- a/src/db/operations/members.rs +++ b/src/db/operations/members.rs @@ -14,6 +14,7 @@ use crate::mail::manager::send_emails; use crate::mail::manager::subscribe_nda; use crate::mail::manager::unsubscribe_nda; use crate::mail::templates::Template; +use crate::rules::engine::ADMIN_CAN_ADD_MEMBER; use crate::rules::engine::ONLY_ADMINS; use crate::rules::engine::REMOVE_MEMBER; use crate::rules::engine::RENEW_MEMBER; @@ -52,6 +53,14 @@ pub fn membership_and_scoped_host( Trust::Ndaed => { internal::member::membership_and_ndaed_host(&connection, group.id, user.user_uuid) } + Trust::Vouched => { + internal::member::membership_and_vouched_host(&connection, group.id, user.user_uuid) + } + Trust::Authenticated => internal::member::membership_and_authenticated_host( + &connection, + group.id, + user.user_uuid, + ), _ => Ok(None), } } @@ -141,6 +150,37 @@ fn db_leave( ) } +pub async fn transfer( + pool: &Pool, + scope_and_user: &ScopeAndUser, + group_name: &str, + old_user: &User, + new_user: &User, + cis_client: Arc, +) -> Result<(), Error> { + let connection = pool.get()?; + let host = internal::user::user_by_id(&connection, &scope_and_user.user_id)?; + ADMIN_CAN_ADD_MEMBER.run(&RuleContext::minimal_with_member_uuid( + &pool.clone(), + scope_and_user, + &group_name, + &host.user_uuid, + &new_user.user_uuid, + ))?; + internal::member::transfer_membership(&connection, &group_name, &host, &old_user, &new_user)?; + if group_name == "nda" { + let old_user_profile = + internal::user::slim_user_profile_by_uuid(&connection, &old_user.user_uuid)?; + let new_user_profile = + internal::user::slim_user_profile_by_uuid(&connection, &old_user.user_uuid)?; + unsubscribe_nda(&old_user_profile.email); + subscribe_nda(&new_user_profile.email); + } + drop(connection); + send_groups_to_cis(pool, Arc::clone(&cis_client), &old_user.user_uuid).await?; + send_groups_to_cis(pool, cis_client, &new_user.user_uuid).await +} + pub async fn add( pool: &Pool, scope_and_user: &ScopeAndUser, @@ -150,11 +190,12 @@ pub async fn add( expiration: Option, cis_client: Arc, ) -> Result<(), Error> { - ONLY_ADMINS.run(&RuleContext::minimal( + ADMIN_CAN_ADD_MEMBER.run(&RuleContext::minimal_with_member_uuid( &pool.clone(), scope_and_user, &group_name, &host.user_uuid, + &user.user_uuid, ))?; let connection = pool.get()?; let expiration = if expiration.is_none() { diff --git a/src/db/operations/mod.rs b/src/db/operations/mod.rs index f4382a1..129b6fd 100644 --- a/src/db/operations/mod.rs +++ b/src/db/operations/mod.rs @@ -5,6 +5,7 @@ pub mod invitations; pub mod logs; pub mod members; pub mod models; +pub mod raws; pub mod requests; pub mod terms; pub mod users; diff --git a/src/db/operations/models.rs b/src/db/operations/models.rs index 06e2caf..04e03c7 100644 --- a/src/db/operations/models.rs +++ b/src/db/operations/models.rs @@ -1,6 +1,11 @@ +use crate::db::logs::Log; use crate::db::model::Group; use crate::db::model::GroupsList; +use crate::db::model::Invitation; +use crate::db::model::Membership; +use crate::db::model::Request; use crate::db::types::*; +use crate::db::users::UserProfile; use crate::error::PacksError; use crate::user::User; use crate::utils::maybe_to_utc; @@ -501,6 +506,15 @@ pub struct InvitationEmail { pub body: Option, } +#[derive(Serialize)] +pub struct RawUserData { + pub user_profile: UserProfile, + pub memberships: Vec, + pub invitations: Vec, + pub requests: Vec, + pub logs: Vec, +} + #[cfg(test)] mod test { use super::*; diff --git a/src/db/operations/raws.rs b/src/db/operations/raws.rs new file mode 100644 index 0000000..e20e536 --- /dev/null +++ b/src/db/operations/raws.rs @@ -0,0 +1,33 @@ +use crate::db::internal; +use crate::db::internal::raw::*; +use crate::db::operations::models::RawUserData; +use crate::db::Pool; +use dino_park_gate::scope::ScopeAndUser; +use failure::Error; +use uuid::Uuid; + +pub fn raw_user_data( + pool: &Pool, + scope_and_user: &ScopeAndUser, + user_uuid: Option, +) -> Result { + let connection = pool.get()?; + let user_uuid = match user_uuid { + Some(user_uuid) => user_uuid, + _ => internal::user::user_by_id(&connection, &scope_and_user.user_id)?.user_uuid, + }; + + let user_profile = raw_user_for_user(&connection, &user_uuid)?; + let memberships = raw_memberships_for_user(&connection, &user_uuid)?; + let invitations = raw_invitations_for_user(&connection, &user_uuid)?; + let requests = raw_requests_for_user(&connection, &user_uuid)?; + let logs = raw_logs_for_user(&connection, &user_uuid)?; + + Ok(RawUserData { + user_profile, + memberships, + invitations, + requests, + logs, + }) +} diff --git a/src/db/users.rs b/src/db/users.rs index 07ce5b3..483c865 100644 --- a/src/db/users.rs +++ b/src/db/users.rs @@ -6,6 +6,7 @@ use crate::rules::functions::is_nda_group; use cis_profile::schema::Display; use cis_profile::schema::Profile; use cis_profile::schema::StandardAttributeString; +use failure::Error; use serde::Deserialize; use serde::Serialize; use serde_json::Value; @@ -44,6 +45,7 @@ pub struct UserProfileValue { pub trust: TrustType, } +#[derive(Serialize)] pub struct UserProfile { pub user_uuid: Uuid, pub user_id: String, @@ -54,7 +56,7 @@ pub struct UserProfile { } impl TryFrom for UserProfileValue { - type Error = serde_json::Error; + type Error = Error; fn try_from(u: UserProfile) -> Result { Ok(UserProfileValue { @@ -69,7 +71,7 @@ impl TryFrom for UserProfileValue { } impl TryFrom for UserProfile { - type Error = serde_json::Error; + type Error = Error; fn try_from(u: UserProfileValue) -> Result { Ok(UserProfile { user_uuid: u.user_uuid, @@ -83,7 +85,7 @@ impl TryFrom for UserProfile { } impl TryFrom for UserProfile { - type Error = failure::Error; + type Error = Error; fn try_from(p: Profile) -> Result { let trust = trust_for_profile(&p); Ok(UserProfile { diff --git a/src/rules/engine.rs b/src/rules/engine.rs index 605edb0..a5cf1ad 100644 --- a/src/rules/engine.rs +++ b/src/rules/engine.rs @@ -16,6 +16,10 @@ pub const CURRENT_USER_CAN_REQUEST: Engine = Engine { rules: &[¤t_user_can_join, &is_reviewed_group], }; +pub const ADMIN_CAN_ADD_MEMBER: Engine = Engine { + rules: &[&rule_only_admins, &member_can_join], +}; + pub const SEARCH_USERS: Engine = Engine { rules: &[&rule_host_can_invite], }; @@ -61,7 +65,7 @@ pub struct Engine<'a> { } impl<'a> Engine<'a> { - pub fn run(self: &Self, ctx: &RuleContext) -> Result<(), RuleError> { + pub fn run(&self, ctx: &RuleContext) -> Result<(), RuleError> { let ok = self.rules.iter().try_for_each(|rule| rule(ctx)); if ok.is_err() && ctx.scope_and_user.groups_scope == GroupsTrust::Admin { info!("using admin privileges for {}", ctx.host_uuid); diff --git a/tests/api/sudo.rs b/tests/api/sudo.rs index 30076c5..c77171c 100644 --- a/tests/api/sudo.rs +++ b/tests/api/sudo.rs @@ -184,3 +184,133 @@ async fn add_remove() -> Result<(), Error> { Ok(()) } + +#[actix_rt::test] +async fn transfer() -> Result<(), Error> { + reset()?; + let app = App::new().service(test_app().await); + let mut app = test::init_service(app).await; + + let host_user = basic_user(1, true); + let add_user_1 = basic_user(11, false); + let add_user_2 = basic_user(12, false); + let host = Soa::from(&host_user).admin().aal_medium(); + + let res = post( + &mut app, + "/groups/api/v1/groups", + json!({ "name": "some-group", "description": "a group", "trust": "Authenticated" }), + &host, + ) + .await; + assert!(res.status().is_success()); + + let res = post( + &mut app, + "/groups/api/v1/sudo/member/some-group", + json!({ "user_uuid": user_uuid(&add_user_1) }), + &host, + ) + .await; + assert!(res.status().is_success()); + + let res = get(&mut app, "/groups/api/v1/members/some-group", &host).await; + assert!(res.status().is_success()); + let members = read_json(res).await; + assert_eq!(members["members"].as_array().map(|a| a.len()), Some(2)); + + let res = get( + &mut app, + "/groups/api/v1/groups/some-group/details", + &Soa::from(&add_user_2), + ) + .await; + assert!(res.status().is_success()); + let details = read_json(res).await; + assert_eq!(details["member"].as_bool(), Some(false)); + + let res = get( + &mut app, + "/groups/api/v1/groups/some-group/details", + &Soa::from(&add_user_1), + ) + .await; + assert!(res.status().is_success()); + let details = read_json(res).await; + assert_eq!(details["member"].as_bool(), Some(true)); + + let res = post( + &mut app, + "/groups/api/v1/sudo/transfer", + json!({ "group_name": "some-group", "old_user_uuid": user_uuid(&add_user_1), "new_user_uuid": user_uuid(&add_user_2) }), + &host, + ) + .await; + assert!(res.status().is_success()); + + let res = get(&mut app, "/groups/api/v1/members/some-group", &host).await; + assert!(res.status().is_success()); + let members = read_json(res).await; + assert_eq!(members["members"].as_array().map(|a| a.len()), Some(2)); + + let res = get( + &mut app, + "/groups/api/v1/groups/some-group/details", + &Soa::from(&add_user_2), + ) + .await; + assert!(res.status().is_success()); + let details = read_json(res).await; + assert_eq!(details["member"].as_bool(), Some(true)); + + let res = get( + &mut app, + "/groups/api/v1/groups/some-group/details", + &Soa::from(&add_user_1), + ) + .await; + assert!(res.status().is_success()); + let details = read_json(res).await; + assert_eq!(details["member"].as_bool(), Some(false)); + + Ok(()) +} + +#[actix_rt::test] +async fn raw_data() -> Result<(), Error> { + reset()?; + let app = App::new().service(test_app().await); + let mut app = test::init_service(app).await; + + let host_user = basic_user(1, true); + let add_user_1 = basic_user(11, false); + let host = Soa::from(&host_user).admin().aal_medium(); + + let res = post( + &mut app, + "/groups/api/v1/groups", + json!({ "name": "some-group", "description": "a group", "trust": "Authenticated" }), + &host, + ) + .await; + assert!(res.status().is_success()); + + let res = post( + &mut app, + "/groups/api/v1/sudo/member/some-group", + json!({ "user_uuid": user_uuid(&add_user_1) }), + &host, + ) + .await; + assert!(res.status().is_success()); + + let res = get( + &mut app, + &format!("/groups/api/v1/sudo/user/data/{}", user_uuid(&host_user)), + &host, + ) + .await; + assert!(res.status().is_success()); + + Ok(()) +}