feat: instance usernames (#3382)

* feat: instance usernames

* fix: sqlx + ee ref

* feat: make one-off button for enabling

* chore: set ee ref

* fix: permissions and nits

* fix: admin instance username + sqlx build

* fix: revert stupid change

* fix: only create instance username when setting enabled

* chore: update to ee latest
This commit is contained in:
HugoCasa
2024-03-12 11:55:18 +01:00
committed by GitHub
parent cc05ec419d
commit cf4358a14b
77 changed files with 1643 additions and 186 deletions
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE queue SET permissioned_as = ('u/' || $1) WHERE permissioned_as = ('u/' || $2) AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "036af7b1cf6d731647fd718458944b9a9759bdb034e73f3065cde6a2f88c8dce"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE usr SET username = $1 WHERE email = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "03cb31686d34035f2dc64d4dff93265af0e44a7e2dabdf28fd9c9327b5b53118"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE app SET policy = jsonb_set(policy, ARRAY['on_behalf_of'], to_jsonb('u/' || $1)) WHERE policy->>'on_behalf_of' = ('u/' || $2) AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "03d4ec7a5feda9001e98c2e55f0788399ee41c9b128099937abb483f1d89a2c6"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE completed_job SET created_by = $1 WHERE created_by = $2 AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "0721acae4f627df4687bb43b830a47faeee5c0a152cda8d62794c14dd200fac1"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE input SET created_by = $1 WHERE created_by = $2 AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "07ce3bce5f71b13d8aaf51f7d8aef8e32ff9e3e9cf58daab1c02276a7f0bd163"
}
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO password (email, login_type, verified) VALUES ($1, 'saml', true) ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar"
]
},
"nullable": []
},
"hash": "120fa12a4c2fdecf1da58ffba0c0aff06ea62f94ff357c446a8e2311e083efdf"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE completed_job SET script_path = REGEXP_REPLACE(script_path,'u/' || $2 || '/(.*)','u/' || $1 || '/\\1') WHERE script_path LIKE ('u/' || $2 || '/%') AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "1c2254c15696d3dbc091488311676641ba3c6f1f1b5e006fc75427c9b231d323"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE resource SET extra_perms = extra_perms - ('u/' || $2) || jsonb_build_object(('u/' || $1), extra_perms->('u/' || $2)) WHERE extra_perms ? ('u/' || $2) AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "1f003a8a6a9402f5f7021b032281a9aa70a8f89777e95e0415877ffa90ecd87c"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE token SET scopes = array(select regexp_replace(unnest(scopes), 'run:([^/]+)/u/' || $2 || '/(.+)', 'run:\\1/u/' || $1 || '/\\2')) WHERE EXISTS (SELECT 1 FROM UNNEST(scopes) scope WHERE scope LIKE ('run:%/u/' || $2 || '/%')) AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "1f9e5d943e597ad9ed4e77088666b89d70db73de0b92f390a976fe3ce88dc4c5"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE draft SET value = to_json(jsonb_set(to_jsonb(value), ARRAY['path'], to_jsonb(REGEXP_REPLACE(value->>'path','u/' || $2 || '/(.*)','u/' || $1 || '/\\1')))) WHERE value->>'path' LIKE ('u/' || $2 || '/%') AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "23e100a803d89cdce24486adc6de0904a68a011157f50ec57e39afb582e48d64"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE usr_to_group SET usr = $1 WHERE usr = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "2f0de5417088df098a200a67197155e10e909bd02c91082341dd73bb1d6d62e1"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE token SET owner = ('u/' || $1) WHERE owner = ('u/' || $2) AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "32ab08ef55f665b1944001ca6ad661940d59e211f60437437b5d53a6ee6a79a4"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE schedule SET script_path = REGEXP_REPLACE(script_path,'u/' || $2 || '/(.*)','u/' || $1 || '/\\1') WHERE script_path LIKE ('u/' || $2 || '/%') AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "35e0f5488724e9bb7595748754bbdd7c5c8d643bbbfa9b39f5b3d5d4684eab3a"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM usr WHERE username = $1 and email != $2 UNION SELECT 1 FROM password WHERE username = $1 UNION SELECT 1 FROM pending_user WHERE username = $1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "3baafb31f22d4de32bd11185f96939c0e5def7bbee4a7f25ca208a667d0dc612"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE app SET extra_perms = extra_perms - ('u/' || $2) || jsonb_build_object(('u/' || $1), extra_perms->('u/' || $2)) WHERE extra_perms ? ('u/' || $2) AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "4a585c1f92628b8763930764befdf38ce2ea7df71b013bd960796cc8230797e6"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO password(email, verified, password_hash, login_type, super_admin, name, company)\n VALUES ($1, $2, $3, 'password', $4, $5, $6)",
"query": "INSERT INTO password(email, verified, password_hash, login_type, super_admin, name, company, username)\n VALUES ($1, $2, $3, 'password', $4, $5, $6, $7)",
"describe": {
"columns": [],
"parameters": {
@@ -10,10 +10,11 @@
"Varchar",
"Bool",
"Varchar",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "11b1586acdfc180c5a077861ee1f7201fcbcec9d0ebada464f9d952c9c3e400d"
"hash": "54c6be1469a80d88fd6d5c30772bb6698610189bac63c2caee3f4d52b0851f31"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE favorite SET usr = $1 WHERE usr = $2 AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "59f6de54d604c7f724ae886928b2f8bf71a5c80991207d2576fb965e5ac60f15"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE completed_job SET permissioned_as = ('u/' || $1) WHERE permissioned_as = ('u/' || $2) AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "5d79c4817696d0ba0d2062eef27ff3856fc4a732adf5796e9b06c826406584dc"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE queue SET schedule_path = REGEXP_REPLACE(schedule_path,'u/' || $2 || '/(.*)','u/' || $1 || '/\\1') WHERE schedule_path LIKE ('u/' || $2 || '/%') AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "6239e15d2389e24e290d86bb96e3ade1656cd403000a34a19a942993b60ff612"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE variable SET extra_perms = extra_perms - ('u/' || $2) || jsonb_build_object(('u/' || $1), extra_perms->('u/' || $2)) WHERE extra_perms ? ('u/' || $2) AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "675ca010b31b3504403b56c4a8e523b6748cf8b289a8c058688bb7b3a143deaf"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE queue SET script_path = REGEXP_REPLACE(script_path,'u/' || $2 || '/(.*)','u/' || $1 || '/\\1') WHERE script_path LIKE ('u/' || $2 || '/%') AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "6818cae88492f83baf55f54bf5dd5397e04dbe771445818f88903ee5677b3631"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE flow SET extra_perms = extra_perms - ('u/' || $2) || jsonb_build_object(('u/' || $1), extra_perms->('u/' || $2)) WHERE extra_perms ? ('u/' || $2) AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "70bad6ac45db6d03c59aa375f915ec55d1c07998b1bd0268725d34f7f0b98103"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE flow SET edited_by = $1 WHERE edited_by = $2 AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "729bf764b10dc821594bbffbc157c061ec6eb09b3cc22c672e0e17da455ac7ef"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE app SET path = REGEXP_REPLACE(path,'u/' || $2 || '/(.*)','u/' || $1 || '/\\1') WHERE path LIKE ('u/' || $2 || '/%') AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "74bee966d2b5e4c1f9b1739bfd6ed828571aa2395d602d06f6ac755c1001c7c9"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE capture SET created_by = $1 WHERE created_by = $2 AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "793870919e0b61b12bcbb30439201acf664cbdc3d8a45fda7ab12b5898b819f1"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE script SET path = REGEXP_REPLACE(path,'u/' || $2 || '/(.*)','u/' || $1 || '/\\1') WHERE path LIKE ('u/' || $2 || '/%') AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "7e950917ef1d80026cae863f0744dbbe229e41b083d2a56940d8032d5dfe4189"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email, login_type::TEXT, super_admin, verified, name, company FROM password WHERE email = $1",
"query": "SELECT email, login_type::TEXT, super_admin, verified, name, company, username FROM password WHERE email = $1",
"describe": {
"columns": [
{
@@ -32,6 +32,11 @@
"ordinal": 5,
"name": "company",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "username",
"type_info": "Varchar"
}
],
"parameters": {
@@ -45,8 +50,9 @@
false,
false,
true,
true,
true
]
},
"hash": "90719f6230467b08e5f2cc89271bcf5e4a6cec39e9d9b42ef3b90f09b3135b83"
"hash": "8813665f8adfcab0daefbac2cc6b50e427dfd1c12d12895451affd307dc59c37"
}
@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT workspace_id, username FROM usr WHERE email = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "username",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "8a8f8c4a9ac35f44e5640ba38b9ef523d890b1687dc40a5b98367285f904e137"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE schedule SET edited_by = $1 WHERE edited_by = $2 AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "979d80eed9500fc0b76c5707ef07db43de1e6d5dee4a7de2fa99fad3292f0d4c"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM pending_user WHERE email = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "9969f99d81b2fa581a1da8f7cc3722f60cbd41814f8a17006c82f48888f05bb6"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE schedule SET path = REGEXP_REPLACE(path,'u/' || $2 || '/(.*)','u/' || $1 || '/\\1') WHERE path LIKE ('u/' || $2 || '/%') AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "9a3f8ca29c86fe31f8cc339897b3b49c1d3aa7b8a95be8a9101bd232ecef843a"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE flow SET path = REGEXP_REPLACE(path,'u/' || $2 || '/(.*)','u/' || $1 || '/\\1') WHERE path LIKE ('u/' || $2 || '/%') AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "a459d973c6392af6af1af8b17bc735128fa06ee80e867e2421e70df7a722081a"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE folder SET owners = ARRAY_REPLACE(owners, 'u/' || $2, 'u/' || $1) WHERE ('u/' || $2) = ANY(owners) AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "a474d655786f0b70e659da8c88e4f934c7d27234491e850b4406f4d5c8da3293"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO password (email, login_type, verified, username) VALUES ($1, 'saml', true, $2) ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "a59aac0bc593d99aedd14fd9606f11191590a12ea98a671e172b867306959884"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE resource SET path = REGEXP_REPLACE(path,'u/' || $2 || '/(.*)','u/' || $1 || '/\\1') WHERE path LIKE ('u/' || $2 || '/%') AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "a65cb39ac640a86c90d1648d1a9e93fa5a7c1be385454153f15625bd58c3cc3a"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS (SELECT 1 FROM password WHERE email = $1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "a670b88ea83d69997f1b70873b927bfe0790d5e1c347e5b15835c8996c039cd1"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE queue SET created_by = $1 WHERE created_by = $2 AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "a6feede7d9c3060b1e4c94bb4fa700d38a0d1b9d25429021c08fc94d57f952d2"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE variable SET path = REGEXP_REPLACE(path,'u/' || $2 || '/(.*)','u/' || $1 || '/\\1') WHERE path LIKE ('u/' || $2 || '/%') AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "abb962124b848ae07e517994ca9ad10e28ed47c5c9cdecf449a3758ae65cb62f"
}
@@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "SELECT p.email as \"email!\", u.username as \"username?\" FROM password p LEFT JOIN usr u ON p.email = u.email WHERE p.username IS NULL AND (SELECT COUNT(DISTINCT username) FROM usr WHERE email = p.email) <= 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email!",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "username?",
"type_info": "Varchar"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
true
]
},
"hash": "b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE draft SET path = REGEXP_REPLACE(path,'u/' || $2 || '/(.*)','u/' || $1 || '/\\1') WHERE path LIKE ('u/' || $2 || '/%') AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "b5fbceb4fde1a4c00d8d36be6f2fb612b0aec2baa6ed76c09bd11ae462642221"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE password SET username = $1 WHERE email = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "b705a3692c98081501bff467daf347e6cb6f6eff578882ce7e5e87db05b4a1e0"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE queue SET canceled_by = $1 WHERE canceled_by = $2 AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "bf282f7ed77ed09bb44b5e253725a15acefac87bedb2725827938c54a3c0e8f8"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email, login_type::text, verified, super_admin, name, company from password ORDER BY super_admin DESC, email LIMIT $1 OFFSET $2",
"query": "SELECT email, login_type::text, verified, super_admin, name, company, username from password ORDER BY super_admin DESC, email LIMIT $1 OFFSET $2",
"describe": {
"columns": [
{
@@ -32,6 +32,11 @@
"ordinal": 5,
"name": "company",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "username",
"type_info": "Varchar"
}
],
"parameters": {
@@ -46,8 +51,9 @@
false,
false,
true,
true,
true
]
},
"hash": "5f2cbdfe6eaf6cfaee1ec830678a5fdcf1024c8fab2b4a40d93dd5ffa4e7ba5c"
"hash": "c2060e8cacef6c3b5ce51ed203a2dbafc18d66f2924d1fe518c6728997647db2"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT username FROM password WHERE email = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "username",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true
]
},
"hash": "c58c35f74739ae139848099ef379aa721d37f97d3aab19d5ee798a650b757af9"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE folder SET extra_perms = extra_perms - ('u/' || $2) || jsonb_build_object(('u/' || $1), extra_perms->('u/' || $2)) WHERE extra_perms ? ('u/' || $2) AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "cc9e2460b8fded240eff64cf3cf348f9bd36a86233f3b0a3e73f51698ed353dd"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE script SET created_by = $1 WHERE created_by = $2 AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "d48f06a5ddf41fc11e31c6f53cfb9b9e18256877087a1601bd6f51f826575f26"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE schedule SET extra_perms = extra_perms - ('u/' || $2) || jsonb_build_object(('u/' || $1), extra_perms->('u/' || $2)) WHERE extra_perms ? ('u/' || $2) AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "d6b7d48c4ebf1161f2b781829b57957bbe7aaaf734c63c2a20c2ba600b423b49"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT username FROM pending_user WHERE email = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "username",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "d6c0f1fe3a6dd0b9ccb9d4765a73dc3e7cf71f1ed15d155009aa61657040f4f6"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE script SET extra_perms = extra_perms - ('u/' || $2) || jsonb_build_object(('u/' || $1), extra_perms->('u/' || $2)) WHERE extra_perms ? ('u/' || $2) AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "e38dd0f093636efa2ec1cda4618fc104f7b8ea88130692ce9c1390bda289627a"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO usr (workspace_id, username, email, is_admin, operator) VALUES ($1, $2, $3, false, $4)",
"query": "INSERT INTO usr (workspace_id, username, email, is_admin, operator) VALUES ($1, $2, $3, false, $4) ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
@@ -13,5 +13,5 @@
},
"nullable": []
},
"hash": "a92f6de42016f24f9b8b4c0d997dc557e796c004ff318aede92481df044c004a"
"hash": "e822d186203fe809b764007ad7c02870a3b7d93ae43b40ac2cd3181dffab0837"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE completed_job SET canceled_by = $1 WHERE canceled_by = $2 AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "e9d9edc030061c5ccf2fb4294acbbae160e97faede6934d90f4882d806c14813"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO pending_user (email, username) VALUES ($1, $2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "f2d85395311fd9b3293fc4d23644ab486d6f0bf633c2cc38ace9c1f1cd12ea80"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE app_version SET created_by = $1 WHERE created_by = $2 AND EXISTS (SELECT 1 FROM app WHERE workspace_id = $3 AND app.id = app_version.app_id)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "f362c8bb2e214f218115a9c2808d01427d43581ad11cc492f59363aaea730788"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE raw_app SET extra_perms = extra_perms - ('u/' || $2) || jsonb_build_object(('u/' || $1), extra_perms->('u/' || $2)) WHERE extra_perms ? ('u/' || $2) AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "f7be09062288e645544e3ca3912a67ab6418985b299b118fbde24e29a9bc1871"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE group_ SET extra_perms = extra_perms - ('u/' || $2) || jsonb_build_object(('u/' || $1), extra_perms->('u/' || $2)) WHERE extra_perms ? ('u/' || $2) AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "fad42991fb062a34d7b5952ec8dd169e4e6ae16ab34ccce0950805122c27a2b4"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE completed_job SET schedule_path = REGEXP_REPLACE(schedule_path,'u/' || $2 || '/(.*)','u/' || $1 || '/\\1') WHERE schedule_path LIKE ('u/' || $2 || '/%') AND workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "fe5e941310ffdbbe7cc5f3e2beb213999ff64bbe4f8e2532a7aa48184cb008e7"
}
+1 -1
View File
@@ -1 +1 @@
11edcb35cb919903b6d6766502c06559788e467d
e632d5f821ee229823288b30b7ef5d3e59db07fa
@@ -0,0 +1 @@
-- Add down migration script here
@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TABLE password ADD COLUMN username VARCHAR(50)
@@ -0,0 +1 @@
-- Add down migration script here
@@ -0,0 +1,6 @@
-- Add up migration script here
create table pending_user (
email varchar(255) not null primary key,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
username varchar(50) not null
);
@@ -0,0 +1 @@
-- Add down migration script here
@@ -0,0 +1,5 @@
INSERT INTO
global_settings (name, value)
SELECT 'automate_username_creation', 'true'
WHERE NOT EXISTS (SELECT 1 FROM workspace WHERE id != 'admins')
ON CONFLICT DO NOTHING;
@@ -0,0 +1 @@
-- Add down migration script here
@@ -0,0 +1,2 @@
-- Add up migration script here
UPDATE password SET username = 'admin' WHERE email = 'admin@windmill.dev';
+73 -7
View File
@@ -340,6 +340,76 @@ paths:
schema:
type: string
/users/username_info/{email}:
get:
summary: global username info (require super admin)
operationId: globalUsernameInfo
tags:
- user
parameters:
- name: email
in: path
required: true
schema:
type: string
responses:
"200":
description: user renamed
content:
application/json:
schema:
type: object
properties:
username:
type: string
workspace_usernames:
type: array
items:
type: object
properties:
workspace_id:
type: string
username:
type: string
required:
- workspace_id
- username
required:
- username
- workspace_usernames
/users/rename/{email}:
post:
summary: global rename user (require super admin)
operationId: globalUserRename
tags:
- user
parameters:
- name: email
in: path
required: true
schema:
type: string
requestBody:
description: new username
required: true
content:
application/json:
schema:
type: object
properties:
new_username:
type: string
required:
- new_username
responses:
"200":
description: user renamed
content:
text/plain:
schema:
type: string
/users/delete/{email}:
delete:
summary: global delete user (require super admin)
@@ -880,7 +950,6 @@ paths:
type: string
required:
- workspace_id
- username
responses:
"200":
description: status
@@ -977,7 +1046,6 @@ paths:
- email
- is_admin
- operator
- username
responses:
"200":
description: status
@@ -1750,7 +1818,6 @@ paths:
items:
$ref: "#/components/schemas/UserUsage"
/w/{workspace}/users/list_usernames:
get:
summary: list usernames
@@ -4840,7 +4907,6 @@ paths:
type: string
format: uuid
/w/{workspace}/jobs/workflow_as_code/{job_id}/{entrypoint}:
post:
summary: run code-workflow task
@@ -6509,7 +6575,7 @@ paths:
type: string
required:
- owner
responses:
"200":
description: owner added to folder
@@ -8924,7 +8990,6 @@ components:
name:
type: string
CreateResource:
type: object
properties:
@@ -9325,7 +9390,6 @@ components:
required:
- id
- name
- username
Workspace:
type: object
@@ -9376,6 +9440,8 @@ components:
type: string
company:
type: string
username:
type: string
required:
- email
+23 -3
View File
@@ -11,7 +11,7 @@ use std::time::Duration;
use crate::{
db::{ApiAuthed, DB},
ee::validate_license_key,
utils::require_super_admin,
utils::{generate_instance_username_for_all_users, require_super_admin},
HTTP_CLIENT,
};
@@ -26,7 +26,7 @@ use serde::Deserialize;
use tokio::time::timeout;
use windmill_common::{
error::{self, to_anyhow, JsonResult, Result},
global_settings::ENV_SETTINGS,
global_settings::{AUTOMATE_USERNAME_CREATION_SETTING, ENV_SETTINGS},
server::Smtp,
};
@@ -144,6 +144,22 @@ pub async fn set_global_setting_internal(
key: String,
value: serde_json::Value,
) -> error::Result<()> {
match key.as_str() {
AUTOMATE_USERNAME_CREATION_SETTING => {
if value.clone().as_bool().unwrap_or(false) {
generate_instance_username_for_all_users(db)
.await
.map_err(|err| {
error::Error::InternalErr(format!(
"Failed to generate instance wide usernames: {}",
err
))
})?;
}
}
_ => {}
}
match value {
serde_json::Value::Null => {
delete_global_setting(db, &key).await?;
@@ -162,6 +178,7 @@ pub async fn set_global_setting_internal(
tracing::info!("Set global setting {} to {}", key, v);
}
};
Ok(())
}
@@ -170,7 +187,10 @@ pub async fn get_global_setting(
authed: ApiAuthed,
Path(key): Path<String>,
) -> JsonResult<serde_json::Value> {
if !key.starts_with("default_error_handler_") && !key.starts_with("default_recovery_handler_") {
if !key.starts_with("default_error_handler_")
&& !key.starts_with("default_recovery_handler_")
&& key != AUTOMATE_USERNAME_CREATION_SETTING
{
require_super_admin(&db, &authed.email).await?;
}
let value = sqlx::query!("SELECT value FROM global_settings WHERE name = $1", key)
+143
View File
@@ -6,6 +6,8 @@
* LICENSE-AGPL for a copy of the license.
*/
use regex::Regex;
use sqlx::{Postgres, Transaction};
use windmill_common::{
error::{self, Error},
users::{SUPERADMIN_NOTIFICATION_EMAIL, SUPERADMIN_SECRET_EMAIL},
@@ -30,3 +32,144 @@ pub async fn require_super_admin(db: &DB, email: &str) -> error::Result<()> {
Ok(())
}
}
lazy_static::lazy_static! {
pub static ref INVALID_USERNAME_CHARS: Regex = Regex::new(r"[^A-Za-z0-9_]").unwrap();
}
pub async fn generate_instance_wide_unique_username<'c>(
tx: &mut Transaction<'c, Postgres>,
email: &str,
) -> error::Result<String> {
let mut username = email.split('@').next().unwrap().to_string();
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 email != $2 UNION SELECT 1 FROM password WHERE username = $1 UNION SELECT 1 FROM pending_user WHERE username = $1)",
&username,
&email
)
.fetch_one(&mut **tx)
.await?
.unwrap_or(false);
i += 1;
}
Ok(username)
}
pub async fn generate_instance_username_for_all_users(db: &DB) -> error::Result<()> {
let mut tx = db.begin().await?;
// get users that have a no instance username and either 1 or 0 workspace usernames
let users = sqlx::query!(r#"SELECT p.email as "email!", u.username as "username?" FROM password p LEFT JOIN usr u ON p.email = u.email WHERE p.username IS NULL AND (SELECT COUNT(DISTINCT username) FROM usr WHERE email = p.email) <= 1"#)
.fetch_all(&mut *tx)
.await?;
for user in users {
let username = if let Some(username) = user.username {
// if has workspace username, check that username is unique
let username_conflict = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM usr WHERE username = $1 and email != $2 UNION SELECT 1 FROM password WHERE username = $1 UNION SELECT 1 FROM pending_user WHERE username = $1)",
&username,
&user.email
).fetch_one(&mut *tx).await?.unwrap_or(false);
if !username_conflict {
username
} else {
generate_instance_wide_unique_username(&mut tx, &user.email).await?
}
} else {
generate_instance_wide_unique_username(&mut tx, &user.email).await?
};
sqlx::query!(
"UPDATE password SET username = $1 WHERE email = $2",
&username,
&user.email
)
.execute(&mut *tx)
.await?;
}
tx.commit().await?;
Ok(())
}
pub async fn get_instance_username_or_create_pending<'c>(
tx: &mut Transaction<'c, Postgres>,
email: &str,
) -> error::Result<String> {
let user = sqlx::query_scalar!("SELECT username FROM password WHERE email = $1", email)
.fetch_optional(&mut **tx)
.await?;
if let Some(opt_username) = user {
if let Some(username) = opt_username {
Ok(username)
} else {
Err(Error::BadRequest(format!("No instance-wide username found for {email}. The user has different usernames for different workspaces. Ask the instance administrator to solve the conflict in the instance settings.")))
}
} else {
let pending_username =
sqlx::query_scalar!("SELECT username FROM pending_user WHERE email = $1", email)
.fetch_optional(&mut **tx)
.await?;
if let Some(username) = pending_username {
Ok(username)
} else {
let username = generate_instance_wide_unique_username(&mut *tx, email).await?;
sqlx::query!(
"INSERT INTO pending_user (email, username) VALUES ($1, $2)",
email,
username
)
.execute(&mut **tx)
.await
.map_err(|e| Error::InternalErr(format!("creating pending user: {e}")))?;
Ok(username)
}
}
}
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)
}
}
+112 -49
View File
@@ -9,6 +9,7 @@
use std::collections::HashMap;
use crate::db::ApiAuthed;
use crate::utils::{get_instance_username_or_create_pending, INVALID_USERNAME_CHARS};
use crate::BASE_URL;
use crate::{
apps::AppWithLastVersion,
@@ -46,6 +47,7 @@ use windmill_common::workspaces::WorkspaceGitSyncSettings;
use windmill_common::{
error::{to_anyhow, Error, JsonResult, Result},
flows::Flow,
global_settings::AUTOMATE_USERNAME_CREATION_SETTING,
oauth2::WORKSPACE_SLACK_BOT_TOKEN_PATH,
scripts::{Schema, Script, ScriptLang},
utils::{paginate, rd_string, require_admin, Pagination},
@@ -229,7 +231,7 @@ struct EditLargeFileStorageConfig {
struct CreateWorkspace {
id: String,
name: String,
username: String,
username: Option<String>,
}
#[derive(Deserialize)]
@@ -272,7 +274,7 @@ pub struct NewWorkspaceInvite {
#[derive(Deserialize)]
pub struct NewWorkspaceUser {
pub email: String,
pub username: String,
pub username: Option<String>,
pub is_admin: bool,
pub operator: bool,
}
@@ -538,47 +540,62 @@ async fn auto_add_user(
operator: &bool,
tx: &mut Transaction<'_, Postgres>,
) -> Result<()> {
let mut username = email
.split('@')
.next()
.unwrap()
.to_string()
.replace(".", "");
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);
username = INVALID_USERNAME_CHARS
.replace_all(&mut username, "")
.to_string();
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(".", "");
if username.is_empty() {
username = "user".to_string()
}
username = INVALID_USERNAME_CHARS
.replace_all(&mut username, "")
.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 username.is_empty() {
username = "user".to_string()
}
if i > 1 {
username = format!("{}{}", base_username, i)
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_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)",
"INSERT INTO usr (workspace_id, username, email, is_admin, operator) VALUES ($1, $2, $3, false, $4) ON CONFLICT DO NOTHING",
&w_id,
&username,
&email,
@@ -1249,8 +1266,6 @@ lazy_static::lazy_static! {
}
};
pub static ref INVALID_USERNAME_CHARS: Regex = Regex::new(r"[^A-Za-z0-9_]").unwrap();
}
async fn create_workspace_require_superadmin() -> String {
@@ -1330,13 +1345,35 @@ async fn create_workspace(
// .execute(&mut tx)
// .await?;
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 {
if nw.username.is_some() && nw.username.unwrap().len() > 0 {
return Err(Error::BadRequest(
"username is not allowed when username creation is automated".to_string(),
));
}
get_instance_username_or_create_pending(&mut tx, &authed.email).await?
} else {
nw.username
.ok_or(Error::BadRequest("username is required".to_string()))?
};
sqlx::query!(
"INSERT INTO usr
(workspace_id, email, username, is_admin)
VALUES ($1, $2, $3, true)",
nw.id,
authed.email,
nw.username,
username,
)
.execute(&mut *tx)
.await?;
@@ -1353,7 +1390,7 @@ async fn create_workspace(
"INSERT INTO usr_to_group
VALUES ($1, 'all', $2)",
nw.id,
nw.username
username
)
.execute(&mut *tx)
.await?;
@@ -1741,16 +1778,11 @@ async fn add_user(
nu.email = nu.email.to_lowercase();
let mut tx = db.begin().await?;
if !VALID_USERNAME.is_match(&nu.username) {
return Err(windmill_common::error::Error::BadRequest(format!(
"Usermame can only contain alphanumeric characters and underscores and must start with a letter"
)));
}
let already_exists_email = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM usr WHERE workspace_id = $1 AND email = $2)",
&w_id,
username,
nu.email,
)
.fetch_one(&mut *tx)
.await?
@@ -1763,13 +1795,44 @@ async fn add_user(
)));
}
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 {
if nu.username.is_some() && nu.username.unwrap().len() > 0 {
return Err(Error::BadRequest(
"username is not allowed when username creation is automated".to_string(),
));
}
get_instance_username_or_create_pending(&mut tx, &nu.email).await?
} else {
let username = nu
.username
.ok_or(Error::BadRequest("username is required".to_string()))?;
if !VALID_USERNAME.is_match(&username) {
return Err(windmill_common::error::Error::BadRequest(format!(
"Usermame can only contain alphanumeric characters and underscores and must start with a letter"
)));
}
username
};
sqlx::query!(
"INSERT INTO usr
(workspace_id, email, username, is_admin, operator)
VALUES ($1, $2, $3, $4, $5)",
&w_id,
nu.email,
nu.username,
username,
nu.is_admin,
nu.operator
)
@@ -1780,7 +1843,7 @@ async fn add_user(
Group,
"INSERT INTO usr_to_group (workspace_id, usr, group_) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
&w_id,
nu.username,
username,
"all",
)
.execute(&mut *tx)
@@ -1788,7 +1851,7 @@ async fn add_user(
audit_log(
&mut *tx,
&nu.username,
&username,
"users.add_to_workspace",
ActionKind::Create,
&w_id,
@@ -22,6 +22,8 @@ pub const EXPOSE_DEBUG_METRICS_SETTING: &str = "expose_debug_metrics";
pub const KEEP_JOB_DIR_SETTING: &str = "keep_job_dir";
pub const REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING: &str = "require_preexisting_user_for_oauth";
pub const AUTOMATE_USERNAME_CREATION_SETTING: &str = "automate_username_creation";
pub const ENV_SETTINGS: [&str; 55] = [
"DISABLE_NSJAIL",
"MODE",
+7 -2
View File
@@ -12,6 +12,7 @@ import {
Table,
UserService,
WorkspaceService,
SettingService,
} from "./deps.ts";
import { requireLogin } from "./context.ts";
@@ -240,11 +241,15 @@ export async function add(
`Workspace at id ${workspaceId} on ${remote} does not exist. Creating...`
)
);
const automateUsernameCreation: boolean =
(await SettingService.getGlobal({
key: "automate_username_creation",
})) ?? false;
await WorkspaceService.createWorkspace({
requestBody: {
id: workspaceId,
name: opts.createWorkspaceName ?? workspaceName,
username: opts.createUsername,
username: automateUsernameCreation ? undefined : opts.createUsername,
},
});
}
@@ -360,7 +365,7 @@ const command = new Command()
)
.option(
"--create-username <username:string>",
"Specify your own username in the newly created workspace. Ignored if --create is not specified or the workspace already exists.",
"Specify your own username in the newly created workspace. Ignored if --create is not specified, the workspace already exists or automatic username creation is enabled on the instance.",
{
default: "admin",
}
+16 -6
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { globalEmailInvite, superadmin, workspaceStore } from '$lib/stores'
import { UserService, WorkspaceService } from '$lib/gen'
import { SettingService, UserService, WorkspaceService } from '$lib/gen'
import { Button, Popup } from './common'
import { sendUserToast } from '$lib/toast'
import { isCloudHosted } from '$lib/cloud'
@@ -23,12 +23,19 @@
}
}
let automateUsernameCreation = false
async function getAutomateUsernameCreationSetting() {
automateUsernameCreation =
(await SettingService.getGlobal({ key: 'automate_username_creation' })) ?? false
}
getAutomateUsernameCreationSetting()
async function addUser() {
await WorkspaceService.addUser({
workspace: $workspaceStore!,
requestBody: {
email,
username,
username: automateUsernameCreation ? undefined : username,
is_admin: selected == 'admin',
operator: selected == 'operator'
}
@@ -37,6 +44,7 @@
if (!(await UserService.existsEmail({ email }))) {
let isSuperadmin = $superadmin
if (!isCloudHosted()) {
const emailCopy = email
sendUserToast(
`User ${email} is not registered yet on the instance. ${
!isSuperadmin
@@ -49,7 +57,7 @@
{
label: 'Add user to the instance',
callback: () => {
$globalEmailInvite = email
$globalEmailInvite = emailCopy
goto('#superadmin-settings')
}
}
@@ -79,8 +87,10 @@
<span class="text-xs mb-1 leading-6">Email</span>
<input type="email mb-1" on:keyup={handleKeyUp} placeholder="email" bind:value={email} />
<span class="text-xs mb-1 pt-2 leading-6">Username</span>
<input type="text" on:keyup={handleKeyUp} placeholder="username" bind:value={username} />
{#if !automateUsernameCreation}
<span class="text-xs mb-1 pt-2 leading-6">Username</span>
<input type="text" on:keyup={handleKeyUp} placeholder="username" bind:value={username} />
{/if}
<span class="text-xs mb-1 pt-2 leading-6">Role</span>
<ToggleButtonGroup bind:selected class="mb-4">
@@ -117,7 +127,7 @@
username = undefined
})
}}
disabled={email === undefined || username === undefined}
disabled={email === undefined || (!automateUsernameCreation && username === undefined)}
>
Add
</Button>
@@ -0,0 +1,146 @@
<script lang="ts">
import { SettingService, UserService } from '$lib/gen'
import { Button, Popup } from './common'
import { sendUserToast } from '$lib/toast'
import Alert from './common/alert/Alert.svelte'
import { autoPlacement } from '@floating-ui/core'
import { createEventDispatcher } from 'svelte'
export let email: string
export let username: string
export let isConflict = false
let usernameInfo:
| {
username: string
workspace_usernames: {
workspace_id: string
username: string
}[]
}
| undefined = undefined
function handleKeyUp(event: KeyboardEvent) {
const key = event.key
if (key === 'Enter') {
event.preventDefault()
renameUser()
}
}
async function getUsernameInfo() {
usernameInfo = await UserService.globalUsernameInfo({
email
})
if (isConflict) {
username = usernameInfo.username
}
}
getUsernameInfo()
const dispatch = createEventDispatcher()
async function renameUser() {
const automateUsernameCreation =
(await SettingService.getGlobal({ key: 'automate_username_creation' })) ?? false
if (!automateUsernameCreation) {
sendUserToast(
'Modifying the username is only possible when the creation of usernames is automated and defined at instance level..'
)
return
}
await UserService.globalUserRename({
email,
requestBody: {
new_username: username
}
})
sendUserToast(`Rename user ${email} to ${username}`)
dispatch('renamed')
}
</script>
<Popup
floatingConfig={{
middleware: [
autoPlacement({
allowedPlacements: ['bottom-end', 'top-end']
})
]
}}
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
let:close
>
<svelte:fragment slot="button">
<Button color={isConflict ? 'red' : 'light'} size="xs" spacingSize="xs2" nonCaptureEvent={true}
>{isConflict ? 'Fix username conflict' : 'Change username'}</Button
>
</svelte:fragment>
<div class="flex flex-col max-w-2xl p-2">
<span class="text-sm mb-2 leading-6 font-semibold"
>{isConflict ? 'Fix username conflict' : 'Change username'}</span
>
<span class="text-xs mb-1 leading-6"
>{isConflict ? 'Auto-generated instance username' : 'New username'}</span
>
<input
type="text"
class="mb-4"
on:keyup={handleKeyUp}
bind:value={username}
disabled={isConflict}
/>
{#if isConflict}
<Alert title="Username conflict" class="mb-4">
Users are required to have an instance-wide username that is shared across all workspaces.
However, this user has different usernames in different workspaces.
{#if usernameInfo?.workspace_usernames && usernameInfo.workspace_usernames.filter((w) => w.username !== username).length > 0}
<br />
<br />
Workspaces requiring username modification: {usernameInfo.workspace_usernames
.filter((w) => w.username !== username)
.map((wu) => `${wu.workspace_id} (${wu.username})`)
.join(', ')}
{/if}
</Alert>
{/if}
{#if !isConflict && usernameInfo?.workspace_usernames && usernameInfo.workspace_usernames.filter((w) => w.username !== username).length > 0}
<Alert title="Concerned workspaces" class="mb-4">
{usernameInfo.workspace_usernames
.filter((w) => w.username !== username)
.map((wu) => `${wu.workspace_id}`)
.join(', ')}
</Alert>
{/if}
<Alert type="warning" title="Manual action required" class="mb-4">
This operation does not handle references in scripts, workflows and applications to scripts in
the workspace, and references in resources to variables. You will have to handle those
manually.
<br />
</Alert>
<Button
variant="contained"
color="blue"
size="xs"
on:click={() => {
renameUser().then(() => {
close(null)
})
}}
disabled={email === undefined || !username}
>
Confirm username change
</Button>
</div>
</Popup>
@@ -1,5 +1,5 @@
<script lang="ts">
import { UserService, GlobalUserInfo } from '$lib/gen'
import { UserService, GlobalUserInfo, SettingService } from '$lib/gen'
import TableCustom from '$lib/components/TableCustom.svelte'
import InviteGlobalUser from '$lib/components/InviteGlobalUser.svelte'
import { Button, Drawer, DrawerContent, Tab, Tabs } from '$lib/components/common'
@@ -18,7 +18,9 @@
import { ExternalLink } from 'lucide-svelte'
import { settingsKeys } from './instanceSettings'
import ConfirmationModal from './common/confirmationModal/ConfirmationModal.svelte'
import ChangeInstanceUsername from './ChangeInstanceUsername.svelte'
import Tooltip from './Tooltip.svelte'
import { isCloudHosted } from '$lib/cloud'
let drawer: Drawer
let filter = ''
@@ -52,6 +54,23 @@
let nbDisplayed = 50
let instanceSettings
let automateUsernameCreation = false
async function getAutomateUsernameCreationSetting() {
automateUsernameCreation =
(await SettingService.getGlobal({ key: 'automate_username_creation' })) ?? false
}
getAutomateUsernameCreationSetting()
let automateUsernameModalOpen = false
async function enableAutomateUsernameCreationSetting() {
await SettingService.setGlobal({
key: 'automate_username_creation',
requestBody: { value: true }
})
getAutomateUsernameCreationSetting()
sendUserToast('Automatic username creation enabled')
listUsers()
}
</script>
<SearchItems
@@ -93,6 +112,42 @@
<div class="pt-4" />
<TabContent value="users">
<div class="h-full">
{#if !automateUsernameCreation && !isCloudHosted()}
<div class="mb-4">
<h3 class="mb-2">
Automatic username creation
<Tooltip>
Automatically create a username for new users based on their email, shared
across workspaces.
</Tooltip>
</h3>
<Button
btnClasses="w-auto"
size="sm"
color="dark"
on:click={() => {
automateUsernameModalOpen = true
}}
>
Enable (recommended)
</Button>
<ConfirmationModal
open={automateUsernameModalOpen}
on:confirmed={() => {
automateUsernameModalOpen = false
enableAutomateUsernameCreationSetting()
}}
on:canceled={() => (automateUsernameModalOpen = false)}
title="Automatic username creation"
confirmationText="Enable"
>
Once activated, it will not be possible to disable this feature. In case
existing users have different usernames in different workspaces, you will have
to manually confirm the username for each user.
</ConfirmationModal>
</div>
{/if}
<div class="py-2 mb-4">
<InviteGlobalUser on:new={listUsers} />
</div>
@@ -108,16 +163,38 @@
<th>email</th>
<th>auth</th>
<th>name</th>
{#if automateUsernameCreation}
<th>username</th>
{/if}
<th />
<th />
</tr>
<tbody slot="body" class="overflow-y-auto w-full h-full max-h-full">
{#if filteredUsers && users}
{#each filteredUsers.slice(0, nbDisplayed) as { email, super_admin, login_type, name } (email)}
{#each filteredUsers.slice(0, nbDisplayed) as { email, super_admin, login_type, name, username } (email)}
<tr class="border">
<td>{email}</td>
<td>{login_type}</td>
<td><span class="break-words">{truncate(name ?? '', 30)}</span></td>
{#if automateUsernameCreation}
<td>
{#if username}
{username}
{:else}
{#key filteredUsers.map((u) => u.username).join()}
<ChangeInstanceUsername
username=""
{email}
isConflict
on:renamed={() => {
listUsers()
}}
/>
{/key}
{/if}
</td>
{/if}
<td>
<ToggleButtonGroup
selected={super_admin}
@@ -143,9 +220,22 @@
</ToggleButtonGroup>
</td>
<td>
<div class="flex flex-row gap-x-1">
<button
class="text-red-500 whitespace-nowrap"
<div class="flex flex-row gap-x-1 justify-end">
{#if automateUsernameCreation && username}
<ChangeInstanceUsername
{username}
{email}
on:renamed={() => {
listUsers()
}}
/>
{/if}
<Button
color="light"
variant="contained"
size="xs"
spacingSize="xs2"
btnClasses="text-red-500"
on:click={() => {
deleteConfirmedCallback = async () => {
console.log(email)
@@ -156,7 +246,7 @@
}}
>
Remove
</button>
</Button>
</div>
</td>
</tr>
+53 -48
View File
@@ -112,14 +112,14 @@
<DrawerContent title="User Settings" on:close={closeDrawer}>
<div class="flex flex-col h-full">
<div>
<div class="text-xs pt-1 pb-2 text-tertiary flex-col flex">
Windmill <Version />
</div>
<div class="font-semibold flex items-baseline">
Theme: <DarkModeToggle forcedDarkMode={false} />
</div>
<div class="flex flex-wrap md:gap-40 gap-10 mt-2">
{#if scopes == undefined}
{#if scopes == undefined}
<div class="text-xs pt-1 pb-2 text-tertiary flex-col flex">
Windmill <Version />
</div>
<div class="font-semibold flex items-baseline">
Theme: <DarkModeToggle forcedDarkMode={false} />
</div>
<div class="flex flex-wrap md:gap-40 gap-10 mt-2">
<div>
<h2 class="border-b">User info</h2>
<div class="">
@@ -167,50 +167,55 @@
</div>
</div>
</div>
{/if}
<div>
<h2 class="border-b">AI user settings</h2>
<div class="flex flex-col gap-4 mt-2">
<Toggle
on:change={(e) => {
updateSetting(
codeCompletionSessionEnabled,
e.detail,
'codeCompletionSessionEnabled'
)
}}
checked={$codeCompletionSessionEnabled}
options={{
right: 'Code completion',
rightTooltip: 'AI completion in the code editors'
}}
/>
<Toggle
on:change={(e) => {
updateSetting(metadataCompletionEnabled, e.detail, 'metadataCompletionEnabled')
}}
checked={$metadataCompletionEnabled}
options={{
right: 'Metadata completion',
rightTooltip: 'AI completion for summaries and descriptions'
}}
/>
<Toggle
on:change={(e) => {
updateSetting(stepInputCompletionEnabled, e.detail, 'stepInputCompletionEnabled')
}}
checked={$stepInputCompletionEnabled}
options={{
right: 'Flow step input completion',
rightTooltip: 'AI completion for flow step inputs'
}}
/>
<div>
<h2 class="border-b">AI user settings</h2>
<div class="flex flex-col gap-4 mt-2">
<Toggle
on:change={(e) => {
updateSetting(
codeCompletionSessionEnabled,
e.detail,
'codeCompletionSessionEnabled'
)
}}
checked={$codeCompletionSessionEnabled}
options={{
right: 'Code completion',
rightTooltip: 'AI completion in the code editors'
}}
/>
<Toggle
on:change={(e) => {
updateSetting(metadataCompletionEnabled, e.detail, 'metadataCompletionEnabled')
}}
checked={$metadataCompletionEnabled}
options={{
right: 'Metadata completion',
rightTooltip: 'AI completion for summaries and descriptions'
}}
/>
<Toggle
on:change={(e) => {
updateSetting(
stepInputCompletionEnabled,
e.detail,
'stepInputCompletionEnabled'
)
}}
checked={$stepInputCompletionEnabled}
options={{
right: 'Flow step input completion',
rightTooltip: 'AI completion for flow step inputs'
}}
/>
</div>
</div>
</div>
</div>
{/if}
<div class="grid grid-cols-2 pt-8 pb-1">
<div class="grid grid-cols-2 pt-8 pb-1" class:pt-8={scopes == undefined}>
<h2 class="py-0 my-0 border-b pt-3">Tokens</h2>
<div class="flex justify-end border-b pb-1">
<Button
@@ -1,7 +1,7 @@
<script lang="ts">
import { goto } from '$app/navigation'
import { UserService, WorkspaceService } from '$lib/gen'
import { SettingService, UserService, WorkspaceService } from '$lib/gen'
import { validateUsername } from '$lib/utils'
import { page } from '$app/stores'
import { usersWorkspaceStore, workspaceStore } from '$lib/stores'
@@ -18,7 +18,7 @@
async function acceptInvite(): Promise<void> {
await UserService.acceptInvite({
requestBody: {
username,
username: automateUsernameCreation ? undefined : username,
workspace_id
}
})
@@ -46,33 +46,44 @@
}
}
UserService.globalWhoami().then((x) => {
let uname = ''
if (x.name) {
uname = x.name.split(' ')[0]
} else {
uname = x.email.split('@')[0]
let automateUsernameCreation = false
async function getAutomateUsernameCreationSetting() {
automateUsernameCreation =
(await SettingService.getGlobal({ key: 'automate_username_creation' })) ?? false
if (!automateUsernameCreation) {
UserService.globalWhoami().then((x) => {
let uname = ''
if (x.name) {
uname = x.name.split(' ')[0]
} else {
uname = x.email.split('@')[0]
}
username = uname.toLowerCase()
})
}
username = uname.toLowerCase()
})
}
getAutomateUsernameCreationSetting()
</script>
<!-- Enable submit form on enter -->
<CenteredModal title="Invitation to join {workspace_id}">
<label class="block pb-2">
<span class="text-secondary text-sm">Your username in workspace {workspace_id}:</span>
<input on:keyup={handleKey} bind:value={username} class:input-error={errorUsername != ''} />
{#if errorUsername}
<span class="text-red-500 text-xs">{errorUsername}</span>
{/if}
</label>
{#if !automateUsernameCreation}
<label class="block pb-2">
<span class="text-secondary text-sm">Your username in workspace {workspace_id}:</span>
<input on:keyup={handleKey} bind:value={username} class:input-error={errorUsername != ''} />
{#if errorUsername}
<span class="text-red-500 text-xs">{errorUsername}</span>
{/if}
</label>
{/if}
<div class="flex flex-row justify-between pt-4 gap-x-1">
<Button variant="border" size="sm" href="/user/workspaces"
>&leftarrow; Back to workspaces</Button
>
<button
disabled={checking || errorUsername != '' || !username}
disabled={checking || (!automateUsernameCreation && (errorUsername != '' || !username))}
class="place-items-end bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-2 border rounded"
type="button"
on:click={acceptInvite}
@@ -1,6 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation'
import { ResourceService, UserService, WorkspaceService } from '$lib/gen'
import { ResourceService, SettingService, UserService, WorkspaceService } from '$lib/gen'
import { validateUsername } from '$lib/utils'
import { logoutWithRedirect } from '$lib/logout'
import { page } from '$app/stores'
@@ -52,7 +52,7 @@
requestBody: {
id,
name,
username
username: automateUsernameCreation ? undefined : username
}
})
if (auto_invite) {
@@ -62,7 +62,14 @@
})
}
if (openAiKey != '') {
let path = `u/${username}/openai_windmill_codegen`
let actualUsername = username
if (automateUsernameCreation) {
const user = await UserService.whoami({
workspace: id
})
actualUsername = user.username
}
let path = `u/${actualUsername}/openai_windmill_codegen`
await ResourceService.createResource({
workspace: id,
requestBody: {
@@ -107,20 +114,29 @@
}
}
let automateUsernameCreation = false
async function getAutomateUsernameCreationSetting() {
automateUsernameCreation =
(await SettingService.getGlobal({ key: 'automate_username_creation' })) ?? false
if (!automateUsernameCreation) {
UserService.globalWhoami().then((x) => {
let uname = ''
if (x.name) {
uname = x.name.split(' ')[0]
} else {
uname = x.email.split('@')[0]
}
uname = uname.replace(/\./gi, '')
username = uname.toLowerCase()
})
}
}
getAutomateUsernameCreationSetting()
onMount(() => {
loadWorkspaces()
UserService.globalWhoami().then((x) => {
let uname = ''
if (x.name) {
uname = x.name.split(' ')[0]
} else {
uname = x.email.split('@')[0]
}
uname = uname.replace(/\./gi, '')
username = uname.toLowerCase()
})
WorkspaceService.isDomainAllowed().then((x) => {
isDomainAllowed = x
})
@@ -139,7 +155,7 @@
<label class="block pb-4 pt-4">
<span class="text-secondary text-sm">Workspace name</span>
<span class="ml-4 text-tertiary text-xs">Displayable name</span>
<!-- svelte-ignore a11y-autofocus -->
<input autofocus type="text" bind:value={name} />
</label>
<label class="block pb-4">
@@ -150,13 +166,15 @@
{/if}
<input type="text" bind:value={id} class:input-error={errorId != ''} />
</label>
<label class="block pb-4">
<span class="text-secondary text-sm">Your username in that workspace</span>
<input type="text" bind:value={username} on:keyup={handleKeyUp} />
{#if errorUser}
<span class="text-red-500 text-xs">{errorUser}</span>
{/if}
</label>
{#if !automateUsernameCreation}
<label class="block pb-4">
<span class="text-secondary text-sm">Your username in that workspace</span>
<input type="text" bind:value={username} on:keyup={handleKeyUp} />
{#if errorUser}
<span class="text-red-500 text-xs">{errorUser}</span>
{/if}
</label>
{/if}
<label class="block pb-4">
<span class="text-secondary text-sm">
OpenAI key for Windmill AI
@@ -226,7 +244,11 @@
>&leftarrow; Back to workspaces</Button
>
<Button
disabled={checking || errorId != '' || errorUser != '' || !name || !username || !id}
disabled={checking ||
errorId != '' ||
!name ||
(!automateUsernameCreation && (errorUser != '' || !username)) ||
!id}
on:click={createWorkspace}
>
Create workspace