-
Notifications
You must be signed in to change notification settings - Fork 611
/
Copy pathsync_admins.rs
209 lines (171 loc) · 6.72 KB
/
sync_admins.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
use crate::email::Email;
use crate::schema::{emails, users};
use crate::worker::Environment;
use crates_io_worker::BackgroundJob;
use diesel::prelude::*;
use diesel_async::RunQueryDsl;
use std::collections::HashSet;
use std::fmt::{Display, Formatter};
use std::sync::Arc;
/// See <https://github.com/rust-lang/team/pull/1197>.
const PERMISSION_NAME: &str = "crates_io_admin";
#[derive(Serialize, Deserialize)]
pub struct SyncAdmins;
impl BackgroundJob for SyncAdmins {
const JOB_NAME: &'static str = "sync_admins";
const DEDUPLICATED: bool = true;
type Context = Arc<Environment>;
async fn run(&self, ctx: Self::Context) -> anyhow::Result<()> {
info!("Syncing admins from rust-lang/team repo…");
let repo_admins = ctx.team_repo.get_permission(PERMISSION_NAME).await?.people;
let repo_admin_ids = repo_admins
.iter()
.map(|m| m.github_id)
.collect::<HashSet<_>>();
let mut conn = ctx.deadpool.get().await?;
let format_repo_admins = |github_ids: &HashSet<i32>| {
repo_admins
.iter()
.filter(|m| github_ids.contains(&m.github_id))
.map(|m| format!("{} (github_id: {})", m.github, m.github_id))
.collect::<Vec<_>>()
};
// Existing admins from the database.
let database_admins = users::table
.left_join(emails::table)
.select((users::gh_id, users::gh_login, emails::email.nullable()))
.filter(users::is_admin.eq(true))
.get_results::<(i32, String, Option<String>)>(&mut conn)
.await?;
let database_admin_ids = database_admins
.iter()
.map(|(gh_id, _, _)| *gh_id)
.collect::<HashSet<_>>();
let format_database_admins = |github_ids: &HashSet<i32>| {
database_admins
.iter()
.filter(|(gh_id, _, _)| github_ids.contains(gh_id))
.map(|(gh_id, login, _)| format!("{} (github_id: {})", login, gh_id))
.collect::<Vec<_>>()
};
// New admins from the team repo that don't have admin access yet.
let new_admin_ids = repo_admin_ids
.difference(&database_admin_ids)
.copied()
.collect::<HashSet<_>>();
let added_admin_ids = if new_admin_ids.is_empty() {
Vec::new()
} else {
let new_admins = format_repo_admins(&new_admin_ids).join(", ");
debug!("Granting admin access: {new_admins}");
diesel::update(users::table)
.filter(users::gh_id.eq_any(&new_admin_ids))
.set(users::is_admin.eq(true))
.returning(users::gh_id)
.get_results::<i32>(&mut conn)
.await?
};
// New admins from the team repo that have been granted admin
// access now.
let added_admin_ids = HashSet::from_iter(added_admin_ids);
if !added_admin_ids.is_empty() {
let added_admins = format_repo_admins(&added_admin_ids).join(", ");
info!("Granted admin access: {added_admins}");
}
// New admins from the team repo that don't have a crates.io
// account yet.
let skipped_new_admin_ids = new_admin_ids
.difference(&added_admin_ids)
.copied()
.collect::<HashSet<_>>();
if !skipped_new_admin_ids.is_empty() {
let skipped_new_admins = format_repo_admins(&skipped_new_admin_ids).join(", ");
info!("Skipped missing admins: {skipped_new_admins}");
}
// Existing admins from the database that are no longer in the
// team repo.
let obsolete_admin_ids = database_admin_ids
.difference(&repo_admin_ids)
.copied()
.collect::<HashSet<_>>();
let removed_admin_ids = if obsolete_admin_ids.is_empty() {
Vec::new()
} else {
let obsolete_admins = format_database_admins(&obsolete_admin_ids).join(", ");
debug!("Revoking admin access: {obsolete_admins}");
diesel::update(users::table)
.filter(users::gh_id.eq_any(&obsolete_admin_ids))
.set(users::is_admin.eq(false))
.returning(users::gh_id)
.get_results::<i32>(&mut conn)
.await?
};
let removed_admin_ids = HashSet::from_iter(removed_admin_ids);
if !removed_admin_ids.is_empty() {
let removed_admins = format_database_admins(&removed_admin_ids).join(", ");
info!("Revoked admin access: {removed_admins}");
}
if added_admin_ids.is_empty() && removed_admin_ids.is_empty() {
return Ok(());
}
let added_admins = format_repo_admins(&added_admin_ids);
let removed_admins = format_database_admins(&removed_admin_ids);
let email = AdminAccountEmail::new(added_admins, removed_admins);
for database_admin in &database_admins {
let (_, _, email_address) = database_admin;
if let Some(email_address) = email_address {
if let Err(error) = ctx.emails.send(email_address, email.clone()).await {
warn!(
"Failed to send email to admin {} ({}, github_id: {}): {}",
database_admin.1, email_address, database_admin.0, error
);
}
} else {
warn!(
"No email address found for admin {} (github_id: {})",
database_admin.1, database_admin.0
);
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
struct AdminAccountEmail {
added_admins: Vec<String>,
removed_admins: Vec<String>,
}
impl AdminAccountEmail {
fn new(added_admins: Vec<String>, removed_admins: Vec<String>) -> Self {
Self {
added_admins,
removed_admins,
}
}
}
impl Email for AdminAccountEmail {
fn subject(&self) -> String {
"crates.io: Admin account changes".into()
}
fn body(&self) -> String {
self.to_string()
}
}
impl Display for AdminAccountEmail {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if !self.added_admins.is_empty() {
writeln!(f, "Granted admin access:\n")?;
for new_admin in &self.added_admins {
writeln!(f, "- {}", new_admin)?;
}
writeln!(f)?;
}
if !self.removed_admins.is_empty() {
writeln!(f, "Revoked admin access:")?;
for obsolete_admin in &self.removed_admins {
writeln!(f, "- {}", obsolete_admin)?;
}
}
Ok(())
}
}