Skip to content

Commit c1558ce

Browse files
committed
Simplify user impersonation
1 parent da102f2 commit c1558ce

File tree

1 file changed

+112
-150
lines changed

1 file changed

+112
-150
lines changed

src/zulip.rs

Lines changed: 112 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -172,95 +172,133 @@ async fn process_zulip_request(ctx: &Context, req: Request) -> anyhow::Result<Op
172172
handle_command(ctx, gh_id, &req.data, &req.message).await
173173
}
174174

175-
fn handle_command<'a>(
175+
async fn handle_command<'a>(
176176
ctx: &'a Context,
177-
gh_id: u64,
177+
mut gh_id: u64,
178178
command: &'a str,
179179
message_data: &'a Message,
180-
) -> std::pin::Pin<Box<dyn std::future::Future<Output = anyhow::Result<Option<String>>> + Send + 'a>>
181-
{
182-
Box::pin(async move {
183-
log::trace!("handling zulip command {:?}", command);
184-
let words: Vec<&str> = command.split_whitespace().collect();
180+
) -> anyhow::Result<Option<String>> {
181+
log::trace!("handling zulip command {:?}", command);
182+
let words: Vec<&str> = command.split_whitespace().collect();
185183

184+
// Missing stream means that this is a direct message
185+
if message_data.stream_id.is_none() {
186+
// Handle impersonation
187+
let mut impersonated = false;
186188
if let Some(&"as") = words.get(0) {
187-
return execute_for_other_user(&ctx, words.iter().skip(1).copied(), message_data)
188-
.await
189-
.map_err(|e| {
190-
format_err!("Failed to parse; expected `as <username> <command...>`: {e:?}.")
191-
});
189+
if let Some(username) = words.get(1) {
190+
impersonated = true;
191+
192+
// Impersonate => change actual gh_id
193+
gh_id = match get_id_for_username(&ctx.github, username)
194+
.await
195+
.context("getting ID of github user")?
196+
{
197+
Some(id) => id.try_into().unwrap(),
198+
None => anyhow::bail!("Can only authorize for other GitHub users."),
199+
};
200+
} else {
201+
return Err(anyhow::anyhow!(
202+
"Failed to parse command; expected `as <username> <command...>`."
203+
));
204+
}
192205
}
193206

194-
// Missing stream means that this is a direct message
195-
if message_data.stream_id.is_none() {
196-
let cmd = parse_cli::<ChatCommand, _>(words.into_iter())?;
197-
match cmd {
198-
ChatCommand::Acknowledge { identifier } => {
199-
acknowledge(&ctx, gh_id, (&identifier).into()).await
200-
}
201-
ChatCommand::Add { url, description } => {
202-
add_notification(&ctx, gh_id, &url, &description.join(" ")).await
203-
}
204-
ChatCommand::Move { from, to } => move_notification(&ctx, gh_id, from, to).await,
205-
ChatCommand::Meta { index, description } => {
206-
add_meta_notification(&ctx, gh_id, index, &description.join(" ")).await
207-
}
208-
ChatCommand::Whoami => whoami_cmd(&ctx, gh_id).await,
209-
ChatCommand::Lookup(cmd) => lookup_cmd(&ctx, cmd).await,
210-
ChatCommand::Work(cmd) => workqueue_commands(ctx, gh_id, cmd).await,
207+
let cmd = parse_cli::<ChatCommand, _>(words.into_iter())?;
208+
let output = match cmd {
209+
ChatCommand::Acknowledge { identifier } => {
210+
acknowledge(&ctx, gh_id, (&identifier).into()).await
211211
}
212-
} else {
213-
// We are in a stream, where someone wrote `@**triagebot** <command(s)>`
214-
let cmd_index = words
212+
ChatCommand::Add { url, description } => {
213+
add_notification(&ctx, gh_id, &url, &description.join(" ")).await
214+
}
215+
ChatCommand::Move { from, to } => move_notification(&ctx, gh_id, from, to).await,
216+
ChatCommand::Meta { index, description } => {
217+
add_meta_notification(&ctx, gh_id, index, &description.join(" ")).await
218+
}
219+
ChatCommand::Whoami => whoami_cmd(&ctx, gh_id).await,
220+
ChatCommand::Lookup(cmd) => lookup_cmd(&ctx, cmd).await,
221+
ChatCommand::Work(cmd) => workqueue_commands(ctx, gh_id, cmd).await,
222+
};
223+
224+
let output = output?;
225+
226+
// Let the impersonated person know about the impersonation
227+
if impersonated {
228+
let impersonated_zulip_id = to_zulip_id(&ctx.github, gh_id)
229+
.await?
230+
.ok_or_else(|| anyhow::anyhow!("Zulip user for GitHub ID {gh_id} was not found"))?;
231+
let users = ctx.zulip.get_zulip_users().await?;
232+
let user = users
215233
.iter()
216-
.position(|w| *w == "@**triagebot**")
217-
.unwrap_or(words.len());
218-
let cmd_index = cmd_index + 1;
219-
if cmd_index >= words.len() {
220-
return Ok(Some("Unknown command".to_string()));
234+
.find(|m| m.user_id == impersonated_zulip_id)
235+
.ok_or_else(|| format_err!("Could not find Zulip user email."))?;
236+
237+
let sender = &message_data.sender_full_name;
238+
let message = format!(
239+
"{sender} ran `{command}` on your behalf. Output:\n{}",
240+
output.as_deref().unwrap_or("<empty>")
241+
);
242+
243+
MessageApiRequest {
244+
recipient: Recipient::Private {
245+
id: user.user_id,
246+
email: &user.email,
247+
},
248+
content: &message,
221249
}
222-
let cmd = parse_cli::<StreamCommand, _>(words[cmd_index..].into_iter().copied())?;
223-
match cmd {
224-
StreamCommand::EndTopic => {
225-
post_waiter(&ctx, message_data, WaitingMessage::end_topic())
226-
.await
227-
.map_err(|e| format_err!("Failed to await at this time: {e:?}"))
228-
}
229-
StreamCommand::EndMeeting => {
230-
post_waiter(&ctx, message_data, WaitingMessage::end_meeting())
231-
.await
232-
.map_err(|e| format_err!("Failed to await at this time: {e:?}"))
233-
}
234-
StreamCommand::Read => {
235-
post_waiter(&ctx, message_data, WaitingMessage::start_reading())
236-
.await
237-
.map_err(|e| format_err!("Failed to await at this time: {e:?}"))
238-
}
239-
StreamCommand::PingGoals {
240-
threshold,
241-
next_update,
242-
} => {
243-
if project_goals::check_project_goal_acl(&ctx.github, gh_id).await? {
244-
ping_project_goals_owners(
245-
&ctx.github,
246-
&ctx.zulip,
247-
false,
248-
threshold as i64,
249-
&format!("on {next_update}"),
250-
)
251-
.await
252-
.map_err(|e| format_err!("Failed to await at this time: {e:?}"))?;
253-
Ok(None)
254-
} else {
255-
Err(format_err!(
250+
.send(&ctx.zulip)
251+
.await?;
252+
}
253+
254+
Ok(output)
255+
} else {
256+
// We are in a stream, where someone wrote `@**triagebot** <command(s)>`
257+
let cmd_index = words
258+
.iter()
259+
.position(|w| *w == "@**triagebot**")
260+
.unwrap_or(words.len());
261+
let cmd_index = cmd_index + 1;
262+
if cmd_index >= words.len() {
263+
return Ok(Some("Unknown command".to_string()));
264+
}
265+
let cmd = parse_cli::<StreamCommand, _>(words[cmd_index..].into_iter().copied())?;
266+
match cmd {
267+
StreamCommand::EndTopic => post_waiter(&ctx, message_data, WaitingMessage::end_topic())
268+
.await
269+
.map_err(|e| format_err!("Failed to await at this time: {e:?}")),
270+
StreamCommand::EndMeeting => {
271+
post_waiter(&ctx, message_data, WaitingMessage::end_meeting())
272+
.await
273+
.map_err(|e| format_err!("Failed to await at this time: {e:?}"))
274+
}
275+
StreamCommand::Read => post_waiter(&ctx, message_data, WaitingMessage::start_reading())
276+
.await
277+
.map_err(|e| format_err!("Failed to await at this time: {e:?}")),
278+
StreamCommand::PingGoals {
279+
threshold,
280+
next_update,
281+
} => {
282+
if project_goals::check_project_goal_acl(&ctx.github, gh_id).await? {
283+
ping_project_goals_owners(
284+
&ctx.github,
285+
&ctx.zulip,
286+
false,
287+
threshold as i64,
288+
&format!("on {next_update}"),
289+
)
290+
.await
291+
.map_err(|e| format_err!("Failed to await at this time: {e:?}"))?;
292+
Ok(None)
293+
} else {
294+
Err(format_err!(
256295
"That command is only permitted for those running the project-goal program.",
257296
))
258-
}
259297
}
260-
StreamCommand::DocsUpdate => trigger_docs_update(message_data, &ctx.zulip),
261298
}
299+
StreamCommand::DocsUpdate => trigger_docs_update(message_data, &ctx.zulip),
262300
}
263-
})
301+
}
264302
}
265303

266304
/// Commands for working with the workqueue, e.g. showing how many PRs are assigned
@@ -545,82 +583,6 @@ async fn lookup_zulip_username(ctx: &Context, gh_username: &str) -> anyhow::Resu
545583
))
546584
}
547585

548-
// This does two things:
549-
// * execute the command for the other user
550-
// * tell the user executed for that a command was run as them by the user
551-
// given.
552-
async fn execute_for_other_user(
553-
ctx: &Context,
554-
mut words: impl Iterator<Item = &str>,
555-
message_data: &Message,
556-
) -> anyhow::Result<Option<String>> {
557-
// username is a GitHub username, not a Zulip username
558-
let username = match words.next() {
559-
Some(username) => username,
560-
None => anyhow::bail!("no username provided"),
561-
};
562-
let user_id = match get_id_for_username(&ctx.github, username)
563-
.await
564-
.context("getting ID of github user")?
565-
{
566-
Some(id) => id.try_into().unwrap(),
567-
None => anyhow::bail!("Can only authorize for other GitHub users."),
568-
};
569-
let mut command = words.fold(String::new(), |mut acc, piece| {
570-
acc.push_str(piece);
571-
acc.push(' ');
572-
acc
573-
});
574-
let command = if command.is_empty() {
575-
anyhow::bail!("no command provided")
576-
} else {
577-
assert_eq!(command.pop(), Some(' ')); // pop trailing space
578-
command
579-
};
580-
581-
let members = ctx
582-
.zulip
583-
.get_zulip_users()
584-
.await
585-
.map_err(|e| format_err!("Failed to get list of zulip users: {e:?}."))?;
586-
587-
// Map GitHub `user_id` to `zulip_user_id`.
588-
let zulip_user_id = match to_zulip_id(&ctx.github, user_id).await {
589-
Ok(Some(id)) => id as u64,
590-
Ok(None) => anyhow::bail!("Could not find Zulip ID for GitHub ID: {user_id}"),
591-
Err(e) => anyhow::bail!("Could not find Zulip ID for GitHub id {user_id}: {e:?}"),
592-
};
593-
594-
let user = members
595-
.iter()
596-
.find(|m| m.user_id == zulip_user_id)
597-
.ok_or_else(|| format_err!("Could not find Zulip user email."))?;
598-
599-
let output = handle_command(ctx, user_id, &command, message_data)
600-
.await?
601-
.unwrap_or_default();
602-
603-
// At this point, the command has been run.
604-
let sender = &message_data.sender_full_name;
605-
let message = format!("{sender} ran `{command}` with output `{output}` as you.");
606-
607-
let res = MessageApiRequest {
608-
recipient: Recipient::Private {
609-
id: user.user_id,
610-
email: &user.email,
611-
},
612-
content: &message,
613-
}
614-
.send(&ctx.zulip)
615-
.await;
616-
617-
if let Err(err) = res {
618-
log::error!("Failed to notify real user about command: {:?}", err);
619-
}
620-
621-
Ok(Some(output))
622-
}
623-
624586
#[derive(serde::Serialize)]
625587
pub(crate) struct MessageApiRequest<'a> {
626588
pub(crate) recipient: Recipient<'a>,

0 commit comments

Comments
 (0)