Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

Commit

Webhooks of a repository a private work item only names are not told of it

A work item event always reaches its workspace's webhooks, its origin's (the event's repository), and the other linked repositories' only when its origin is public, as an anonymous readable call says. An item with no origin reaches no repository's webhooks.

G1T-Session:
ses_01m56w3nnce54skbep969amaqy
syntaqxcommitted Parent4ec4b3fBrowse files
2 files+49−90/2 viewed
+29−2
9191 }
9292
9393 /// For an event about a work item (`work_item.*`, or a comment on one):
94−/// its workspace, and every repository it is linked to. It goes to the
95−/// workspace's webhooks and to those of each of those repositories.
94+/// its workspace, and every repository it is linked to. It always goes to
95+/// the workspace's webhooks; which of those repositories' webhooks hear of
96+/// it is [`item_hook_repos`]'s to say.
9697 pub fn item_scoped(event: &Event) -> Option<(String, Vec<String>)> {
9798 let about_item = event.kind.starts_with("work_item.")
9899 || (event.kind.starts_with("comment.") && event.data["itemId"].as_str().is_some_and(|id| !id.is_empty()));
107108 Some((workspace, repo_ids))
108109 }
109110
111+/// The repositories, of those a work item is linked to (`repo_ids`),
112+/// whose webhooks may be told of it: only those whose readers can all read
113+/// it. That is its origin (the event's own repository, `origin`), whose
114+/// readers may read it; and every one of them when its origin is public
115+/// (`origin_public`), as anyone may then. An item with no origin is its
116+/// workspace members' alone: no repository's webhooks hear of it, nor do
117+/// those of a repository it only names while its origin is private.
118+pub fn item_hook_repos(origin: Option<&str>, repo_ids: &[String], origin_public: bool) -> Vec<String> {
119+ let Some(origin) = origin.filter(|id| !id.is_empty()) else {
120+ return Vec::new();
121+ };
122+ repo_ids.iter().filter(|id| origin_public || id.as_str() == origin).cloned().collect()
123+}
124+
110125 /// What is sent to check a webhook works.
111126 pub fn ping(hook_id: &str, url: &str, events: &[String], time: &str) -> Value {
112127 json!({
335350 }
336351
337352 #[test]
353+ fn a_work_item_reaches_only_the_webhooks_of_repositories_whose_readers_may_read_it() {
354+ let linked = vec!["rep_1".to_owned(), "rep_2".to_owned()];
355+ // A private origin: its own webhooks, not those of a repository it only names.
356+ assert_eq!(item_hook_repos(Some("rep_1"), &linked, false), vec!["rep_1".to_owned()]);
357+ // A public one: anyone may read it, so every repository it is linked to.
358+ assert_eq!(item_hook_repos(Some("rep_1"), &linked, true), linked);
359+ // No origin: the workspace's webhooks alone, whatever it is linked to.
360+ assert!(item_hook_repos(None, &["rep_2".to_owned()], false).is_empty());
361+ assert!(item_hook_repos(None, &["rep_2".to_owned()], true).is_empty());
362+ }
363+
364+ #[test]
338365 fn repository_lifecycle_events_can_be_chosen() {
339366 for kind in [
340367 "repo.updated",
+20−7
1717 use g1t_contracts::access::{self, Capability};
1818 use g1t_contracts::events::Event;
1919 use g1t_contracts::identity::{AGENT_ID, AGENT_NAME, UsernamesArgs};
20−use g1t_contracts::repos::{GetArgs, GetByIdArgs, Repo};
20+use g1t_contracts::repos::{GetArgs, GetByIdArgs, ReadableArgs, Repo};
2121 use g1t_contracts::time::rfc3339;
2222 use g1t_contracts::webhooks::*;
2323 use g1t_contracts::{FailureCode, Membership, Outcome, PrincipalKind, Role, User, Viewer, new_id};
638638 self.send_all(event, hooks, Some((repo_id, name.as_ref()))).await
639639 }
640640
641− /// An event about a work item: to its workspace's webhooks, and to
642− /// those of every repository it is linked to. Each repository's
643− /// webhooks are told of their own repository; the workspace's, of
644− /// where it was filed, when it was filed in one.
641+ /// An event about a work item: always to its workspace's webhooks, and
642+ /// to those of the repositories it is linked to whose readers may all
643+ /// read it (`deliver::item_hook_repos`): its origin's (the event's own
644+ /// repository), and the others' only when its origin is public, as one
645+ /// anonymous `readable` call says. Each repository's webhooks are told
646+ /// of their own repository; the workspace's, of where it was filed,
647+ /// when it was filed in one.
645648 async fn on_item_event(&self, event: &Event, workspace: &str, repo_ids: &[String]) -> Result<()> {
649+ let origin = event.repo_id.as_deref().filter(|id| !id.is_empty());
650+ let origin_public = match origin {
651+ Some(id) if repo_ids.iter().any(|linked| linked != id) => {
652+ let public: Vec<Repo> =
653+ g1t_kit::call(&self.repos, "readable", &ReadableArgs { ids: vec![id.to_owned()], viewer: None }).await?;
654+ public.iter().any(|repo| repo.id == id)
655+ }
656+ _ => false,
657+ };
658+ let repo_ids = deliver::item_hook_repos(origin, repo_ids, origin_public);
646659 let hooks = self
647660 .db
648661 .prepare(
649662 "SELECT * FROM hooks WHERE active = 1
650663 AND ((scope = 'workspace' AND workspace = ?1) OR (scope = 'repo' AND repo_id IN (SELECT value FROM json_each(?2))))",
651664 )
652− .bind(&[workspace.into(), serde_json::to_string(repo_ids)?.into()])?
665+ .bind(&[workspace.into(), serde_json::to_string(&repo_ids)?.into()])?
653666 .all()
654667 .await?
655668 .results::<HookRow>()?;
656669 if hooks.is_empty() {
657670 return Ok(());
658671 }
659− let origin = event.repo_id.as_deref().filter(|id| repo_ids.iter().any(|linked| linked == id));
672+ let origin = origin.filter(|id| repo_ids.iter().any(|linked| linked == id));
660673 let name = match origin {
661674 Some(id) => self.repo_name(id, &[workspace.to_owned()]).await?,
662675 None => None,

Comments

Sign in to comment.