diff --git a/docs/pages/operate/slack-etl.mdx b/docs/pages/operate/slack-etl.mdx index 1a9747de8..6f90599cc 100644 --- a/docs/pages/operate/slack-etl.mdx +++ b/docs/pages/operate/slack-etl.mdx @@ -39,8 +39,9 @@ posting to Slack. Create a Slack user token for ETL reads and store it as `SLACK_ETL_TOKEN` in the same secret source used by tools. The Slack tool declares it as an optional -HTTP secret for `slack.com` and `files.slack.com`; iron-proxy injects the real -value when the tool calls Slack. +HTTP secret scoped to the Slack Web API endpoints below and `GET` file downloads +from `files.slack.com`; iron-proxy injects the real value when the workflow +calls Slack. The token must be able to call: diff --git a/docs/public/md/operate/slack-etl.md b/docs/public/md/operate/slack-etl.md index 1a9747de8..6f90599cc 100644 --- a/docs/public/md/operate/slack-etl.md +++ b/docs/public/md/operate/slack-etl.md @@ -39,8 +39,9 @@ posting to Slack. Create a Slack user token for ETL reads and store it as `SLACK_ETL_TOKEN` in the same secret source used by tools. The Slack tool declares it as an optional -HTTP secret for `slack.com` and `files.slack.com`; iron-proxy injects the real -value when the tool calls Slack. +HTTP secret scoped to the Slack Web API endpoints below and `GET` file downloads +from `files.slack.com`; iron-proxy injects the real value when the workflow +calls Slack. The token must be able to call: diff --git a/services/api-rs/crates/centaur-perms/src/tests.rs b/services/api-rs/crates/centaur-perms/src/tests.rs index f93ad99a9..79febb1e2 100644 --- a/services/api-rs/crates/centaur-perms/src/tests.rs +++ b/services/api-rs/crates/centaur-perms/src/tests.rs @@ -1003,6 +1003,62 @@ fn real_slack_tool_parses_and_translates() { ), "expected the SLACK_BOT_TOKEN static secret" ); + + let etl_inputs = out + .inputs + .iter() + .filter_map(|input| match input { + SecretInput::Static(secret) if secret.name == "SLACK_ETL_TOKEN" => Some(secret), + _ => None, + }) + .collect::>(); + assert_eq!(etl_inputs.len(), 2); + + let slack_api = etl_inputs + .iter() + .find(|secret| { + secret + .rules + .iter() + .any(|rule| rule.host.as_deref() == Some("slack.com")) + }) + .expect("expected Slack Web API ETL token rules"); + let mut slack_hosts = slack_api + .rules + .iter() + .map(|rule| rule.host.as_deref().unwrap_or_default().to_owned()) + .collect::>(); + slack_hosts.sort(); + assert_eq!( + slack_hosts, + vec!["slack.com".to_owned(), "www.slack.com".to_owned()] + ); + for rule in &slack_api.rules { + assert_eq!(rule.http_methods, vec!["GET".to_owned(), "POST".to_owned()]); + assert_eq!( + rule.paths, + vec![ + "/api/conversations.list".to_owned(), + "/api/conversations.history".to_owned(), + "/api/conversations.replies".to_owned(), + "/api/users.list".to_owned(), + ] + ); + } + + let files = etl_inputs + .iter() + .find(|secret| { + secret + .rules + .iter() + .any(|rule| rule.host.as_deref() == Some("files.slack.com")) + }) + .expect("expected Slack file download ETL token rule"); + assert_eq!(files.rules.len(), 1); + assert_eq!(files.rules[0].host.as_deref(), Some("files.slack.com")); + assert_eq!(files.rules[0].http_methods, vec!["GET".to_owned()]); + assert!(files.rules[0].paths.is_empty()); } #[test] diff --git a/tools/productivity/slack/pyproject.toml b/tools/productivity/slack/pyproject.toml index c9ce75886..7c18c3440 100644 --- a/tools/productivity/slack/pyproject.toml +++ b/tools/productivity/slack/pyproject.toml @@ -32,5 +32,6 @@ optional_secrets = [ {type = "http", name = "SLACK_BOT_TOKEN", mode = "inject", inject_header = "Authorization", inject_formatter = "Bearer {{ .Value }}", hosts = ["slack.com"]}, {type = "http", name = "SLACK_SEARCH_TOKEN", mode = "inject", inject_header = "Authorization", inject_formatter = "Bearer {{ .Value }}", hosts = ["slack.com", "www.slack.com"], methods = ["POST"], paths = ["/api/assistant.search.context"]}, {type = "http", name = "SLACK_UPLOAD_TOKEN", mode = "inject", inject_header = "Authorization", inject_formatter = "Bearer {{ .Value }}", hosts = ["slack.com", "files.slack.com"]}, - {type = "http", name = "SLACK_ETL_TOKEN", mode = "inject", inject_header = "Authorization", inject_formatter = "Bearer {{ .Value }}", hosts = ["slack.com", "files.slack.com"]}, + {type = "http", name = "SLACK_ETL_TOKEN", mode = "inject", inject_header = "Authorization", inject_formatter = "Bearer {{ .Value }}", hosts = ["slack.com", "www.slack.com"], methods = ["GET", "POST"], paths = ["/api/conversations.list", "/api/conversations.history", "/api/conversations.replies", "/api/users.list"]}, + {type = "http", name = "SLACK_ETL_TOKEN", mode = "inject", inject_header = "Authorization", inject_formatter = "Bearer {{ .Value }}", hosts = ["files.slack.com"], methods = ["GET"]}, ]