Skip to content

Commit 8c8e88f

Browse files
committed
handle inactive users
1 parent d9433a5 commit 8c8e88f

File tree

8 files changed

+207
-7
lines changed

8 files changed

+207
-7
lines changed
Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,12 @@
1-
CREATE VIEW groups_list AS SELECT groups.name, groups.typ, groups.trust, count(memberships.user_uuid) as members_count FROM groups JOIN memberships ON groups.group_id = memberships.group_id GROUP BY groups.group_id;
1+
CREATE VIEW groups_list AS
2+
SELECT
3+
groups.name,
4+
groups.typ,
5+
groups.trust,
6+
count(memberships.user_uuid) AS members_count
7+
FROM
8+
GROUPS
9+
JOIN memberships ON groups.group_id = memberships.group_id
10+
GROUP BY
11+
groups.group_id;
12+

migrations/2020-08-25-110359_nonnda/up.sql

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,3 @@ FROM
1212
users_staff u
1313
WHERE
1414
p.user_uuid = u.user_uuid;
15-

src/api/sudo.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,16 @@ async fn delete_inactive_group(
193193
.map_err(ApiError::GenericBadRequest)
194194
}
195195

196+
#[guard(Staff, Admin, Medium)]
197+
async fn delete_inactive_users(
198+
pool: web::Data<Pool>,
199+
scope_and_user: ScopeAndUser,
200+
) -> Result<HttpResponse, ApiError> {
201+
operations::users::delete_inactive_users(&pool, &scope_and_user)
202+
.map(|_| HttpResponse::Ok().json(""))
203+
.map_err(ApiError::GenericBadRequest)
204+
}
205+
196206
#[guard(Staff, Admin, Medium)]
197207
async fn subscribe_nda_mailing_list(
198208
pool: web::Data<Pool>,
@@ -272,6 +282,7 @@ pub fn sudo_app<T: AsyncCisClientTrait + 'static>() -> impl HttpServiceFactory {
272282
web::resource("/member/{group_name}/{user_uuid}")
273283
.route(web::delete().to(remove_member::<T>)),
274284
)
285+
.service(web::resource("/user/inactive").route(web::delete().to(delete_inactive_users)))
275286
.service(web::resource("/user/{uuid}").route(web::delete().to(delete_user)))
276287
.service(web::resource("/user/uuids/staff").route(web::get().to(all_staff_uuids)))
277288
.service(web::resource("/user/uuids/members").route(web::get().to(all_member_uuids)))

src/db/internal/user.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ pub fn user_profile_by_user_id(
8181
}
8282

8383
pub fn delete_user(connection: &PgConnection, user: &User) -> Result<(), Error> {
84+
diesel::delete(schema::requests::table)
85+
.filter(schema::requests::user_uuid.eq(user.user_uuid))
86+
.execute(connection)?;
8487
diesel::delete(schema::invitations::table)
8588
.filter(schema::invitations::user_uuid.eq(user.user_uuid))
8689
.execute(connection)?;
@@ -420,3 +423,37 @@ pub fn all_members(connection: &PgConnection) -> Result<Vec<Uuid>, Error> {
420423
.get_results::<Uuid>(connection)
421424
.map_err(Into::into)
422425
}
426+
427+
use diesel::pg::expression::dsl::array;
428+
use diesel::pg::types::sql_types::Array;
429+
use diesel::pg::types::sql_types::Jsonb;
430+
use diesel::pg::Pg;
431+
use diesel::sql_types::Text;
432+
433+
sql_function! {
434+
fn jsonb_extract_path(from_json: Jsonb, path_elems: Array<Text>) -> Jsonb
435+
}
436+
437+
diesel_infix_operator!(ExtrPath, " #> ", Jsonb, backend: Pg);
438+
439+
fn extr_path<T, U>(left: T, right: U) -> ExtrPath<T, U>
440+
where
441+
T: Expression,
442+
U: Expression,
443+
{
444+
ExtrPath::new(left, right)
445+
}
446+
447+
pub fn all_inactive(connection: &PgConnection) -> Result<Vec<Uuid>, Error> {
448+
schema::profiles::table
449+
.filter(
450+
extr_path(
451+
schema::profiles::profile,
452+
array::<Text, _>(("active".to_string(), "value".to_string())),
453+
)
454+
.eq(serde_json::Value::from(false)),
455+
)
456+
.select(schema::profiles::user_uuid)
457+
.get_results::<Uuid>(connection)
458+
.map_err(Into::into)
459+
}

src/db/operations/users.rs

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::cis::operations::send_groups_to_cis;
12
use crate::db::internal;
23
use crate::db::logs::log_comment_body;
34
use crate::db::operations::members::revoke_memberships_by_trust;
@@ -17,6 +18,7 @@ use cis_client::AsyncCisClientTrait;
1718
use cis_profile::schema::Profile;
1819
use dino_park_gate::scope::ScopeAndUser;
1920
use failure::Error;
21+
use log::info;
2022
use std::sync::Arc;
2123
use uuid::Uuid;
2224

@@ -116,17 +118,20 @@ pub async fn update_user_cache(
116118
profile: &Profile,
117119
cis_client: Arc<impl AsyncCisClientTrait>,
118120
) -> Result<(), Error> {
121+
let user_uuid = Uuid::parse_str(&profile.uuid.value.clone().ok_or(PacksError::NoUuid)?)?;
122+
if profile.active.value == Some(false) {
123+
return delete_user(pool, &User { user_uuid });
124+
}
119125
let connection = pool.get()?;
120126
let new_trust = trust_for_profile(&profile);
121-
let uuid = Uuid::parse_str(&profile.uuid.value.clone().ok_or(PacksError::NoUuid)?)?;
122-
let old_profile = internal::user::user_profile_by_uuid_maybe(&connection, &uuid)?;
127+
let old_profile = internal::user::user_profile_by_uuid_maybe(&connection, &user_uuid)?;
123128
internal::user::update_user_cache(&connection, profile)?;
124129

125130
if let Some(old_profile) = old_profile {
126131
let old_trust = trust_for_profile(&old_profile.profile);
127132
drop(connection);
128133
let remove_groups = RemoveGroups {
129-
user: User { user_uuid: uuid },
134+
user: User { user_uuid },
130135
group_names: &[],
131136
force: true,
132137
notify: true,
@@ -142,7 +147,12 @@ pub async fn update_user_cache(
142147
)
143148
.await?;
144149
}
150+
} else if let Some(ref groups) = profile.access_information.mozilliansorg.values {
151+
if !groups.0.is_empty() {
152+
send_groups_to_cis(pool, cis_client, &user_uuid).await?;
153+
}
145154
}
155+
146156
Ok(())
147157
}
148158

@@ -156,13 +166,32 @@ pub fn user_profile_by_uuid(pool: &Pool, user_uuid: &Uuid) -> Result<UserProfile
156166
internal::user::user_profile_by_uuid(&connection, user_uuid)
157167
}
158168

169+
pub fn delete_inactive_users(pool: &Pool, scope_and_user: &ScopeAndUser) -> Result<(), Error> {
170+
let connection = pool.get()?;
171+
let host = internal::user::user_by_id(&connection, &scope_and_user.user_id)?;
172+
ONLY_ADMINS.run(&RuleContext::minimal(
173+
pool,
174+
&scope_and_user,
175+
"",
176+
&host.user_uuid,
177+
))?;
178+
let inactive_uuids = internal::user::all_inactive(&connection)?;
179+
drop(connection);
180+
info!("deleting {} users", inactive_uuids.len());
181+
for user_uuid in inactive_uuids {
182+
delete_user(pool, &User { user_uuid })?;
183+
info!("delete user {}", user_uuid);
184+
}
185+
Ok(())
186+
}
187+
159188
pub fn get_all_member_uuids(
160189
pool: &Pool,
161190
scope_and_user: &ScopeAndUser,
162191
) -> Result<Vec<Uuid>, Error> {
163192
let connection = pool.get()?;
164193
let host = internal::user::user_by_id(&connection, &scope_and_user.user_id)?;
165-
SEARCH_USERS.run(&RuleContext::minimal(
194+
ONLY_ADMINS.run(&RuleContext::minimal(
166195
pool,
167196
&scope_and_user,
168197
"",
@@ -174,11 +203,13 @@ pub fn get_all_member_uuids(
174203
pub fn get_all_staff_uuids(pool: &Pool, scope_and_user: &ScopeAndUser) -> Result<Vec<Uuid>, Error> {
175204
let connection = pool.get()?;
176205
let host = internal::user::user_by_id(&connection, &scope_and_user.user_id)?;
177-
SEARCH_USERS.run(&RuleContext::minimal(
206+
ONLY_ADMINS.run(&RuleContext::minimal(
178207
pool,
179208
scope_and_user,
180209
"",
181210
&host.user_uuid,
182211
))?;
183212
internal::user::all_staff(&connection)
184213
}
214+
215+
pub use internal::user::update_user_cache as _update_user_cache;

tests/api/inactive.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
use crate::helpers::api::*;
2+
use crate::helpers::db::get_pool;
3+
use crate::helpers::db::reset;
4+
use crate::helpers::misc::read_json;
5+
use crate::helpers::misc::test_app_and_cis;
6+
use crate::helpers::misc::Soa;
7+
use crate::helpers::sudo::add_to_group;
8+
use crate::helpers::users::basic_user;
9+
use crate::helpers::users::user_id;
10+
use actix_web::test;
11+
use actix_web::App;
12+
use cis_client::getby::GetBy;
13+
use cis_client::AsyncCisClientTrait;
14+
use dino_park_packs::db::operations::users::_update_user_cache;
15+
use dino_park_packs::db::operations::users::update_user_cache;
16+
use failure::Error;
17+
use serde_json::json;
18+
use std::sync::Arc;
19+
20+
#[actix_rt::test]
21+
async fn update_inactive() -> Result<(), Error> {
22+
reset()?;
23+
let (service, cis_client) = test_app_and_cis().await;
24+
let cis_client = Arc::new(cis_client);
25+
let app = App::new().service(service);
26+
let mut app = test::init_service(app).await;
27+
28+
let host_user = basic_user(1, true);
29+
let mut staff_user_1 = basic_user(2, true);
30+
let mut staff_user_2 = basic_user(3, true);
31+
let host = Soa::from(&host_user).aal_medium();
32+
33+
let res = post(
34+
&mut app,
35+
"/groups/api/v1/groups",
36+
json!({ "name": "inactive-test", "description": "a group", "trust": "Staff" }),
37+
&host.clone().creator(),
38+
)
39+
.await;
40+
assert!(res.status().is_success());
41+
42+
let res = get(&mut app, "/groups/api/v1/groups", &host).await;
43+
assert!(res.status().is_success());
44+
assert_eq!(read_json(res).await["groups"][0]["typ"], "Closed");
45+
46+
add_to_group(&mut app, &host, &staff_user_1, "inactive-test").await;
47+
add_to_group(&mut app, &host, &staff_user_2, "inactive-test").await;
48+
49+
let res = get(&mut app, "/groups/api/v1/members/inactive-test", &host).await;
50+
assert!(res.status().is_success());
51+
let members = read_json(res).await;
52+
assert_eq!(members["members"].as_array().map(|a| a.len()), Some(3));
53+
54+
let pool = get_pool();
55+
staff_user_2.active.value = Some(false);
56+
update_user_cache(&pool, &staff_user_2, Arc::clone(&cis_client)).await?;
57+
58+
let res = get(&mut app, "/groups/api/v1/members/inactive-test", &host).await;
59+
assert!(res.status().is_success());
60+
let members = read_json(res).await;
61+
assert_eq!(members["members"].as_array().map(|a| a.len()), Some(2));
62+
63+
// updating an inactive profile must not fail
64+
update_user_cache(&pool, &staff_user_2, Arc::clone(&cis_client)).await?;
65+
66+
let mut staff_user_2_reactivated = cis_client
67+
.get_user_by(&user_id(&staff_user_2), &GetBy::Uuid, None)
68+
.await?;
69+
// enabling a user again with groups resets groups to db state
70+
staff_user_2_reactivated.active.value = Some(true);
71+
update_user_cache(&pool, &staff_user_2_reactivated, Arc::clone(&cis_client)).await?;
72+
assert_eq!(
73+
cis_client
74+
.get_user_by(&user_id(&staff_user_2), &GetBy::Uuid, None)
75+
.await?
76+
.access_information
77+
.mozilliansorg
78+
.values
79+
.map(|kv| kv.0.is_empty()),
80+
Some(true)
81+
);
82+
83+
staff_user_1.active.value = Some(false);
84+
let connection = pool.get()?;
85+
_update_user_cache(&connection, &staff_user_1)?;
86+
87+
let res = get(&mut app, "/groups/api/v1/members/inactive-test", &host).await;
88+
assert!(res.status().is_success());
89+
let members = read_json(res).await;
90+
assert_eq!(members["members"].as_array().map(|a| a.len()), Some(2));
91+
92+
let res = delete(
93+
&mut app,
94+
"/groups/api/v1/sudo/user/inactive",
95+
&host.clone().admin(),
96+
)
97+
.await;
98+
assert!(res.status().is_success());
99+
100+
let res = get(&mut app, "/groups/api/v1/members/inactive-test", &host).await;
101+
assert!(res.status().is_success());
102+
let members = read_json(res).await;
103+
assert_eq!(members["members"].as_array().map(|a| a.len()), Some(1));
104+
105+
Ok(())
106+
}

tests/api/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ mod errors;
77
mod expiration;
88
mod groups;
99
mod import;
10+
mod inactive;
1011
mod invitations;
1112
mod join;
1213
mod requests;

tests/helpers/users.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ pub fn user_uuid(p: &Profile) -> String {
3030
p.uuid.value.clone().unwrap()
3131
}
3232

33+
pub fn user_id(p: &Profile) -> String {
34+
p.user_id.value.clone().unwrap()
35+
}
36+
3337
pub fn user_email(p: &Profile) -> String {
3438
p.primary_email.value.clone().unwrap()
3539
}

0 commit comments

Comments
 (0)