-
-
Notifications
You must be signed in to change notification settings - Fork 751
Expand file tree
/
Copy pathupdate_user_from_github.rs
More file actions
210 lines (191 loc) · 8.54 KB
/
Copy pathupdate_user_from_github.rs
File metadata and controls
210 lines (191 loc) · 8.54 KB
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
210
use crate::{
models::OauthGithub,
schema::{oauth_github, users},
worker::Environment,
};
use anyhow::anyhow;
use chrono::Utc;
use crates_io_github::{GitHubAuth, GitHubError, GitHubUser};
use crates_io_worker::BackgroundJob;
use diesel::prelude::*;
use diesel_async::{AsyncConnection, AsyncPgConnection, RunQueryDsl};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tracing::{error, info};
#[derive(Serialize, Deserialize)]
pub struct UpdateUserFromGithub {
/// Dry run will fetch updates from GitHub and log what it would change, but does not actually
/// update the database.
pub dry_run: bool,
/// GitHub ID
pub account_id: i64,
}
impl BackgroundJob for UpdateUserFromGithub {
const JOB_NAME: &'static str = "update_user_from_github";
const DEDUPLICATED: bool = true;
// These jobs aren't urgent and shouldn't page anyone if they take a long time.
const PRIORITY: i16 = -15;
type Context = Arc<Environment>;
/// For the specified user, queries the GitHub API for the user's current information to see if
/// their account has been deleted or renamed. Updates the `users` and `oauth_github` tables,
/// saving the current time in `last_sync` even if the user information hasn't changed.
async fn run(&self, ctx: Self::Context) -> anyhow::Result<()> {
let mut conn = ctx.deadpool.get().await?;
// If no oauth_github info with this account id is found, then the record has been deleted
// since this job was enqueued. Stop and exit with success so we don't retry the job.
let oauth_github = oauth_github::table
.filter(oauth_github::account_id.eq(self.account_id))
.first::<OauthGithub>(&mut conn)
.await?;
info!(
"Starting UpdateUserFromGithub ({}): user_id {}, github_id {}, old username {}",
if self.dry_run { "DRY RUN" } else { "FOR REAL" },
oauth_github.user_id,
self.account_id,
oauth_github.login,
);
let github_user = self.refresh_user(&ctx, &oauth_github).await?;
let prefix = if self.dry_run {
"[DRY RUN] "
} else {
Default::default()
};
if oauth_github.login == github_user.login {
info!(
"{}UpdateUserFromGithub for crates.io user {} \
no change from username `{}`",
prefix, oauth_github.user_id, oauth_github.login,
);
} else {
info!(
"{}UpdateUserFromGithub update for crates.io user {} \
from username `{}` to username `{}`",
prefix, oauth_github.user_id, oauth_github.login, github_user.login,
);
}
if !self.dry_run {
self.apply_update(&oauth_github, &github_user, &mut conn)
.await;
}
Ok(())
}
}
impl UpdateUserFromGithub {
/// Given the current environment's context, requests information from GitHub using the user's
/// API token.
async fn refresh_user(
&self,
ctx: &Arc<Environment>,
oauth_github: &OauthGithub,
) -> anyhow::Result<GitHubUser> {
let github = ctx.github.as_ref();
let token = ctx
.config
.gh_token_encryption
.decrypt(&oauth_github.encrypted_token)?;
let auth = GitHubAuth::bearer(token);
match github.current_user(&auth).await {
Ok(github_user) => Ok(github_user),
// If the user is not found, the account has been deleted. Update to the ghost
// username.
Err(GitHubError::NotFound(_)) => Ok(self.ghost_user(oauth_github.user_id)),
// Unauthorized/forbidden could mean:
//
// - the token we have for this user is out-of-date
// - the user has revoked crates.io's oauth access
//
// In those cases, try to request the user's info via a GitHub API request
// authenticated with our sync GitHub app's token, unless they are a GitHub Enterprise
// Managed User as indicated by an underscore in their username because we have to be
// authorized by the managing enterprise to see any information on enterprise managed
// users.
Err(GitHubError::Unauthorized(_)) | Err(GitHubError::Forbidden(_)) => {
// Enterprise managed users are the only ones that should contain underscores.
if oauth_github.login.contains('_') {
// We can't get updated info, so keep what we have.
Ok(GitHubUser {
login: oauth_github.login.clone(),
id: self.account_id as i32,
// The other fields are not used in `apply_update`.
avatar_url: Default::default(),
email: Default::default(),
name: Default::default(),
})
} else {
let Some(sync_github_app) = ctx.sync_github_app.as_ref() else {
let error =
anyhow!("sync github app not configured, can't make user API request");
return Err(error);
};
let token = sync_github_app.installation_token().await?;
let auth = GitHubAuth::bearer(token);
match github.get_user_by_id(self.account_id, &auth).await {
Ok(github_user) => Ok(github_user),
Err(GitHubError::NotFound(_)) => Ok(self.ghost_user(oauth_github.user_id)),
// For any other error, stop and try this user again later.
Err(e) => Err(e.into()),
}
}
}
// If we get another sort of error, it may be transient; stop and try this user
// again later.
Err(e @ GitHubError::Other(_)) => Err(e.into()),
}
}
/// Given the information from GitHub about the current user, makes the appropriate changes to
/// the `users` and `oauth_github` tables.
async fn apply_update(
&self,
oauth_github: &OauthGithub,
github_user: &GitHubUser,
conn: &mut AsyncPgConnection,
) {
// Use a transaction so that we either update both or neither the `users` record and the
// corresponding `oauth_github` record. If neither are updated, log and continue to the
// next user rather than stopping-- hopefully we'll get that user updated next time.
if let Err(e) = conn
.transaction(async |conn| {
// This will be removed when we no longer sync crates.io usernames with GitHub.
// (The transaction can be removed when this is removed as well)
// It's only needed if there's a change in username.
if oauth_github.login != github_user.login {
diesel::update(users::table)
.filter(users::id.eq(oauth_github.user_id))
.set(users::gh_login.eq(&github_user.login))
.execute(conn)
.await?;
}
// This update is needed even if there's no change in username to set the
// `last_sync` time to `now`.
diesel::update(oauth_github::table)
.filter(oauth_github::account_id.eq(self.account_id))
.set((
oauth_github::login.eq(&github_user.login),
oauth_github::last_sync.eq(Utc::now()),
))
.execute(conn)
.await?;
Ok::<(), diesel::result::Error>(())
})
.await
{
// Database update failed; it's ok to not update this user this round.
// Better luck next time.
error!(
"Could not update user ID {} from username {} to username {}: {e}",
oauth_github.user_id, oauth_github.login, github_user.login,
);
}
}
/// If this user has been deleted, ensures their username has been changed to
/// `ghost_{crates.io id}` to ensure uniqueness by creating a `GitHubUser` by hand.
fn ghost_user(&self, user_id: i32) -> GitHubUser {
GitHubUser {
avatar_url: None,
email: None,
id: self.account_id as i32,
login: format!("ghost_{}", user_id),
name: None,
}
}
}