feat: git sync users groups (#3391)

* feat: git sync users groups

* fix: sqlx build

* chore: set ee ref + hub sync script
This commit is contained in:
HugoCasa
2024-03-12 23:42:59 +01:00
committed by GitHub
parent 50f77fbcbd
commit 104aa75634
12 changed files with 312 additions and 27 deletions
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email FROM usr WHERE username = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "5b7a1d16d8109a65479ab33d411c60d14ea91d870fdff8606d7aa4ad39f0ba00"
}
@@ -5,7 +5,7 @@
"columns": [
{
"ordinal": 0,
"name": "?column?",
"name": "bool",
"type_info": "Bool"
}
],
@@ -18,8 +18,8 @@
"Left": []
},
"nullable": [
false,
true
true,
false
]
},
"hash": "b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76"
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT username FROM usr WHERE workspace_id = $1 AND email = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "username",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "da5da57ea48ddc6ab271b6c18baa5f7360008e082e2fb8d58faff4461e18c83a"
}
+1 -1
View File
@@ -1 +1 @@
e632d5f821ee229823288b30b7ef5d3e59db07fa
93bf43cfcf05ed285e05ec07267765a5c7d888a3
+11 -1
View File
@@ -9776,7 +9776,13 @@ components:
properties:
type:
type: string
enum: ["S3Storage", "AzureBlobStorage", "AzureWorkloadIdentity", "S3AwsOidc"]
enum:
[
"S3Storage",
"AzureBlobStorage",
"AzureWorkloadIdentity",
"S3AwsOidc",
]
s3_resource_path:
type: string
azure_blob_resource_path:
@@ -9866,6 +9872,8 @@ components:
- secret
- resourcetype
- schedule
- user
- group
repositories:
type: array
items:
@@ -9912,6 +9920,8 @@ components:
- secret
- resourcetype
- schedule
- user
- group
required:
- script_path
- git_repo_resource_path
+71
View File
@@ -25,6 +25,7 @@ use windmill_common::{
use serde::{Deserialize, Serialize};
use sqlx::{query_scalar, FromRow, Postgres, Transaction};
use windmill_git_sync::handle_deployment_metadata;
pub fn workspaced_service() -> Router {
Router::new()
@@ -219,6 +220,7 @@ async fn create_group(
authed: ApiAuthed,
Extension(_db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>,
Json(ng): Json<NewGroup>,
) -> Result<String> {
@@ -260,6 +262,19 @@ async fn create_group(
.await?;
tx.commit().await?;
handle_deployment_metadata(
&authed.email,
&authed.username,
&_db,
&w_id,
windmill_git_sync::DeployedObject::Group { name: ng.name.clone() },
Some(format!("Created group '{}'", &ng.name)),
rsmq,
true,
)
.await?;
Ok(format!("Created group {}", ng.name))
}
@@ -424,6 +439,7 @@ async fn delete_group(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, name)): Path<(String, String)>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
@@ -465,6 +481,19 @@ async fn delete_group(
)
.await?;
tx.commit().await?;
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
windmill_git_sync::DeployedObject::Group { name: name.clone() },
Some(format!("Deleted group '{}'", &name)),
rsmq,
true,
)
.await?;
Ok(format!("delete group at name {}", name))
}
@@ -472,6 +501,7 @@ async fn update_group(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, name)): Path<(String, String)>,
Json(eg): Json<EditGroup>,
) -> Result<String> {
@@ -501,6 +531,19 @@ async fn update_group(
)
.await?;
tx.commit().await?;
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
windmill_git_sync::DeployedObject::Group { name: name.clone() },
Some(format!("Updated group '{}'", &name)),
rsmq,
true,
)
.await?;
Ok(format!("Edited group {}", name))
}
@@ -508,6 +551,7 @@ async fn add_user(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, name)): Path<(String, String)>,
Json(Username { username: user_username }): Json<Username>,
) -> Result<String> {
@@ -538,6 +582,19 @@ async fn add_user(
)
.await?;
tx.commit().await?;
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
windmill_git_sync::DeployedObject::Group { name: name.clone() },
Some(format!("Added user to group '{}'", &name)),
rsmq,
true,
)
.await?;
Ok(format!("Added {} to group {}", user_username, name))
}
@@ -652,6 +709,7 @@ async fn remove_user(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, name)): Path<(String, String)>,
Json(Username { username: user_username }): Json<Username>,
) -> Result<String> {
@@ -685,5 +743,18 @@ async fn remove_user(
.await?;
tx.commit().await?;
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
windmill_git_sync::DeployedObject::Group { name: name.clone() },
Some(format!("Removed user from group '{}'", &name)),
rsmq,
true,
)
.await?;
Ok(format!("Removed {} to group {}", user_username, name))
}
+58 -4
View File
@@ -53,6 +53,7 @@ use windmill_common::{
users::SUPERADMIN_SECRET_EMAIL,
utils::{not_found_if_none, rd_string, require_admin, Pagination, StripPath},
};
use windmill_git_sync::handle_deployment_metadata;
pub const TTL_TOKEN_DB_H: u32 = 72;
@@ -1304,6 +1305,7 @@ async fn accept_invite(
ApiAuthed { email, .. }: ApiAuthed,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Json(nu): Json<AcceptInvite>,
) -> Result<(StatusCode, String)> {
let mut tx = db.begin().await?;
@@ -1316,7 +1318,6 @@ async fn accept_invite(
.fetch_optional(&mut *tx)
.await?;
// let mut username = nu.username;
if let Some(r) = r {
let username;
(tx, username) = add_user_to_workspace(
@@ -1340,6 +1341,18 @@ async fn accept_invite(
)
.await?;
tx.commit().await?;
handle_deployment_metadata(
&email,
&username,
&db,
&nu.workspace_id,
windmill_git_sync::DeployedObject::User { email: email.clone() },
Some(format!("User '{}' accepted invite", &email)),
rsmq,
true,
)
.await?;
webhook.send_instance_event(InstanceEvent::UserJoinedWorkspace {
email: email.clone(),
workspace: nu.workspace_id.clone(),
@@ -1504,8 +1517,9 @@ async fn get_workspace_user(
}
async fn update_workspace_user(
ApiAuthed { username, is_admin, .. }: ApiAuthed,
ApiAuthed { username, email, is_admin, .. }: ApiAuthed,
Extension(db): Extension<DB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, username_to_update)): Path<(String, String)>,
Json(eu): Json<EditWorkspaceUser>,
) -> Result<String> {
@@ -1556,7 +1570,29 @@ async fn update_workspace_user(
None,
)
.await?;
let user_email = sqlx::query_scalar!(
"SELECT email FROM usr WHERE username = $1 AND workspace_id = $2",
&username_to_update,
&w_id
)
.fetch_one(&mut *tx)
.await?;
tx.commit().await?;
handle_deployment_metadata(
&email,
&username,
&db,
&w_id,
windmill_git_sync::DeployedObject::User { email: user_email.clone() },
Some(format!("Updated user '{}'", &user_email)),
rsmq,
true,
)
.await?;
Ok(format!("user {username} updated"))
}
@@ -1652,6 +1688,7 @@ async fn create_user(
Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Extension(argon2): Extension<Arc<Argon2<'_>>>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Json(mut nu): Json<NewUser>,
) -> Result<(StatusCode, String)> {
require_super_admin(&db, &email).await?;
@@ -1726,7 +1763,7 @@ async fn create_user(
tx.commit().await?;
invite_user_to_all_auto_invite_worspaces(&db, &nu.email).await?;
invite_user_to_all_auto_invite_worspaces(&db, &nu.email, rsmq).await?;
send_email_if_possible(
"Invited to Windmill",
&format!(
@@ -1790,8 +1827,9 @@ pub async fn send_email_if_possible_intern(subject: &str, content: &str, to: &st
}
async fn delete_workspace_user(
ApiAuthed { username, is_admin, .. }: ApiAuthed,
ApiAuthed { username, email, is_admin, .. }: ApiAuthed,
Extension(db): Extension<DB>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, username_to_delete)): Path<(String, String)>,
) -> Result<String> {
let mut tx = db.begin().await?;
@@ -1835,6 +1873,22 @@ async fn delete_workspace_user(
)
.await?;
tx.commit().await?;
handle_deployment_metadata(
&email,
&username,
&db,
&w_id,
windmill_git_sync::DeployedObject::User { email: email_to_delete.clone() },
Some(format!(
"Removed user '{}' from workspace",
&email_to_delete
)),
rsmq,
true,
)
.await?;
Ok(format!("username {} deleted", username_to_delete))
}
+65 -4
View File
@@ -53,6 +53,7 @@ use windmill_common::{
utils::{paginate, rd_string, require_admin, Pagination},
variables::ExportableListableVariable,
};
use windmill_git_sync::handle_deployment_metadata;
use windmill_queue::QueueTransaction;
use crate::oauth2_ee::InstanceEvent;
@@ -634,6 +635,7 @@ async fn auto_add_user(
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>,
@@ -661,6 +663,8 @@ async fn edit_auto_invite(
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!(
@@ -680,16 +684,16 @@ async fn edit_auto_invite(
.await?;
if auto_add {
let users = sqlx::query!(
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?;
.fetch_all(&mut *tx).await?);
for user in users {
for user in users_to_auto_add.as_ref().unwrap() {
auto_add_user(&user.email, &w_id, &operator, &mut tx).await?;
}
} else {
@@ -727,6 +731,22 @@ async fn edit_auto_invite(
.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
@@ -1751,7 +1771,11 @@ async fn delete_workspace(
Ok(format!("Deleted workspace {}", &w_id))
}
pub async fn invite_user_to_all_auto_invite_worspaces(db: &DB, email: &str) -> Result<()> {
pub async fn invite_user_to_all_auto_invite_worspaces(
db: &DB,
email: &str,
rsmq: Option<rsmq_async::MultiplexedRsmq>,
) -> Result<()> {
let mut tx = db.begin().await?;
let domain = email.split('@').last().unwrap();
let workspaces = sqlx::query!(
@@ -1761,10 +1785,19 @@ pub async fn invite_user_to_all_auto_invite_worspaces(db: &DB, email: &str) -> R
)
.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);
auto_add_user(email, &r.workspace_id, &operator, &mut tx).await?;
let username = sqlx::query_scalar!(
"SELECT username FROM usr WHERE workspace_id = $1 AND email = $2",
r.workspace_id,
email
)
.fetch_one(&mut *tx)
.await?;
auto_added_workspace_usernames.push((r.workspace_id, username));
} else {
sqlx::query!(
"INSERT INTO workspace_invite
@@ -1780,6 +1813,21 @@ pub async fn invite_user_to_all_auto_invite_worspaces(db: &DB, email: &str) -> R
}
}
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(())
}
@@ -1837,6 +1885,7 @@ async fn add_user(
ApiAuthed { username, email, is_admin, .. }: ApiAuthed,
Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path(w_id): Path<String>,
Json(mut nu): Json<NewWorkspaceUser>,
) -> Result<(StatusCode, String)> {
@@ -1928,6 +1977,18 @@ async fn add_user(
tx.commit().await?;
handle_deployment_metadata(
&email,
&username,
&db,
&w_id,
windmill_git_sync::DeployedObject::User { email: nu.email.clone() },
Some(format!("Added user '{}' to workspace", &nu.email)),
rsmq,
true,
)
.await?;
send_email_if_possible(
&format!("Added to Windmill's workspace: {w_id}"),
&format!(
@@ -19,6 +19,8 @@ pub enum ObjectType {
Secret,
Schedule,
ResourceType,
User,
Group,
}
#[derive(Serialize, Deserialize, Debug)]
+22 -9
View File
@@ -25,19 +25,30 @@ pub enum DeployedObject {
Variable { path: String, parent_path: Option<String> },
Schedule { path: String },
ResourceType { path: String },
User { email: String },
Group { name: String },
}
impl DeployedObject {
pub fn get_path(&self) -> &str {
pub fn get_path(&self) -> String {
match self {
DeployedObject::Script { path, .. } => path,
DeployedObject::Flow { path, .. } => path,
DeployedObject::App { path, .. } => path,
DeployedObject::Folder { path, .. } => path,
DeployedObject::Resource { path, .. } => path,
DeployedObject::Variable { path, .. } => path,
DeployedObject::Schedule { path, .. } => path,
DeployedObject::ResourceType { path, .. } => path,
DeployedObject::Script { path, .. } => path.to_owned(),
DeployedObject::Flow { path, .. } => path.to_owned(),
DeployedObject::App { path, .. } => path.to_owned(),
DeployedObject::Folder { path, .. } => path.to_owned(),
DeployedObject::Resource { path, .. } => path.to_owned(),
DeployedObject::Variable { path, .. } => path.to_owned(),
DeployedObject::Schedule { path, .. } => path.to_owned(),
DeployedObject::ResourceType { path, .. } => path.to_owned(),
DeployedObject::User { email } => format!("users/{email}"),
DeployedObject::Group { name } => format!("groups/{name}"),
}
}
pub fn get_ignore_regex_filter(&self) -> bool {
match self {
DeployedObject::User { .. } | DeployedObject::Group { .. } => true,
_ => false,
}
}
@@ -51,6 +62,8 @@ impl DeployedObject {
DeployedObject::Variable { parent_path, .. } => parent_path.to_owned(),
DeployedObject::Schedule { .. } => None,
DeployedObject::ResourceType { .. } => None,
DeployedObject::User { .. } => None,
DeployedObject::Group { .. } => None,
}
}
}
@@ -62,6 +62,8 @@
variables: boolean
secrets: boolean
schedules: boolean
users: boolean
groups: boolean
}
type GitSyncType =
| 'script'
@@ -73,6 +75,8 @@
| 'variable'
| 'secret'
| 'schedule'
| 'user'
| 'group'
let s3FileViewer: S3FilePicker
@@ -329,6 +333,12 @@
if (typesMap.schedules == expectedValue) {
result.push('schedule')
}
if (typesMap.users == expectedValue) {
result.push('user')
}
if (typesMap.groups == expectedValue) {
result.push('group')
}
return result
}
@@ -479,7 +489,9 @@
variables: (settings.exclude_types_override?.indexOf('variable') ?? -1) >= 0,
secrets: (settings.exclude_types_override?.indexOf('secret') ?? -1) >= 0,
schedules: (settings.exclude_types_override?.indexOf('schedule') ?? -1) >= 0,
folders: (settings.exclude_types_override?.indexOf('folder') ?? -1) >= 0
folders: (settings.exclude_types_override?.indexOf('folder') ?? -1) >= 0,
users: (settings.exclude_types_override?.indexOf('user') ?? -1) >= 0,
groups: (settings.exclude_types_override?.indexOf('group') ?? -1) >= 0
}
}
}),
@@ -492,7 +504,9 @@
variables: (settings.git_sync.include_type?.indexOf('variable') ?? -1) >= 0,
secrets: (settings.git_sync.include_type?.indexOf('secret') ?? -1) >= 0,
schedules: (settings.git_sync.include_type?.indexOf('schedule') ?? -1) >= 0,
folders: (settings.git_sync.include_type?.indexOf('folder') ?? -1) >= 0
folders: (settings.git_sync.include_type?.indexOf('folder') ?? -1) >= 0,
users: (settings.git_sync.include_type?.indexOf('user') ?? -1) >= 0,
groups: (settings.git_sync.include_type?.indexOf('group') ?? -1) >= 0
}
}
} else {
@@ -508,7 +522,9 @@
resources: false,
variables: false,
secrets: false,
schedules: false
schedules: false,
users: false,
groups: false
}
}
gitSyncTestJobs = []
@@ -1248,6 +1264,16 @@
on:change={(_) => resetGitSyncRepositoryExclude('resourcetypes')}
options={{ right: 'Resource Types' }}
/>
<Toggle
bind:checked={gitSyncSettings.include_type.users}
on:change={(_) => resetGitSyncRepositoryExclude('users')}
options={{ right: 'Users' }}
/>
<Toggle
bind:checked={gitSyncSettings.include_type.groups}
on:change={(_) => resetGitSyncRepositoryExclude('groups')}
options={{ right: 'Groups' }}
/>
</div>
</div>
</div>
@@ -1419,7 +1445,7 @@
gitSyncSettings.repositories = [
...gitSyncSettings.repositories,
{
script_path: 'hub/7958/sync-script-to-git-repo-windmill',
script_path: 'hub/8701/sync-script-to-git-repo-windmill',
git_repo_resource_path: '',
use_individual_branch: false,
exclude_types_override: {
@@ -1431,7 +1457,9 @@
resources: false,
variables: false,
secrets: false,
schedules: false
schedules: false,
users: false,
groups: false
}
}
]