split better oss and ee

This commit is contained in:
Ruben Fiszel
2024-11-02 16:20:16 +01:00
parent 98358a8ba7
commit fd4fecf945
13 changed files with 88 additions and 521 deletions
+3 -3
View File
@@ -160,7 +160,7 @@ hex = "^0"
sql-builder = "^3"
argon2 = "^0"
quick_cache = "^0"
rand = "0.8.5"
rand = "^0"
rand_core = { version = "^0", features = ["std"] }
magic-crypt = "^3"
git-version = "^0"
@@ -172,7 +172,7 @@ urlencoding = "^2"
url = "^2"
async-oauth2 = "^0"
reqwest = { version = "^0.12", features = ["json", "stream", "gzip"] }
time = "0.3.16"
time = "^0"
serde_urlencoded = "^0"
tokio-tar = "^0"
tempfile = "^3"
@@ -198,7 +198,7 @@ swc_ecma_visit = "=0.104.8"
async-recursion = "^1"
base64 = "0.21.0"
base64 = "^0"
base32 = "^0"
hmac = "0.12.1"
sha2 = "0.10.6"
+1 -1
View File
@@ -1 +1 @@
150c3176f2c192366f2e8298ef6ece43780b529e
f136a2f499e0fe7c10c54c79488851980d796eb2
+2
View File
@@ -84,12 +84,14 @@ mod stripe_ee;
mod tracing_init;
mod triggers;
mod users;
mod users_ee;
mod utils;
mod variables;
mod webhook_util;
mod websocket_triggers;
mod workers;
mod workspaces;
mod workspaces_ee;
pub const GIT_VERSION: &str =
git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
+1 -1
View File
@@ -28,13 +28,13 @@ use serde::Deserialize;
#[cfg(feature = "enterprise")]
use windmill_common::ee::{send_critical_alert, CriticalAlertKind, CriticalErrorChannel};
use windmill_common::{
email_ee::send_email,
error::{self, JsonResult, Result},
global_settings::{
AUTOMATE_USERNAME_CREATION_SETTING, EMAIL_DOMAIN_SETTING, ENV_SETTINGS,
HUB_ACCESSIBLE_URL_SETTING, HUB_BASE_URL_SETTING,
},
server::Smtp,
utils::send_email,
};
#[cfg(feature = "parquet")]
+41
View File
@@ -0,0 +1,41 @@
use std::sync::Arc;
use crate::db::ApiAuthed;
use crate::users::{EditPassword, NewUser};
use crate::{db::DB, webhook_util::WebhookShared};
use argon2::Argon2;
use http::StatusCode;
use windmill_common::error::{Error, Result};
pub async fn create_user(
_authed: ApiAuthed,
_db: DB,
_webhook: WebhookShared,
_argon2: Arc<Argon2<'_>>,
_rsmq: Option<rsmq_async::MultiplexedRsmq>,
mut _nu: NewUser,
) -> Result<(StatusCode, String)> {
Err(Error::InternalErr(
"Not implemented in Windmill's Open Source repository".to_string(),
))
}
pub async fn set_password(
_db: DB,
_argon2: Arc<Argon2<'_>>,
_authed: ApiAuthed,
_ep: EditPassword,
) -> Result<String> {
Err(Error::InternalErr(
"Not implemented in Windmill's Open Source repository".to_string(),
))
}
pub fn send_email_if_possible(_subject: &str, _content: &str, _to: &str) {
tracing::warn!(
"send_email_if_possible is not implemented in Windmill's Open Source repository"
);
}
-19
View File
@@ -155,25 +155,6 @@ pub async fn get_instance_username_or_create_pending<'c>(
}
}
pub async fn get_and_delete_pending_username_or_generate<'c>(
tx: &mut Transaction<'c, Postgres>,
email: &str,
) -> error::Result<String> {
let username = sqlx::query_scalar!("SELECT username FROM pending_user WHERE email = $1", email)
.fetch_optional(&mut **tx)
.await?;
if let Some(username) = username {
sqlx::query!("DELETE FROM pending_user WHERE email = $1", email)
.execute(&mut **tx)
.await?;
Ok(username)
} else {
let username = generate_instance_wide_unique_username(&mut *tx, email).await?;
Ok(username)
}
}
pub fn content_plain(body: Body) -> Response {
use axum::http::header;
Response::builder()
+11 -293
View File
@@ -9,14 +9,15 @@
use std::collections::HashMap;
use crate::db::ApiAuthed;
use crate::utils::{get_instance_username_or_create_pending, INVALID_USERNAME_CHARS};
use crate::users_ee::send_email_if_possible;
use crate::utils::get_instance_username_or_create_pending;
use crate::BASE_URL;
use crate::{
apps::AppWithLastVersion,
db::DB,
folders::Folder,
resources::{Resource, ResourceType},
users::{send_email_if_possible, WorkspaceInvite, VALID_USERNAME},
users::{WorkspaceInvite, VALID_USERNAME},
utils::require_super_admin,
webhook_util::WebhookShared,
};
@@ -34,7 +35,7 @@ use itertools::Itertools;
use regex::Regex;
use uuid::Uuid;
use windmill_audit::audit_ee::{audit_log, AuditAuthor, AuditAuthorable};
use windmill_audit::audit_ee::audit_log;
use windmill_audit::ActionKind;
use windmill_common::db::UserDB;
use windmill_common::s3_helpers::LargeFileStorage;
@@ -218,11 +219,12 @@ struct EditDeployTo {
deploy_to: Option<String>,
}
#[allow(dead_code)]
#[derive(Deserialize)]
struct EditAutoInvite {
operator: Option<bool>,
invite_all: Option<bool>,
auto_add: Option<bool>,
pub struct EditAutoInvite {
pub operator: Option<bool>,
pub invite_all: Option<bool>,
pub auto_add: Option<bool>,
}
#[derive(Deserialize)]
@@ -573,250 +575,21 @@ async fn edit_deploy_to() -> Result<String> {
));
}
const BANNED_DOMAINS: &str = include_str!("../banned_domains.txt");
pub const BANNED_DOMAINS: &str = include_str!("../banned_domains.txt");
async fn is_allowed_auto_domain(ApiAuthed { email, .. }: ApiAuthed) -> JsonResult<bool> {
let domain = email.split('@').last().unwrap();
return Ok(Json(!BANNED_DOMAINS.contains(domain)));
}
async fn auto_add_user(
email: &str,
w_id: &str,
operator: &bool,
tx: &mut Transaction<'_, Postgres>,
authorable: &impl AuditAuthorable,
) -> Result<String> {
let automate_username_creation = sqlx::query_scalar!(
"SELECT value FROM global_settings WHERE name = $1",
AUTOMATE_USERNAME_CREATION_SETTING,
)
.fetch_optional(&mut **tx)
.await?
.map(|v| v.as_bool())
.flatten()
.unwrap_or(false);
let username = if automate_username_creation {
get_instance_username_or_create_pending(&mut *tx, &email).await?
} else {
let mut username = email
.split('@')
.next()
.unwrap()
.to_string()
.replace(".", "");
username = INVALID_USERNAME_CHARS
.replace_all(&mut username, "")
.to_string();
if username.is_empty() {
username = "user".to_string()
}
let base_username = username.clone();
let mut username_conflict = true;
let mut i = 1;
while username_conflict {
if i > 1000 {
return Err(Error::InternalErr(format!(
"too many username conflicts for {}",
email
)));
}
if i > 1 {
username = format!("{}{}", base_username, i)
}
username_conflict = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM usr WHERE username = $1 AND workspace_id = $2)",
&username,
&w_id
)
.fetch_one(&mut **tx)
.await?
.unwrap_or(false);
i += 1;
}
username
};
sqlx::query!(
"INSERT INTO usr (workspace_id, username, email, is_admin, operator) VALUES ($1, $2, $3, false, $4) ON CONFLICT DO NOTHING",
&w_id,
&username,
&email,
&operator
)
.execute(&mut **tx)
.await?;
sqlx::query_as!(
Group,
"INSERT INTO usr_to_group (workspace_id, usr, group_) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
&w_id,
username,
"all",
)
.execute(&mut **tx)
.await?;
let audit_author = if authorable.username() == authorable.email() && authorable.email() == email
{
// if the user is auto adding themselves (e.g. by joining the instance), we use their newly created workspace username for audit logs
AuditAuthor {
username: username.clone(),
email: email.to_string(),
username_override: None,
}
} else {
AuditAuthor {
username: authorable.username().to_string(),
email: authorable.email().to_string(),
username_override: authorable.username_override().map(|x| x.to_string()),
}
};
audit_log(
&mut **tx,
&audit_author,
"users.auto_invite_add",
ActionKind::Create,
&w_id,
Some(email),
None,
)
.await?;
Ok(username)
}
async fn edit_auto_invite(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>,
ApiAuthed { is_admin, email, username, .. }: ApiAuthed,
Json(ea): Json<EditAutoInvite>,
) -> Result<String> {
require_admin(is_admin, &username)?;
// #[cfg(not(feature = "enterprise"))]
// {
// return Err(Error::BadRequest(
// "Auto-invite is only available on enterprise".to_string(),
// ));
// }
let domain = if ea.invite_all.is_some_and(|x| x) {
if *CLOUD_HOSTED {
return Err(Error::BadRequest(
"invite_all is only available locally".to_string(),
));
} else {
"*"
}
} else {
email.split('@').last().unwrap()
};
let mut tx = db.begin().await?;
let mut users_to_auto_add = Option::None;
if let (Some(operator), Some(auto_add)) = (ea.operator, ea.auto_add) {
if BANNED_DOMAINS.contains(domain) {
return Err(Error::BadRequest(format!(
"Domain {} is not allowed",
domain
)));
}
sqlx::query!(
"UPDATE workspace_settings SET auto_invite_domain = $1, auto_invite_operator = $2, auto_add = $4 WHERE workspace_id = $3",
domain,
operator,
&w_id,
auto_add,
)
.execute(&mut *tx)
.await?;
if auto_add {
users_to_auto_add = Some(sqlx::query!(
"SELECT email FROM password WHERE ($2::text = '*' OR email LIKE CONCAT('%', $2::text)) AND NOT EXISTS (
SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email
)",
&w_id,
domain
)
.fetch_all(&mut *tx).await?);
for user in users_to_auto_add.as_ref().unwrap() {
auto_add_user(&user.email, &w_id, &operator, &mut tx, &authed).await?;
send_email_if_possible(
&format!("Added to Windmill's workspace: {w_id}"),
&format!(
"You have been granted access to Windmill's workspace {w_id} by {email}.
Access the workspace at {}/?workspace={w_id}",
BASE_URL.read().await.clone()
),
&user.email,
);
}
} else {
sqlx::query!(
"INSERT INTO workspace_invite
(workspace_id, email, is_admin, operator)
SELECT $1::text, email, false, $3 FROM password WHERE ($2::text = '*' OR email LIKE CONCAT('%', $2::text)) AND NOT EXISTS (
SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email
)
ON CONFLICT DO NOTHING",
&w_id,
domain,
operator
)
.execute(&mut *tx)
.await?;
}
} else {
sqlx::query!(
"UPDATE workspace_settings SET auto_invite_domain = NULL, auto_invite_operator = NULL, auto_add = NULL WHERE workspace_id = $1",
&w_id,
)
.execute(&mut *tx)
.await?;
}
audit_log(
&mut *tx,
&authed,
"workspaces.edit_auto_invite_domain",
ActionKind::Update,
&w_id,
Some(&authed.email),
Some([("operator", &format!("{:?}", ea.operator)[..])].into()),
)
.await?;
tx.commit().await?;
if let Some(users) = users_to_auto_add {
for user in users {
handle_deployment_metadata(
&email,
&username,
&db,
&w_id,
windmill_git_sync::DeployedObject::User { email: user.email.clone() },
Some(format!("Auto-added user '{}' to workspace", &user.email)),
rsmq.clone(),
true,
)
.await?;
}
}
Ok(format!(
"Edit auto-invite for workspace {} to {}",
&w_id, domain
))
crate::workspaces_ee::edit_auto_invite(authed, db, rsmq, w_id, ea).await
}
async fn edit_webhook(
@@ -2001,61 +1774,6 @@ async fn delete_workspace(
Ok(format!("Deleted workspace {}", &w_id))
}
pub async fn invite_user_to_all_auto_invite_worspaces(
db: &DB,
email: &str,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
authorable: &impl AuditAuthorable,
) -> Result<()> {
let mut tx = db.begin().await?;
let domain = email.split('@').last().unwrap();
let workspaces = sqlx::query!(
"SELECT workspace_id, auto_invite_operator, auto_add FROM workspace_settings ws WHERE (auto_invite_domain = $1 OR auto_invite_domain = '*') AND NOT EXISTS (SELECT 1 FROM usr WHERE workspace_id = ws.workspace_id AND email = $2)",
domain,
email
)
.fetch_all(&mut *tx)
.await?;
let mut auto_added_workspace_usernames: Vec<(String, String)> = vec![];
for r in workspaces {
if r.auto_add.is_some() && r.auto_add.unwrap() {
let operator = r.auto_invite_operator.unwrap_or(false);
let username =
auto_add_user(email, &r.workspace_id, &operator, &mut tx, authorable).await?;
auto_added_workspace_usernames.push((r.workspace_id, username));
} else {
sqlx::query!(
"INSERT INTO workspace_invite
(workspace_id, email, is_admin, operator)
VALUES ($1, $2, false, $3)
ON CONFLICT DO NOTHING",
r.workspace_id,
email,
r.auto_invite_operator
)
.execute(&mut *tx)
.await?;
}
}
tx.commit().await?;
for workspace_username_tuple in auto_added_workspace_usernames {
let (w_id, username) = workspace_username_tuple;
handle_deployment_metadata(
&email,
&username,
db,
&w_id,
windmill_git_sync::DeployedObject::User { email: email.to_string() },
Some(format!("Auto-added user '{}' to workspace", email)),
rsmq.clone(),
true,
)
.await?;
}
Ok(())
}
async fn invite_user(
ApiAuthed { username, is_admin, .. }: ApiAuthed,
Extension(db): Extension<DB>,
+16
View File
@@ -0,0 +1,16 @@
use crate::{
db::{ApiAuthed, DB},
workspaces::EditAutoInvite,
};
pub async fn edit_auto_invite(
_authed: ApiAuthed,
_db: DB,
_rsmq: Option<rsmq_async::MultiplexedRsmq>,
_w_id: String,
_ea: EditAutoInvite,
) -> windmill_common::error::Result<String> {
Err(windmill_common::error::Error::InternalErr(
"Not implemented on OSS".to_string(),
))
}
-73
View File
@@ -1,73 +0,0 @@
#[cfg(feature = "enterprise")]
use crate::db::DB;
use crate::ee::LicensePlan::Community;
#[cfg(feature = "enterprise")]
use crate::error;
use serde::Deserialize;
use std::sync::Arc;
use tokio::sync::RwLock;
lazy_static::lazy_static! {
pub static ref LICENSE_KEY_VALID: Arc<RwLock<bool>> = Arc::new(RwLock::new(true));
pub static ref LICENSE_KEY_ID: Arc<RwLock<String>> = Arc::new(RwLock::new("".to_string()));
pub static ref LICENSE_KEY: Arc<RwLock<String>> = Arc::new(RwLock::new("".to_string()));
}
pub enum LicensePlan {
Community,
Pro,
Enterprise,
}
pub async fn get_license_plan() -> LicensePlan {
// Implementation is not open source
return Community;
}
#[derive(Deserialize)]
#[serde(untagged)]
pub enum CriticalErrorChannel {}
pub enum CriticalAlertKind {
#[cfg(feature = "enterprise")]
CriticalError,
#[cfg(feature = "enterprise")]
RecoveredCriticalError,
}
#[cfg(feature = "enterprise")]
pub async fn send_critical_alert(
_error_message: String,
_db: &DB,
_kind: CriticalAlertKind,
_channels: Option<Vec<CriticalErrorChannel>>,
) {
}
#[cfg(feature = "enterprise")]
pub async fn schedule_key_renewal(_http_client: &reqwest::Client, _db: &crate::db::DB) -> () {
// Implementation is not open source
}
#[cfg(feature = "enterprise")]
pub async fn renew_license_key(
_http_client: &reqwest::Client,
_db: &crate::db::DB,
_key: Option<String>,
_manual: bool,
) -> String {
// Implementation is not open source
"".to_string()
}
#[cfg(feature = "enterprise")]
pub async fn create_customer_portal_session(
_http_client: &reqwest::Client,
_key: Option<String>,
) -> error::Result<String> {
// Implementation is not open source
Ok("".to_string())
}
#[cfg(feature = "enterprise")]
pub async fn worker_groups_alerts(_db: &DB) {}
-76
View File
@@ -1,76 +0,0 @@
#[cfg(feature = "enterprise")]
use crate::db::DB;
use crate::ee::LicensePlan::Community;
#[cfg(feature = "enterprise")]
use crate::error;
use serde::Deserialize;
use std::sync::Arc;
use tokio::sync::RwLock;
lazy_static::lazy_static! {
pub static ref LICENSE_KEY_VALID: Arc<RwLock<bool>> = Arc::new(RwLock::new(true));
pub static ref LICENSE_KEY_ID: Arc<RwLock<String>> = Arc::new(RwLock::new("".to_string()));
pub static ref LICENSE_KEY: Arc<RwLock<String>> = Arc::new(RwLock::new("".to_string()));
}
pub enum LicensePlan {
Community,
Pro,
Enterprise,
}
pub async fn get_license_plan() -> LicensePlan {
// Implementation is not open source
return Community;
}
#[derive(Deserialize)]
#[serde(untagged)]
pub enum CriticalErrorChannel {
Email { email: String },
Slack { slack_channel: String },
}
pub enum CriticalAlertKind {
#[cfg(feature = "enterprise")]
CriticalError,
#[cfg(feature = "enterprise")]
RecoveredCriticalError,
}
#[cfg(feature = "enterprise")]
pub async fn send_critical_alert(
_error_message: String,
_db: &DB,
_kind: CriticalAlertKind,
_channels: Option<Vec<CriticalErrorChannel>>,
) {
}
#[cfg(feature = "enterprise")]
pub async fn schedule_key_renewal(_http_client: &reqwest::Client, _db: &crate::db::DB) -> () {
// Implementation is not open source
}
#[cfg(feature = "enterprise")]
pub async fn renew_license_key(
_http_client: &reqwest::Client,
_db: &crate::db::DB,
_key: Option<String>,
_manual: bool,
) -> String {
// Implementation is not open source
"".to_string()
}
#[cfg(feature = "enterprise")]
pub async fn create_customer_portal_session(
_http_client: &reqwest::Client,
_key: Option<String>,
) -> error::Result<String> {
// Implementation is not open source
Ok("".to_string())
}
#[cfg(feature = "enterprise")]
pub async fn worker_groups_alerts(_db: &DB) {}
+11
View File
@@ -0,0 +1,11 @@
use crate::server::Smtp;
pub async fn send_email(
_subject: &str,
_content: &str,
_to: Vec<String>,
_smtp: Smtp,
_client_timeout: Option<tokio::time::Duration>,
) -> crate::error::Result<()> {
Ok(())
}
+1
View File
@@ -22,6 +22,7 @@ pub mod auth;
pub mod bench;
pub mod db;
pub mod ee;
pub mod email_ee;
pub mod error;
pub mod external_ip;
pub mod flow_status;
+1 -55
View File
@@ -11,13 +11,11 @@ use crate::ee::LICENSE_KEY_ID;
use crate::ee::{send_critical_alert, CriticalAlertKind};
use crate::error::{to_anyhow, Error, Result};
use crate::global_settings::UNIQUE_ID_SETTING;
use crate::server::Smtp;
use crate::DB;
use anyhow::Context;
use gethostname::gethostname;
use git_version::git_version;
use mail_send::mail_builder::MessageBuilder;
use mail_send::SmtpClientBuilder;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use reqwest::Client;
use serde::{Deserialize, Serialize};
@@ -243,58 +241,6 @@ pub fn generate_lock_id(database_name: &str) -> i64 {
0x3d32ad9e * (CRC_IEEE.checksum(database_name.as_bytes()) as i64)
}
pub async fn send_email(
subject: &str,
content: &str,
to: Vec<String>,
smtp: Smtp,
client_timeout: Option<tokio::time::Duration>,
) -> Result<()> {
let mut client = SmtpClientBuilder::new(smtp.host, smtp.port)
.implicit_tls(smtp.tls_implicit.unwrap_or(false));
if std::env::var("ACCEPT_INVALID_CERTS").is_ok() {
client = client.allow_invalid_certs();
}
let client = if let (Some(username), Some(password)) = (smtp.username, smtp.password) {
if !username.is_empty() {
client.credentials((username, password))
} else {
client
}
} else {
client
};
let message = MessageBuilder::new()
.from(("Windmill", smtp.from.as_str()))
.to(to.clone())
.subject(subject)
.text_body(content);
match client_timeout {
Some(timeout) => {
tokio::time::timeout(timeout, client.connect())
.await
.map_err(to_anyhow)?
.map_err(to_anyhow)?
.send(message)
.await
.map_err(to_anyhow)?;
}
None => {
client
.connect()
.await
.map_err(to_anyhow)?
.send(message)
.await
.map_err(to_anyhow)?;
}
}
tracing::info!("Sent email to {:#?}: {subject}", to);
return Ok(());
}
pub async fn report_critical_error(error_message: String, _db: DB) -> () {
tracing::error!("CRITICAL ERROR: {error_message}");