mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-22 00:01:34 +00:00
95d4c6a94d
* feat(cli): non-interactive Slack connect/disconnect
Extract create_slack_workspace_artifacts / create_slack_instance_artifacts
from the browser OAuth callbacks and expose them via two new endpoints that
accept a pre-minted xoxb bot token:
- POST /w/{workspace}/workspaces/connect_slack (admin)
- POST /oauth/connect_slack_instance (super-admin)
Both produce bit-for-bit identical DB state to the UI browser flow.
Wire three CLI commands as thin wrappers:
- wmill workspace connect-slack
- wmill workspace disconnect-slack
- wmill instance connect-slack
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cli): round-trip stability for workspace settings handlers
wmill sync push was destroying UI-configured error_handler/success_handler
state on every deploy. Two orthogonal bugs:
(a) pushWorkspaceSettings called editErrorHandler with `path: undefined`
when the YAML lacked the handler block, which the backend treats as a
clear — so syncing settings.yaml that didn't mention the handler wiped
the DB row. Fix: skip the call entirely when absent from YAML.
(b) edit_error_handler omitted muted_on_cancel / muted_on_user_path when
false, but the CLI always sends them, causing perpetual deepEqual
drift and a spurious editErrorHandler call on every sync push. Fix:
always persist both booleans.
migrateToGroupedFormat now preserves explicit `null` on
error_handler / success_handler as a "clear remote" signal distinct from
absence. Widen ErrorHandlerConfig | null / SuccessHandlerConfig | null to
make this explicit in the type.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(cli): sync support for workspace-level Slack OAuth override
Add slack_oauth_client_id and slack_oauth_client_secret to the v2 tarball
export and to pushWorkspaceSettings, so the workspace-level OAuth override
is now fully managed as code through settings.yaml.
Semantics:
- both defined and truthy → setWorkspaceSlackOauthConfig (upsert)
- both defined but falsy (e.g. empty strings) and remote has a value
→ deleteWorkspaceSlackOauthConfig
- either omitted → leave remote alone ("not managed by git")
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(cli): normalize workspace settings sync to "omit = clear"
Earlier commits on this branch introduced an "omit = keep" rule for
error_handler / success_handler / slack_oauth_client_{id,secret} that
diverged from every other workspace setting (webhook, deploy_to, etc. all
treat YAML as canonical: absence = clear). Normalize:
- v2 tarball always emits these 4 fields (null when remote is NULL) so
round-trip is bijective and settings.yaml is a complete snapshot.
- pushWorkspaceSettings drops the absent-from-YAML guards; YAML is
canonical. Absence and explicit null both clear the remote — same rule
as every other field.
- set_slack_oauth_config / delete_slack_oauth_config now fire
handle_deployment_metadata so UI mutations reach git-sync-enabled
workspaces' committed settings.yaml.
Policy for users: pull before push (same as every other setting). On first
post-upgrade pull, explicit `null` keys appear for any workspace whose
handlers / oauth override are unset — one-time YAML diff, no semantic
change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(cli): add unit + integration coverage for Slack settings sync
Unit tests (settings_unit.test.ts): cover migrateToGroupedFormat preserving
explicit `null` on error_handler / success_handler, and passthrough of
slack_oauth_client_id / _secret (both populated and null values).
Integration tests (slack_settings_sync.test.ts, skipped on CI per the same
convention as datatable_settings_sync.test.ts): exercise the full backend
via withTestBackend to verify
1. pull emits null for unset error_handler / success_handler /
slack_oauth_client_id / _secret;
2. round-trip with all-null handlers is idempotent;
3. push of populated slack_oauth_config upserts;
4. omitting the slack_oauth keys from YAML clears remote (universal
"omit = clear" rule);
5. explicit null error_handler in YAML clears remote;
6. round-trip preserves a populated error_handler exactly, including the
always-persisted muted_on_cancel / muted_on_user_path booleans.
Also feature-gates `use crate::oauth2_oss::workspace_connect_slack` and its
route registration behind `cfg(feature = "oauth2")`: the import caused a
build failure on subsets of the workspace without the oauth2 feature,
surfaced by the integration test harness.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: bump ee-repo-ref to 59b6123
Pins windmill-ee-private to the tip of branch alp/slack_cli, which
contains the companion EE changes (helper extraction, non-interactive
Slack connect handlers, git-sync for Slack settings mutations).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Update SQLx metadata
* chore: regenerate system prompts for new slack CLI commands
Captures the new workspace connect-slack, workspace disconnect-slack,
and instance connect-slack commands in the auto-generated files that
CI enforces via system_prompts/check-freshness.sh.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to b4a5ca11e3b96ff03793c2bd396dbc1fe6ea1022
This commit updates the EE repository reference after PR #550 was merged in windmill-ee-private.
Previous ee-repo-ref: d7e44d0519327ec9077625130365e887826f324b
New ee-repo-ref: b4a5ca11e3b96ff03793c2bd396dbc1fe6ea1022
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
198 lines
5.9 KiB
Rust
198 lines
5.9 KiB
Rust
#[cfg(feature = "private")]
|
|
#[allow(unused)]
|
|
pub use crate::oauth2_ee::*;
|
|
|
|
/*
|
|
* Author: Ruben Fiszel
|
|
* Copyright: Windmill Labs, Inc 2022
|
|
* This file and its contents are licensed under the AGPLv3 License.
|
|
* Please see the included NOTICE for copyright information and
|
|
* LICENSE-AGPL for a copy of the license.
|
|
*/
|
|
|
|
#[cfg(not(feature = "private"))]
|
|
use std::{collections::HashMap, fmt::Debug};
|
|
|
|
#[cfg(not(feature = "private"))]
|
|
use axum::{routing::get, Json, Router};
|
|
#[cfg(not(feature = "private"))]
|
|
use hmac::Mac;
|
|
|
|
#[cfg(all(feature = "oauth2", not(feature = "private")))]
|
|
use itertools::Itertools;
|
|
#[cfg(not(feature = "private"))]
|
|
use serde::{Deserialize, Serialize};
|
|
#[cfg(not(feature = "private"))]
|
|
use sqlx::{Postgres, Transaction};
|
|
#[cfg(all(feature = "oauth2", not(feature = "private")))]
|
|
use windmill_common::more_serde::maybe_number_opt;
|
|
#[cfg(all(feature = "oauth2", not(feature = "private")))]
|
|
use windmill_oauth::{helpers, AccessToken, RefreshToken, Scope};
|
|
|
|
#[cfg(all(feature = "oauth2", not(feature = "private")))]
|
|
use crate::OAUTH_CLIENTS;
|
|
#[cfg(not(feature = "private"))]
|
|
use windmill_common::error;
|
|
#[cfg(not(feature = "private"))]
|
|
use windmill_common::oauth2::*;
|
|
|
|
#[cfg(not(feature = "private"))]
|
|
use crate::db::DB;
|
|
#[cfg(not(feature = "private"))]
|
|
use std::str;
|
|
|
|
#[cfg(not(feature = "private"))]
|
|
pub fn global_service() -> Router {
|
|
Router::new()
|
|
.route("/list_logins", get(list_logins))
|
|
.route("/list_connects", get(list_connects))
|
|
}
|
|
|
|
#[cfg(not(feature = "private"))]
|
|
pub fn workspaced_service() -> Router {
|
|
Router::new()
|
|
}
|
|
|
|
#[cfg(not(feature = "private"))]
|
|
pub async fn workspace_connect_slack() -> Result<http::status::StatusCode, error::Error> {
|
|
Err(error::Error::BadRequest(
|
|
"Slack only available on enterprise".to_string(),
|
|
))
|
|
}
|
|
|
|
#[cfg(not(feature = "private"))]
|
|
pub async fn connect_slack_instance() -> Result<http::status::StatusCode, error::Error> {
|
|
Err(error::Error::BadRequest(
|
|
"Slack only available on enterprise".to_string(),
|
|
))
|
|
}
|
|
|
|
#[cfg(all(feature = "oauth2", not(feature = "private")))]
|
|
pub use windmill_oauth::{AllClients, BasicClientsMap, ClientWithScopes};
|
|
|
|
#[cfg(not(feature = "private"))]
|
|
pub use windmill_oauth::{OAuthClient, OAuthConfig};
|
|
|
|
#[cfg(all(feature = "oauth2", not(feature = "private")))]
|
|
pub async fn build_oauth_clients(
|
|
_base_url: &str,
|
|
_oauths_from_config: Option<HashMap<String, OAuthClient>>,
|
|
_db: &DB,
|
|
) -> anyhow::Result<AllClients> {
|
|
// Implementation is not open source
|
|
return Ok(AllClients {
|
|
logins: HashMap::default(),
|
|
connects: HashMap::default(),
|
|
slack: None,
|
|
});
|
|
}
|
|
|
|
#[cfg(all(feature = "oauth2", not(feature = "private")))]
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
pub struct TokenResponse {
|
|
access_token: AccessToken,
|
|
#[serde(deserialize_with = "maybe_number_opt")]
|
|
#[serde(default)]
|
|
expires_in: Option<u64>,
|
|
refresh_token: Option<RefreshToken>,
|
|
#[serde(deserialize_with = "helpers::deserialize_space_delimited_vec")]
|
|
#[serde(serialize_with = "helpers::serialize_space_delimited_vec")]
|
|
#[serde(default)]
|
|
scope: Option<Vec<Scope>>,
|
|
}
|
|
|
|
#[cfg(not(feature = "private"))]
|
|
#[derive(Serialize)]
|
|
struct Logins {
|
|
oauth: Vec<String>,
|
|
saml: Option<String>,
|
|
auto_login: Option<String>,
|
|
}
|
|
#[cfg(not(feature = "private"))]
|
|
async fn list_logins() -> error::JsonResult<Logins> {
|
|
// Implementation is not open source
|
|
return Ok(Json(Logins { oauth: vec![], saml: None, auto_login: None }));
|
|
}
|
|
|
|
#[allow(unused)]
|
|
#[cfg(all(feature = "oauth2", not(feature = "private")))]
|
|
async fn list_connects() -> error::JsonResult<Vec<String>> {
|
|
Ok(Json(
|
|
(&OAUTH_CLIENTS.load().connects)
|
|
.keys()
|
|
.map(|x| x.to_owned())
|
|
.collect_vec(),
|
|
))
|
|
}
|
|
|
|
#[allow(unused)]
|
|
#[cfg(not(all(feature = "oauth2", not(feature = "private"))))]
|
|
async fn list_connects() -> windmill_common::error::JsonResult<Vec<String>> {
|
|
// Implementation is not open source
|
|
return Ok(axum::Json(vec![]));
|
|
}
|
|
|
|
#[cfg(not(feature = "private"))]
|
|
pub async fn _refresh_token<'c>(
|
|
_tx: Transaction<'c, Postgres>,
|
|
_path: &str,
|
|
_w_id: &str,
|
|
_id: i32,
|
|
_db: &DB,
|
|
) -> error::Result<String> {
|
|
// Implementation is not open source
|
|
Err(error::Error::BadRequest(
|
|
"Not implemented in Windmill's Open Source repository".to_string(),
|
|
))
|
|
}
|
|
|
|
#[cfg(not(feature = "private"))]
|
|
pub async fn check_nb_of_user(db: &DB) -> error::Result<()> {
|
|
let nb_users_sso =
|
|
sqlx::query_scalar!("SELECT COUNT(*) FROM password WHERE login_type != 'password'",)
|
|
.fetch_one(db)
|
|
.await?;
|
|
if nb_users_sso.unwrap_or(0) >= 10 {
|
|
return Err(error::Error::BadRequest(
|
|
"You have reached the maximum number of oauth users accounts (10) without an enterprise license"
|
|
.to_string(),
|
|
));
|
|
}
|
|
|
|
let nb_users = sqlx::query_scalar!("SELECT COUNT(*) FROM password",)
|
|
.fetch_one(db)
|
|
.await?;
|
|
if nb_users.unwrap_or(0) >= 50 {
|
|
return Err(error::Error::BadRequest(
|
|
"You have reached the maximum number of accounts (50) without an enterprise license"
|
|
.to_string(),
|
|
));
|
|
}
|
|
return Ok(());
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
#[cfg(not(feature = "private"))]
|
|
pub struct SlackVerifier {
|
|
mac: HmacSha256,
|
|
}
|
|
#[cfg(not(feature = "private"))]
|
|
impl SlackVerifier {
|
|
pub fn new<S: AsRef<[u8]>>(secret: S) -> anyhow::Result<SlackVerifier> {
|
|
HmacSha256::new_from_slice(secret.as_ref())
|
|
.map(|mac| SlackVerifier { mac })
|
|
.map_err(|_| anyhow::anyhow!("invalid secret"))
|
|
}
|
|
|
|
pub fn verify(&self, ts: &str, body: &str, exp_sig: &str) -> anyhow::Result<()> {
|
|
let basestring = format!("v0:{}:{}", ts, body);
|
|
let mut mac = self.mac.clone();
|
|
mac.update(basestring.as_bytes());
|
|
let sig = format!("v0={}", hex::encode(mac.finalize().into_bytes()));
|
|
if sig != exp_sig {
|
|
Err(anyhow::anyhow!("signature mismatch"))?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|