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
2 files+49−90/2 viewed
| 91 | 91 | } | |
| 92 | 92 | ||
| 93 | 93 | /// 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. | |
| 96 | 97 | pub fn item_scoped(event: &Event) -> Option<(String, Vec<String>)> { | |
| 97 | 98 | let about_item = event.kind.starts_with("work_item.") | |
| 98 | 99 | || (event.kind.starts_with("comment.") && event.data["itemId"].as_str().is_some_and(|id| !id.is_empty())); | |
| ⋯ | |||
| 107 | 108 | Some((workspace, repo_ids)) | |
| 108 | 109 | } | |
| 109 | 110 | ||
| 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 | + | ||
| 110 | 125 | /// What is sent to check a webhook works. | |
| 111 | 126 | pub fn ping(hook_id: &str, url: &str, events: &[String], time: &str) -> Value { | |
| 112 | 127 | json!({ | |
| ⋯ | |||
| 335 | 350 | } | |
| 336 | 351 | ||
| 337 | 352 | #[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] | |
| 338 | 365 | fn repository_lifecycle_events_can_be_chosen() { | |
| 339 | 366 | for kind in [ | |
| 340 | 367 | "repo.updated", | |
| 17 | 17 | use g1t_contracts::access::{self, Capability}; | |
| 18 | 18 | use g1t_contracts::events::Event; | |
| 19 | 19 | 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}; | |
| 21 | 21 | use g1t_contracts::time::rfc3339; | |
| 22 | 22 | use g1t_contracts::webhooks::*; | |
| 23 | 23 | use g1t_contracts::{FailureCode, Membership, Outcome, PrincipalKind, Role, User, Viewer, new_id}; | |
| ⋯ | |||
| 638 | 638 | self.send_all(event, hooks, Some((repo_id, name.as_ref()))).await | |
| 639 | 639 | } | |
| 640 | 640 | ||
| 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. | |
| 645 | 648 | 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); | |
| 646 | 659 | let hooks = self | |
| 647 | 660 | .db | |
| 648 | 661 | .prepare( | |
| 649 | 662 | "SELECT * FROM hooks WHERE active = 1 | |
| 650 | 663 | AND ((scope = 'workspace' AND workspace = ?1) OR (scope = 'repo' AND repo_id IN (SELECT value FROM json_each(?2))))", | |
| 651 | 664 | ) | |
| 652 | − | .bind(&[workspace.into(), serde_json::to_string(repo_ids)?.into()])? | |
| 665 | + | .bind(&[workspace.into(), serde_json::to_string(&repo_ids)?.into()])? | |
| 653 | 666 | .all() | |
| 654 | 667 | .await? | |
| 655 | 668 | .results::<HookRow>()?; | |
| 656 | 669 | if hooks.is_empty() { | |
| 657 | 670 | return Ok(()); | |
| 658 | 671 | } | |
| 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)); | |
| 660 | 673 | let name = match origin { | |
| 661 | 674 | Some(id) => self.repo_name(id, &[workspace.to_owned()]).await?, | |
| 662 | 675 | None => None, | |
Comments
Sign in to comment.