fix: replace email with permissioned_as for triggers/schedules (#8439)

* refactor: replace email with permissioned_as for triggers/schedules

Add a new `permissioned_as` column (format: `u/{username}`, `g/{group}`,
or raw email) to all trigger tables and schedule. This value is used
directly for job permission checks, removing the need for email lookups
when creating/updating triggers.

- Migration: add permissioned_as to all 9 trigger tables + schedule,
  drop email from trigger tables (schedule keeps it for backwards compat)
- Backend: resolve_email() (async, DB) -> resolve_permissioned_as() (sync)
- Email cache: get_email_from_permissioned_as() with quick_cache for
  places that still need email (fetch_api_authed, schedule backwards compat)
- Frontend: rename email/preserve_email -> permissioned_as/preserve_permissioned_as
  in deploy data and OpenAPI schemas
- Tests updated for new field names and u/{username} format

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix sqlx/build

* update ee ref

* refactor: simplify resolve_edited_by to always use authed username

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix compile + migration

* update ref

* test: add trigger trait method tests for permissioned_as queries

Add tests that call TriggerCrud and Listener trait methods directly
to verify dynamic SQL correctly references the permissioned_as column.
Covers get_trigger_by_path, list_triggers, set_trigger_mode, and
fetch_enabled_unlistened_triggers for all trigger types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* update sqlx

* fix: use permissioned_as directly for schedules and fix audit RLS for groups

- Schedule: permissioned_as only set on create, not on edit/set_enabled
- Schedule: stop reading email column, use get_email_from_permissioned_as
- Triggers: use fetch_api_authed_from_permissioned_as instead of edited_by
- Triggers: rename listener fields for clarity (username -> edited_by)
- Fix audit author username for group permissioned_as (g/test -> group-test)
  to match session.user, preventing RLS policy violations on audit_partitioned
- OpenAPI: remove permissioned_as/preserve_permissioned_as from EditSchedule
- Add backwards-compat comments for schedule email writes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: regenerate system prompts for permissioned_as field

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix build

* refactor: generalize onBehalfOf naming, add permissioned_as to EditSchedule

- Frontend: rename onBehalfOfPermissionedAs -> onBehalfOf with comments
  explaining it carries emails for flows/scripts and permissioned_as for
  triggers/schedules
- Frontend: rename getOnBehalfOfEmail -> getOnBehalfOf,
  getOnBehalfOfPermissionedAsForDeploy -> getOnBehalfOfForDeploy,
  customOnBehalfOfEmails -> customOnBehalfOf
- Backend: add optional permissioned_as/preserve_permissioned_as to
  EditSchedule with COALESCE (only updates when provided)
- Backend: add on_behalf_of audit log for schedule edit
- Backend: remove unused resolve_on_behalf_of_permissioned_as
- Tests: remove email assertions from schedule update test (email is
  just backwards compat, only permissioned_as matters)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: preserve email column when permissioned_as is preserved on schedule edit

Derive email from the preserved permissioned_as via cache lookup instead
of always writing authed.email. This keeps the email column consistent
with the old behavior for backwards compat with old workers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: update deploy UI labels from "edited by" to "run as" for triggers

Triggers now use permissioned_as (not edited_by) for permissions, so
update the deploy UI wording to reflect this. Also update wm_deployers
group description to mention schedules and permissioned_as.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use u/username format for custom trigger/schedule deploy selection

When picking a custom user for trigger/schedule deployment, store
u/${username} (permissioned_as format) instead of the email. Flows/scripts
continue to use email format for on_behalf_of_email.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: show u/username format for "me" option in trigger deploy selector

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: simplify OnBehalfOfSelector to return the right format per kind

OnBehalfOfSelector now handles the email vs permissioned_as format
internally based on kind:
- triggers: returns u/username, displays u/username in all options
- flows/scripts/apps: returns email, displays username

The onSelect callback now takes (choice, value?) where value is already
in the correct format. Parent components just store it directly without
needing to know about the format difference.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: always show u/username format in OnBehalfOfSelector for all kinds

Display is now consistent: all kinds show u/username in the selector.
The returned value still differs (email for flows/scripts, u/username
for triggers) since the backend APIs expect different formats.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace email with permissioned_as in http_trigger test insert

The email column was dropped from trigger tables in the migration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: review fixes — migration, app policy, capture cleanup, naming

- Migration: remove DEFAULT '', use nullable → populate → SET NOT NULL
- App policy: set both on_behalf_of and on_behalf_of_email for all choices
- OnBehalfOfSelector: return OnBehalfOfDetails {email, permissionedAs} instead of ambiguous value
- Remove unused email field from Capture struct and query
- Rename getSourceEmail/getTargetEmail → getSourceOnBehalfOf/getTargetOnBehalfOf
- Rename test functions from preserve_email to preserve_permissioned_as

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add permissioned_as to all test schedule INSERTs

Since the migration no longer uses DEFAULT '', all INSERTs must
explicitly provide permissioned_as. Updated test fixtures and
schedule_push tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: strip permissioned_as from exports/sync, fix OpenAPI required field

- Add permissioned_as to workspace export strip list (like edited_by)
- Add permissioned_as to CLI TriggerFile Omit list
- Fix TriggerExtraProperty.required: email → permissioned_as
- Regenerate frontend and CLI types

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove accidentally committed generated files

These directories are gitignored and should not be tracked.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: regenerate system prompts for permissioned_as schema changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove permissioned_as from CLI TriggerFile Omit list

Already stripped in workspace export, no need to also omit from the type.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: optimize email cache key and revert TriggerFile Omit change

- Use single concatenated string for cache key instead of (String, String) tuple
- Remove permissioned_as from CLI TriggerFile Omit (already stripped in export)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: zero-allocation email cache lookups using Equivalent trait

Use a borrowed EmailCacheKey(&str, &str) for cache lookups via
quick_cache's Equivalent support. Only allocates (String, String)
on cache miss for insert. This is called on every trigger fire
and schedule push.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add permissioned_as to Schedule required fields in OpenAPI spec

The backend always returns permissioned_as (non-optional String),
so the schema should reflect that.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: handle group- prefix in migration UPDATE statements

edited_by can be 'group-{name}' for group-owned triggers/schedules.
The migration now correctly maps these to 'g/{name}' format instead
of incorrectly producing 'u/group-{name}'.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Revert "fix: handle group- prefix in migration UPDATE statements"

This reverts commit 0971392b38.

* fix: use superadmin email to resolve permissioned_as in schedule migration

For users upgrading from older versions where edited_by may not reflect
the actual schedule owner, check if the email belongs to a superadmin
and look up their username. Otherwise fall back to edited_by.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: fall back to superadmin email when not in workspace usr table

If the superadmin isn't a member of the workspace, use their email
as raw permissioned_as instead of falling back to edited_by.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: always update permissioned_as and email on schedule edit

Consistent with pre-refactor behavior where email and edited_by
were always updated on every edit. permissioned_as is now always
set (to editing user or preserved value), removing the COALESCE
that previously preserved it when not provided.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add schedule permission tests and centralize group prefix constants

Tests: schedule create/update for normal user, workspace admin, and
superadmin not in workspace. Verifies schedule fields (email,
permissioned_as, edited_by) and pushed job fields (permissioned_as,
permissioned_as_email).

Constants: centralize "u/", "g/", "group-" as PERMISSIONED_AS_USER_PREFIX,
PERMISSIONED_AS_GROUP_PREFIX, USERNAME_GROUP_PREFIX.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use @unknown.windmill.dev for synthetic email fallback

Prevents privilege escalation: a user with username like
'superadmin_secret' would get superadmin via the synthetic
email matching SUPERADMIN_SECRET_EMAIL. Using a different
subdomain avoids any collision with hardcoded @windmill.dev emails.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* update ee ref

* sqlx

* chore: regenerate system prompts after main merge

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to bda51bc33bcb573659e7ff07d0a23ff6e23b8148

This commit updates the EE repository reference after PR #468 was merged in windmill-ee-private.

Previous ee-repo-ref: 8cf1802f8fe183f430830590b4f3172a50207843

New ee-repo-ref: bda51bc33bcb573659e7ff07d0a23ff6e23b8148

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
This commit is contained in:
hugocasa
2026-03-20 17:28:38 +01:00
committed by GitHub
parent 51957f7d92
commit efb4a27d51
104 changed files with 2447 additions and 1035 deletions
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO websocket_trigger (\n path, url, script_path, is_flow, workspace_id,\n edited_by, email, server_id, error\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ",
"query": "\n INSERT INTO websocket_trigger (\n path, url, script_path, is_flow, workspace_id,\n edited_by, permissioned_as, server_id, error\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -18,5 +18,5 @@
},
"nullable": []
},
"hash": "1d4bb4f53574ef95ef1016b760f849ec2372ac6a21bb2556d17a96dc72ea4980"
"hash": "02748cae17e8966dbd57a33017ccb747c84fcc12fbfd93c6c749570b94d35696"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO websocket_trigger (\n path, url, script_path, is_flow, workspace_id,\n edited_by, email, mode\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8::trigger_mode)\n ",
"query": "\n INSERT INTO websocket_trigger (\n path, url, script_path, is_flow, workspace_id,\n edited_by, permissioned_as, mode\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8::trigger_mode)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -28,5 +28,5 @@
},
"nullable": []
},
"hash": "7c1ae9cac13d1387cfa94149f039054dd8c30c16b4657e73cdb0d7c7f1cb3b6d"
"hash": "02e04f9ebc0e14f98f290bf2dc3eb00bc613ba7d29f8dd5ff31a4acd0ef3adfd"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO kafka_trigger (\n path, kafka_resource_path, topics, group_id, script_path,\n is_flow, workspace_id, edited_by, email\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ",
"query": "\n INSERT INTO kafka_trigger (\n path, kafka_resource_path, topics, group_id, script_path,\n is_flow, workspace_id, edited_by, permissioned_as\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -18,5 +18,5 @@
},
"nullable": []
},
"hash": "1cad2ebfbdc46f9c0d93329897a71701f17a33b708b334d909563c9a0dcc9c23"
"hash": "066c9690d1606bf889879b7e3c686529c37db0d5f18c83706bfbc63c8c3e4315"
}
@@ -0,0 +1,35 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email, permissioned_as, edited_by FROM schedule WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "permissioned_as",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "edited_by",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "0f26c74f604e1c3c613de8ba654cac1a41b20b1d3ea0f1a1c4ea2fcbbd314d7e"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO http_trigger (\n path, route_path, route_path_key, script_path, is_flow,\n workspace_id, edited_by, email, http_method,\n authentication_method, is_static_website, workspaced_route,\n wrap_body, raw_string\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::http_method,\n $10::authentication_method, $11, $12, $13, $14)\n ",
"query": "\n INSERT INTO http_trigger (\n path, route_path, route_path_key, script_path, is_flow,\n workspace_id, edited_by, permissioned_as, http_method,\n authentication_method, is_static_website, workspaced_route,\n wrap_body, raw_string\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::http_method,\n $10::authentication_method, $11, $12, $13, $14)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -50,5 +50,5 @@
},
"nullable": []
},
"hash": "6afa076744233fc5e92188ff978990fa3a704afe3eec523f4e203f7f6e247261"
"hash": "13d60d85694b5a5fcfc7687a07b78a54ff53245271466b1f3a9d9edf43cdaa1f"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE email_trigger\n SET\n script_path = $1,\n path = $2,\n is_flow = $3,\n local_part = $4,\n workspaced_local_part = $5,\n edited_by = $6,\n email = $7,\n edited_at = now(),\n error_handler_path = $8,\n error_handler_args = $9,\n retry = $10,\n mode = $11\n WHERE\n workspace_id = $12 AND path = $13\n ",
"query": "\n UPDATE email_trigger\n SET\n script_path = $1,\n path = $2,\n is_flow = $3,\n local_part = $4,\n workspaced_local_part = $5,\n edited_by = $6,\n permissioned_as = $7,\n edited_at = now(),\n error_handler_path = $8,\n error_handler_args = $9,\n retry = $10,\n mode = $11\n WHERE\n workspace_id = $12 AND path = $13\n ",
"describe": {
"columns": [],
"parameters": {
@@ -33,5 +33,5 @@
},
"nullable": []
},
"hash": "388ff2abd495cf71e87cf0c4ddc73b6c84867fb966df91b320c54acdd5e61315"
"hash": "1b7803a2060a19cb6e71f1e97619891ea8449b4a9433908cf717738846f7eec5"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE sqs_trigger\n SET\n queue_url = $1,\n aws_resource_path = $2,\n message_attributes = $3,\n aws_auth_resource_type = $4,\n script_path = $5,\n path = $6,\n is_flow = $7,\n edited_by = $8,\n email = $9,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $12,\n error_handler_args = $13,\n retry = $14\n WHERE\n workspace_id = $10 AND path = $11\n ",
"query": "\n UPDATE sqs_trigger\n SET\n queue_url = $1,\n aws_resource_path = $2,\n message_attributes = $3,\n aws_auth_resource_type = $4,\n script_path = $5,\n path = $6,\n is_flow = $7,\n edited_by = $8,\n permissioned_as = $9,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $12,\n error_handler_args = $13,\n retry = $14\n WHERE\n workspace_id = $10 AND path = $11\n ",
"describe": {
"columns": [],
"parameters": {
@@ -33,5 +33,5 @@
},
"nullable": []
},
"hash": "c723c3a5066a487b93e2642993f3bf624a1f50d06c7de75157420d97cf144763"
"hash": "1d2514b3d75ffb6cc0eb09ceb8fde974a07881eb08164e885448d9a17c4ca6db"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO email_trigger (\n path, local_part, workspaced_local_part, script_path,\n is_flow, workspace_id, edited_by, email\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ",
"query": "\n INSERT INTO email_trigger (\n path, local_part, workspaced_local_part, script_path,\n is_flow, workspace_id, edited_by, permissioned_as\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -17,5 +17,5 @@
},
"nullable": []
},
"hash": "1074c6c98e6a0c83ac04172a39abea21c793f58947051d39931d4da0868a1d77"
"hash": "1e28751bb98a1c477c0e582a2a39f81bf34e2d72f35ef1ea5d8c057ec9e694d8"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO kafka_trigger (\n path, kafka_resource_path, topics, group_id, script_path,\n is_flow, workspace_id, edited_by, email, auto_commit\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)\n ",
"query": "\n INSERT INTO kafka_trigger (\n path, kafka_resource_path, topics, group_id, script_path,\n is_flow, workspace_id, edited_by, permissioned_as, auto_commit\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -19,5 +19,5 @@
},
"nullable": []
},
"hash": "45fc21026fa76e5d69f00a68a7be81abb3ec627578f2d14f0ce33896dc6ab4cf"
"hash": "1ef63255389bdc47d5392a84aad38adf1ccc3a4923f988d8abaafc9749307c0e"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE\n mqtt_trigger\n SET\n mqtt_resource_path = $1,\n subscribe_topics = $2,\n client_version = $3,\n client_id = $4,\n v3_config = $5,\n v5_config = $6,\n is_flow = $7,\n edited_by = $8,\n email = $9,\n script_path = $10,\n path = $11,\n edited_at = now(),\n error = NULL,\n server_id = NULL,\n error_handler_path = $14,\n error_handler_args = $15,\n retry = $16\n WHERE\n workspace_id = $12 AND\n path = $13\n ",
"query": "\n UPDATE\n mqtt_trigger\n SET\n mqtt_resource_path = $1,\n subscribe_topics = $2,\n client_version = $3,\n client_id = $4,\n v3_config = $5,\n v5_config = $6,\n is_flow = $7,\n edited_by = $8,\n permissioned_as = $9,\n script_path = $10,\n path = $11,\n edited_at = now(),\n error = NULL,\n server_id = NULL,\n error_handler_path = $14,\n error_handler_args = $15,\n retry = $16\n WHERE\n workspace_id = $12 AND\n path = $13\n ",
"describe": {
"columns": [],
"parameters": {
@@ -35,5 +35,5 @@
},
"nullable": []
},
"hash": "e486a64b76da5de97e404c81dd6e29d333ada2dcfbbddb028f37794b85778ca8"
"hash": "1f693e2fba9885f7fc49bd2994240c4421fdae4aab496a3397c5c07d960582f2"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO postgres_trigger (\n workspace_id,\n path,\n postgres_resource_path,\n replication_slot_name,\n publication_name,\n script_path,\n is_flow,\n mode,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, now(), $11, $12, $13\n )\n ",
"query": "\n INSERT INTO postgres_trigger (\n workspace_id,\n path,\n postgres_resource_path,\n replication_slot_name,\n publication_name,\n script_path,\n is_flow,\n mode,\n edited_by,\n permissioned_as,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, now(), $11, $12, $13\n )\n ",
"describe": {
"columns": [],
"parameters": {
@@ -33,5 +33,5 @@
},
"nullable": []
},
"hash": "fb942aa7894b4ae904f0233405f62f201e3f5deed593128017b886342ef6d210"
"hash": "21d7ce033b5f67499f579aeae98806599401fcfa9765a58b8cd45a20b411d0ca"
}
@@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "SELECT created_by, permissioned_as, permissioned_as_email\n FROM v2_job\n WHERE workspace_id = 'test-workspace'\n AND trigger_kind = 'schedule'\n AND trigger = $1\n ORDER BY created_at DESC\n LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "created_by",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "permissioned_as",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "permissioned_as_email",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "270cfaea4f888e73e21a957e0328ec1f990fce409160eb7b0e807bff56defff4"
}
@@ -1,11 +1,11 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email, edited_by FROM http_trigger WHERE path = $1 AND workspace_id = $2",
"query": "SELECT permissioned_as, edited_by FROM websocket_trigger WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"name": "permissioned_as",
"type_info": "Varchar"
},
{
@@ -25,5 +25,5 @@
false
]
},
"hash": "8311a553c44221751ffdbbe6a997d6feba8d43292daf6c5433b66bd8450e8854"
"hash": "39062cdb183b97906c25602000321a76d4e629ba027364190a2487cc9bd93235"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO email_trigger (\n workspace_id,\n path,\n script_path,\n is_flow,\n local_part,\n workspaced_local_part,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry,\n mode\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, now(), $9, $10, $11, $12\n )\n ",
"query": "\n INSERT INTO email_trigger (\n workspace_id,\n path,\n script_path,\n is_flow,\n local_part,\n workspaced_local_part,\n edited_by,\n permissioned_as,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry,\n mode\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, now(), $9, $10, $11, $12\n )\n ",
"describe": {
"columns": [],
"parameters": {
@@ -32,5 +32,5 @@
},
"nullable": []
},
"hash": "3d763dbb411e28ce026cc9ab525b20b409cfe17bf1cdef22aaffc719cf6c53e3"
"hash": "3aa3d0362fa8ed97dd034454b05f14880c9bb43d0c3ce4ea9b4511aa2d092d0c"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO http_trigger (\n workspace_id,\n path,\n route_path,\n route_path_key,\n workspaced_route,\n authentication_resource_path,\n wrap_body,\n raw_string,\n script_path,\n summary,\n description,\n is_flow,\n mode,\n request_type,\n authentication_method,\n http_method,\n static_asset_config,\n edited_by,\n email,\n edited_at,\n is_static_website,\n error_handler_path,\n error_handler_args,\n retry\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, now(), $20, $21, $22, $23\n )\n ",
"query": "\n INSERT INTO http_trigger (\n workspace_id,\n path,\n route_path,\n route_path_key,\n workspaced_route,\n authentication_resource_path,\n wrap_body,\n raw_string,\n script_path,\n summary,\n description,\n is_flow,\n mode,\n request_type,\n authentication_method,\n http_method,\n static_asset_config,\n edited_by,\n permissioned_as,\n edited_at,\n is_static_website,\n error_handler_path,\n error_handler_args,\n retry\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, now(), $20, $21, $22, $23\n )\n ",
"describe": {
"columns": [],
"parameters": {
@@ -81,5 +81,5 @@
},
"nullable": []
},
"hash": "ee9ee0fbf5dd72d190e18c56622244b984fdb36fbf70197d9f8cb6306c9670db"
"hash": "41ad9954bebe31b0545837147b1616da2e6743f3a45775cd556a7414bc8f9726"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE nats_trigger\n SET\n nats_resource_path = $1,\n subjects = $2,\n stream_name = $3,\n consumer_name = $4,\n use_jetstream = $5,\n script_path = $6,\n path = $7,\n is_flow = $8,\n edited_by = $9,\n email = $10,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $13,\n error_handler_args = $14,\n retry = $15\n WHERE\n workspace_id = $11 AND path = $12\n ",
"query": "\n UPDATE nats_trigger\n SET\n nats_resource_path = $1,\n subjects = $2,\n stream_name = $3,\n consumer_name = $4,\n use_jetstream = $5,\n script_path = $6,\n path = $7,\n is_flow = $8,\n edited_by = $9,\n permissioned_as = $10,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $13,\n error_handler_args = $14,\n retry = $15\n WHERE\n workspace_id = $11 AND path = $12\n ",
"describe": {
"columns": [],
"parameters": {
@@ -24,5 +24,5 @@
},
"nullable": []
},
"hash": "9f41ea5cbe4cffa74e4a283fe8f023c813e349956487f7b6599da452c068e9b9"
"hash": "4ba114f54ed88c27dd4aed05a273c91e98913397b310ff760c1dbb9077764e9b"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE schedule SET\n schedule = $1,\n timezone = $2,\n args = $3,\n on_failure = $4,\n on_failure_times = $5,\n on_failure_exact = $6,\n on_failure_extra_args = $7,\n on_recovery = $8,\n on_recovery_times = $9,\n on_recovery_extra_args = $10,\n on_success = $11,\n on_success_extra_args = $12,\n ws_error_handler_muted = $13,\n retry = $14,\n summary = $15,\n no_flow_overlap = $16,\n tag = $17,\n paused_until = $18,\n path = $19,\n workspace_id = $20,\n cron_version = COALESCE($21, cron_version),\n description = $22,\n dynamic_skip = $23,\n email = COALESCE($24, email),\n edited_by = $25\n WHERE path = $19 AND workspace_id = $20\n RETURNING\n workspace_id,\n path,\n edited_by,\n edited_at,\n schedule,\n timezone,\n enabled,\n script_path,\n is_flow,\n args AS \"args: _\",\n extra_perms,\n email,\n error,\n on_failure,\n on_failure_times,\n on_failure_exact,\n on_failure_extra_args AS \"on_failure_extra_args: _\",\n on_recovery,\n on_recovery_times,\n on_recovery_extra_args AS \"on_recovery_extra_args: _\",\n on_success,\n on_success_extra_args AS \"on_success_extra_args: _\",\n ws_error_handler_muted,\n retry,\n no_flow_overlap,\n summary,\n description,\n tag,\n paused_until,\n cron_version,\n dynamic_skip\n ",
"query": "\n UPDATE schedule SET\n schedule = $1,\n timezone = $2,\n args = $3,\n on_failure = $4,\n on_failure_times = $5,\n on_failure_exact = $6,\n on_failure_extra_args = $7,\n on_recovery = $8,\n on_recovery_times = $9,\n on_recovery_extra_args = $10,\n on_success = $11,\n on_success_extra_args = $12,\n ws_error_handler_muted = $13,\n retry = $14,\n summary = $15,\n no_flow_overlap = $16,\n tag = $17,\n paused_until = $18,\n path = $19,\n workspace_id = $20,\n cron_version = COALESCE($21, cron_version),\n description = $22,\n dynamic_skip = $23,\n email = $24,\n edited_by = $25,\n permissioned_as = $26\n WHERE path = $19 AND workspace_id = $20\n RETURNING\n workspace_id,\n path,\n edited_by,\n edited_at,\n schedule,\n timezone,\n enabled,\n script_path,\n is_flow,\n args AS \"args: _\",\n extra_perms,\n email,\n permissioned_as,\n error,\n on_failure,\n on_failure_times,\n on_failure_exact,\n on_failure_extra_args AS \"on_failure_extra_args: _\",\n on_recovery,\n on_recovery_times,\n on_recovery_extra_args AS \"on_recovery_extra_args: _\",\n on_success,\n on_success_extra_args AS \"on_success_extra_args: _\",\n ws_error_handler_muted,\n retry,\n no_flow_overlap,\n summary,\n description,\n tag,\n paused_until,\n cron_version,\n dynamic_skip\n ",
"describe": {
"columns": [
{
@@ -65,96 +65,101 @@
},
{
"ordinal": 12,
"name": "permissioned_as",
"type_info": "Varchar"
},
{
"ordinal": 13,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 13,
"ordinal": 14,
"name": "on_failure",
"type_info": "Varchar"
},
{
"ordinal": 14,
"ordinal": 15,
"name": "on_failure_times",
"type_info": "Int4"
},
{
"ordinal": 15,
"ordinal": 16,
"name": "on_failure_exact",
"type_info": "Bool"
},
{
"ordinal": 16,
"ordinal": 17,
"name": "on_failure_extra_args: _",
"type_info": "Jsonb"
},
{
"ordinal": 17,
"ordinal": 18,
"name": "on_recovery",
"type_info": "Varchar"
},
{
"ordinal": 18,
"ordinal": 19,
"name": "on_recovery_times",
"type_info": "Int4"
},
{
"ordinal": 19,
"ordinal": 20,
"name": "on_recovery_extra_args: _",
"type_info": "Jsonb"
},
{
"ordinal": 20,
"ordinal": 21,
"name": "on_success",
"type_info": "Varchar"
},
{
"ordinal": 21,
"ordinal": 22,
"name": "on_success_extra_args: _",
"type_info": "Jsonb"
},
{
"ordinal": 22,
"ordinal": 23,
"name": "ws_error_handler_muted",
"type_info": "Bool"
},
{
"ordinal": 23,
"ordinal": 24,
"name": "retry",
"type_info": "Jsonb"
},
{
"ordinal": 24,
"ordinal": 25,
"name": "no_flow_overlap",
"type_info": "Bool"
},
{
"ordinal": 25,
"ordinal": 26,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 26,
"ordinal": 27,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 27,
"ordinal": 28,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 28,
"ordinal": 29,
"name": "paused_until",
"type_info": "Timestamptz"
},
{
"ordinal": 29,
"ordinal": 30,
"name": "cron_version",
"type_info": "Text"
},
{
"ordinal": 30,
"ordinal": 31,
"name": "dynamic_skip",
"type_info": "Varchar"
}
@@ -185,6 +190,7 @@
"Text",
"Varchar",
"Varchar",
"Varchar",
"Varchar"
]
},
@@ -201,6 +207,7 @@
true,
false,
false,
false,
true,
true,
true,
@@ -222,5 +229,5 @@
true
]
},
"hash": "987d79f7c6d7bc148cc8aab67e47161cfca045966e995e28c7a7ad090cffeda0"
"hash": "54b4c762add9b1ebfdb2a6d5abd6d20e86dc0e6544f0bb22fa4ec68aa54a4dc8"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE\n http_trigger\n SET\n route_path = $1,\n route_path_key = $2,\n workspaced_route = $3,\n wrap_body = $4,\n raw_string = $5,\n authentication_resource_path = $6,\n script_path = $7,\n path = $8,\n is_flow = $9,\n mode = $10,\n http_method = $11,\n static_asset_config = $12,\n edited_by = $13,\n email = $14,\n request_type = $15,\n authentication_method = $16,\n summary = $17,\n description = $18,\n edited_at = now(),\n is_static_website = $19,\n error_handler_path = $20,\n error_handler_args = $21,\n retry = $22\n WHERE\n workspace_id = $23 AND\n path = $24\n ",
"query": "\n UPDATE\n http_trigger\n SET\n route_path = $1,\n route_path_key = $2,\n workspaced_route = $3,\n wrap_body = $4,\n raw_string = $5,\n authentication_resource_path = $6,\n script_path = $7,\n path = $8,\n is_flow = $9,\n mode = $10,\n http_method = $11,\n static_asset_config = $12,\n edited_by = $13,\n permissioned_as = $14,\n request_type = $15,\n authentication_method = $16,\n summary = $17,\n description = $18,\n edited_at = now(),\n is_static_website = $19,\n error_handler_path = $20,\n error_handler_args = $21,\n retry = $22\n WHERE\n workspace_id = $23 AND\n path = $24\n ",
"describe": {
"columns": [],
"parameters": {
@@ -82,5 +82,5 @@
},
"nullable": []
},
"hash": "2fd0d3224382b000028d98b0af4c431d3cadd54cca65d83c1ab7f2d2972e2282"
"hash": "5efbf92ac7347e73769c66ffdc4037c7e56a5939d9ffcbee13e0264cbb2a6dfe"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n gcp_resource_path,\n script_path,\n is_flow,\n mode as \"mode: _\",\n workspace_id,\n path,\n edited_by,\n email,\n delivery_config AS \"delivery_config: _\",\n retry as \"retry: _\",\n error_handler_path,\n error_handler_args as \"error_handler_args: _\"\n FROM\n gcp_trigger\n WHERE\n workspace_id = $1 AND\n path = $2 AND\n delivery_type = 'push'::DELIVERY_MODE\n ",
"query": "\n SELECT\n gcp_resource_path,\n script_path,\n is_flow,\n mode as \"mode: _\",\n workspace_id,\n path,\n edited_by,\n permissioned_as,\n delivery_config AS \"delivery_config: _\",\n retry as \"retry: _\",\n error_handler_path,\n error_handler_args as \"error_handler_args: _\"\n FROM\n gcp_trigger\n WHERE\n workspace_id = $1 AND\n path = $2 AND\n delivery_type = 'push'::DELIVERY_MODE\n ",
"describe": {
"columns": [
{
@@ -51,7 +51,7 @@
},
{
"ordinal": 7,
"name": "email",
"name": "permissioned_as",
"type_info": "Varchar"
},
{
@@ -96,5 +96,5 @@
true
]
},
"hash": "1cf2eb1426e8be89c3649272103bcd029e99b029b7f1b71eda4411d1e24e790d"
"hash": "6cfa6b5f16207863a03b77fffbb94ae91872a207ea2362e49e100ed1f0b35198"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO websocket_trigger (\n path, url, script_path, is_flow, workspace_id,\n edited_by, email\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ",
"query": "\n INSERT INTO websocket_trigger (\n path, url, script_path, is_flow, workspace_id,\n edited_by, permissioned_as\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -16,5 +16,5 @@
},
"nullable": []
},
"hash": "57b7236cae0b6a1940f4c2d4b202692450ee231488d9a55ca59ff53a6f674626"
"hash": "72bc0b4acb3fb155436df74b0bb11600df8e55d50fa48d21ce2c5ae82eaf9f1c"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n path,\n script_path,\n is_flow,\n route_path,\n authentication_resource_path,\n workspace_id,\n request_type AS \"request_type: _\",\n authentication_method AS \"authentication_method: _\",\n edited_by,\n email,\n static_asset_config AS \"static_asset_config: _\",\n wrap_body,\n raw_string,\n workspaced_route,\n is_static_website,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\",\n mode as \"mode: _\"\n FROM\n http_trigger\n WHERE\n http_method = $1 AND\n (mode = 'enabled'::TRIGGER_MODE OR mode = 'suspended'::TRIGGER_MODE)\n ",
"query": "\n SELECT\n path,\n script_path,\n is_flow,\n route_path,\n authentication_resource_path,\n workspace_id,\n request_type AS \"request_type: _\",\n authentication_method AS \"authentication_method: _\",\n edited_by,\n permissioned_as,\n static_asset_config AS \"static_asset_config: _\",\n wrap_body,\n raw_string,\n workspaced_route,\n is_static_website,\n error_handler_path,\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\",\n mode as \"mode: _\"\n FROM\n http_trigger\n WHERE\n http_method = $1 AND\n (mode = 'enabled'::TRIGGER_MODE OR mode = 'suspended'::TRIGGER_MODE)\n ",
"describe": {
"columns": [
{
@@ -75,7 +75,7 @@
},
{
"ordinal": 9,
"name": "email",
"name": "permissioned_as",
"type_info": "Varchar"
},
{
@@ -175,5 +175,5 @@
false
]
},
"hash": "3cd37daa80bc3697d331c19e01a49916fc03fdf9eceff73fa153020b4a48f4a2"
"hash": "7402639802ba5f286db8436d21dcce24aa615fe1db7e8b4fc468963216aab69d"
}
@@ -0,0 +1,18 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO nats_trigger (path, nats_resource_path, subjects, use_jetstream, script_path, is_flow, workspace_id, edited_by, permissioned_as) VALUES ($1, $2, $3, $4, $5, false, 'test-workspace', 'test-user', 'u/test-user')",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"VarcharArray",
"Bool",
"Varchar"
]
},
"nullable": []
},
"hash": "75b9a2153ba12ef443212d83743605e0f9d8c1f0fbe3271f0e65b1dc5cd47987"
}
@@ -1,11 +1,11 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email, edited_by FROM schedule WHERE path = $1 AND workspace_id = $2",
"query": "SELECT permissioned_as, edited_by FROM schedule WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"name": "permissioned_as",
"type_info": "Varchar"
},
{
@@ -25,5 +25,5 @@
false
]
},
"hash": "17aafb72843659df9594d6d2466d2afaf26e666ffe52e0ea85792ea31b63410c"
"hash": "893ff34f2b22cf89a24a0b613ed390077fe6c75f56a3419f530e542bab0fb1a4"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO http_trigger (\n path, route_path, route_path_key, script_path, is_flow,\n workspace_id, edited_by, email, http_method,\n authentication_method, is_static_website, workspaced_route,\n wrap_body, raw_string, mode\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::http_method,\n $10::authentication_method, $11, $12, $13, $14, $15::trigger_mode)\n ",
"query": "\n INSERT INTO http_trigger (\n path, route_path, route_path_key, script_path, is_flow,\n workspace_id, edited_by, permissioned_as, http_method,\n authentication_method, is_static_website, workspaced_route,\n wrap_body, raw_string, mode\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::http_method,\n $10::authentication_method, $11, $12, $13, $14, $15::trigger_mode)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -62,5 +62,5 @@
},
"nullable": []
},
"hash": "57eca702e951f5303a74643c7ba64472e2c2a781fbb6366d998a0f1ca22fcdf2"
"hash": "8d53b0f2df5fdb6c43b9f5e92c8a97669676249dbf512ff39db94ecacc51e04e"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO schedule (\n workspace_id, path, edited_by, schedule, enabled,\n script_path, is_flow, email, timezone\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ",
"query": "\n INSERT INTO schedule (\n workspace_id, path, edited_by, schedule, enabled,\n script_path, is_flow, email, timezone, permissioned_as\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -13,10 +13,11 @@
"Varchar",
"Bool",
"Varchar",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "75ce9753a8acc8eccb3f2e0baaa5a871f866a8d21961fac8a003bc40c406ee79"
"hash": "906773b0de209734e9cf9c6421d4b3bf95ed2e00ac07942d8bba2b11b8462d3b"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO websocket_trigger (\n workspace_id,\n path,\n url,\n script_path,\n is_flow,\n mode,\n filters,\n initial_messages,\n url_runnable_args,\n edited_by,\n can_return_message,\n can_return_error_result,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, now(), $14, $15, $16\n )\n ",
"query": "\n INSERT INTO websocket_trigger (\n workspace_id,\n path,\n url,\n script_path,\n is_flow,\n mode,\n filters,\n initial_messages,\n url_runnable_args,\n edited_by,\n can_return_message,\n can_return_error_result,\n permissioned_as,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, now(), $14, $15, $16\n )\n ",
"describe": {
"columns": [],
"parameters": {
@@ -36,5 +36,5 @@
},
"nullable": []
},
"hash": "e99c958628d83e3fea054eaa182b1301e0b04c7eb9801d9de13b37bd908d902f"
"hash": "942c0abb55c910862fd45d3fa56a4eb6729f1a658101bda2d0b0fca96b3cfee5"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO nats_trigger (\n path, nats_resource_path, subjects, script_path,\n is_flow, workspace_id, edited_by, email, use_jetstream\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ",
"query": "\n INSERT INTO nats_trigger (\n path, nats_resource_path, subjects, script_path,\n is_flow, workspace_id, edited_by, permissioned_as, use_jetstream\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -18,5 +18,5 @@
},
"nullable": []
},
"hash": "97c60a4193a75b611db41543e7fe6da81fc631cc6ac43576f8a18afedad4d4a4"
"hash": "9883cff4f988767aeea7bce8b66672029b2a4c90e06c05d4356b3ec6bd8d2748"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO kafka_trigger (\n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\n filters,\n auto_offset_reset,\n auto_commit,\n script_path,\n is_flow,\n mode,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, now(), $14, $15, $16\n )\n ",
"query": "\n INSERT INTO kafka_trigger (\n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\n filters,\n auto_offset_reset,\n auto_commit,\n script_path,\n is_flow,\n mode,\n edited_by,\n permissioned_as,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, now(), $14, $15, $16\n )\n ",
"describe": {
"columns": [],
"parameters": {
@@ -36,5 +36,5 @@
},
"nullable": []
},
"hash": "5dd6315ec270c268e905262e4b0a920837354d91a0ae16b1236c1267da71765f"
"hash": "a0a545fda5f3ebea0113d5daaf13358c964d9fb0f41bf2a1c834305b4d2398f2"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE kafka_trigger\n SET\n kafka_resource_path = $1,\n group_id = $2,\n topics = $3,\n filters = $4,\n auto_offset_reset = $5,\n auto_commit = $6,\n script_path = $7,\n path = $8,\n is_flow = $9,\n edited_by = $10,\n email = $11,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $14,\n error_handler_args = $15,\n retry = $16\n WHERE\n workspace_id = $12 AND path = $13\n ",
"query": "\n UPDATE kafka_trigger\n SET\n kafka_resource_path = $1,\n group_id = $2,\n topics = $3,\n filters = $4,\n auto_offset_reset = $5,\n auto_commit = $6,\n script_path = $7,\n path = $8,\n is_flow = $9,\n edited_by = $10,\n permissioned_as = $11,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $14,\n error_handler_args = $15,\n retry = $16\n WHERE\n workspace_id = $12 AND path = $13\n ",
"describe": {
"columns": [],
"parameters": {
@@ -25,5 +25,5 @@
},
"nullable": []
},
"hash": "072e5ab78f929c6b7264f98c1588cb24cc635836276ee6faa2438f494bfbce04"
"hash": "a37cfc632dd37cf37c06743239b5ebc784e5da5ee25d47af187a75220d8fded7"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO gcp_trigger (\n gcp_resource_path,\n subscription_id,\n topic_id,\n delivery_type,\n delivery_config,\n workspace_id,\n path,\n script_path,\n is_flow,\n email,\n mode,\n edited_by,\n error_handler_path,\n error_handler_args,\n retry,\n auto_acknowledge_msg,\n ack_deadline\n )\n VALUES (\n $1,\n $2,\n $3,\n $4,\n $5,\n $6,\n $7,\n $8,\n $9,\n $10,\n $11,\n $12,\n $13,\n $14,\n $15,\n $16,\n $17\n )",
"query": "\n INSERT INTO gcp_trigger (\n gcp_resource_path,\n subscription_id,\n topic_id,\n delivery_type,\n delivery_config,\n workspace_id,\n path,\n script_path,\n is_flow,\n permissioned_as,\n mode,\n edited_by,\n error_handler_path,\n error_handler_args,\n retry,\n auto_acknowledge_msg,\n ack_deadline\n )\n VALUES (\n $1,\n $2,\n $3,\n $4,\n $5,\n $6,\n $7,\n $8,\n $9,\n $10,\n $11,\n $12,\n $13,\n $14,\n $15,\n $16,\n $17\n )",
"describe": {
"columns": [],
"parameters": {
@@ -47,5 +47,5 @@
},
"nullable": []
},
"hash": "aa59a96bf2d7edfa7c550e66c4d52ddc8e84eacc633e361e49a5219d0bec94b9"
"hash": "a5b6ca174b7a3be1b6507ef40b9cc67d85089199c900afc6668f2336b803562e"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO postgres_trigger (\n path, script_path, is_flow, workspace_id, edited_by, email,\n postgres_resource_path, replication_slot_name, publication_name\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ",
"query": "\n INSERT INTO postgres_trigger (\n path, script_path, is_flow, workspace_id, edited_by, permissioned_as,\n postgres_resource_path, replication_slot_name, publication_name\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -18,5 +18,5 @@
},
"nullable": []
},
"hash": "0300afc35a880eef163dfdfd9d5299fac14562ee8595c792f3c30d042fa2d3eb"
"hash": "a8245a3b29927c26894be884c38e4d848674d5318e20bb5fc6c4261da25744ca"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE email_trigger\n SET\n script_path = $1,\n path = $2,\n is_flow = $3,\n edited_by = $4,\n email = $5,\n edited_at = now(),\n error_handler_path = $6,\n error_handler_args = $7,\n retry = $8,\n mode = $9\n WHERE\n workspace_id = $10 AND path = $11\n ",
"query": "\n UPDATE email_trigger\n SET\n script_path = $1,\n path = $2,\n is_flow = $3,\n edited_by = $4,\n permissioned_as = $5,\n edited_at = now(),\n error_handler_path = $6,\n error_handler_args = $7,\n retry = $8,\n mode = $9\n WHERE\n workspace_id = $10 AND path = $11\n ",
"describe": {
"columns": [],
"parameters": {
@@ -31,5 +31,5 @@
},
"nullable": []
},
"hash": "b5cda8eb32384f315689001f45676d4bf44cb4397dd0a722e7c9d035b58a09c1"
"hash": "aa7d3d159943250787a8ebc964cb46e5fbb157d1ec0684789a8d37eff1adf9a5"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE schedule SET\n enabled = $1,\n email = $2\n WHERE path = $3 AND workspace_id = $4\n RETURNING\n workspace_id,\n path,\n edited_by,\n edited_at,\n schedule,\n timezone,\n enabled,\n script_path,\n is_flow,\n args AS \"args: _\",\n extra_perms,\n email,\n error,\n on_failure,\n on_failure_times,\n on_failure_exact,\n on_failure_extra_args AS \"on_failure_extra_args: _\",\n on_recovery,\n on_recovery_times,\n on_recovery_extra_args AS \"on_recovery_extra_args: _\",\n on_success,\n on_success_extra_args AS \"on_success_extra_args: _\",\n ws_error_handler_muted,\n retry,\n no_flow_overlap,\n summary,\n description,\n tag,\n paused_until,\n cron_version,\n dynamic_skip\n ",
"query": "\n UPDATE schedule SET\n enabled = $1,\n email = $2\n WHERE path = $3 AND workspace_id = $4\n RETURNING\n workspace_id,\n path,\n edited_by,\n edited_at,\n schedule,\n timezone,\n enabled,\n script_path,\n is_flow,\n args AS \"args: _\",\n extra_perms,\n email,\n permissioned_as,\n error,\n on_failure,\n on_failure_times,\n on_failure_exact,\n on_failure_extra_args AS \"on_failure_extra_args: _\",\n on_recovery,\n on_recovery_times,\n on_recovery_extra_args AS \"on_recovery_extra_args: _\",\n on_success,\n on_success_extra_args AS \"on_success_extra_args: _\",\n ws_error_handler_muted,\n retry,\n no_flow_overlap,\n summary,\n description,\n tag,\n paused_until,\n cron_version,\n dynamic_skip\n ",
"describe": {
"columns": [
{
@@ -65,96 +65,101 @@
},
{
"ordinal": 12,
"name": "permissioned_as",
"type_info": "Varchar"
},
{
"ordinal": 13,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 13,
"ordinal": 14,
"name": "on_failure",
"type_info": "Varchar"
},
{
"ordinal": 14,
"ordinal": 15,
"name": "on_failure_times",
"type_info": "Int4"
},
{
"ordinal": 15,
"ordinal": 16,
"name": "on_failure_exact",
"type_info": "Bool"
},
{
"ordinal": 16,
"ordinal": 17,
"name": "on_failure_extra_args: _",
"type_info": "Jsonb"
},
{
"ordinal": 17,
"ordinal": 18,
"name": "on_recovery",
"type_info": "Varchar"
},
{
"ordinal": 18,
"ordinal": 19,
"name": "on_recovery_times",
"type_info": "Int4"
},
{
"ordinal": 19,
"ordinal": 20,
"name": "on_recovery_extra_args: _",
"type_info": "Jsonb"
},
{
"ordinal": 20,
"ordinal": 21,
"name": "on_success",
"type_info": "Varchar"
},
{
"ordinal": 21,
"ordinal": 22,
"name": "on_success_extra_args: _",
"type_info": "Jsonb"
},
{
"ordinal": 22,
"ordinal": 23,
"name": "ws_error_handler_muted",
"type_info": "Bool"
},
{
"ordinal": 23,
"ordinal": 24,
"name": "retry",
"type_info": "Jsonb"
},
{
"ordinal": 24,
"ordinal": 25,
"name": "no_flow_overlap",
"type_info": "Bool"
},
{
"ordinal": 25,
"ordinal": 26,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 26,
"ordinal": 27,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 27,
"ordinal": 28,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 28,
"ordinal": 29,
"name": "paused_until",
"type_info": "Timestamptz"
},
{
"ordinal": 29,
"ordinal": 30,
"name": "cron_version",
"type_info": "Text"
},
{
"ordinal": 30,
"ordinal": 31,
"name": "dynamic_skip",
"type_info": "Varchar"
}
@@ -180,6 +185,7 @@
true,
false,
false,
false,
true,
true,
true,
@@ -201,5 +207,5 @@
true
]
},
"hash": "7927b80ce75d99b2a30f6b29196af000578a3c166509f032d14452cc637d884f"
"hash": "ad96768ff61fab1cfb9421683bb13b64f5f157fafe114d50fb910dc36ebe0f91"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO http_trigger (\n path, route_path, route_path_key, script_path, is_flow,\n workspace_id, edited_by, email, http_method,\n authentication_method, is_static_website, workspaced_route,\n wrap_body, raw_string\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::http_method,\n $10::authentication_method, $11, $12, $13, $14)\n ",
"query": "\n INSERT INTO http_trigger (\n path, route_path, route_path_key, script_path, is_flow,\n workspace_id, edited_by, permissioned_as, http_method,\n authentication_method, is_static_website, workspaced_route,\n wrap_body, raw_string\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::http_method,\n $10::authentication_method, $11, $12, $13, $14)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -50,5 +50,5 @@
},
"nullable": []
},
"hash": "74c11f5a0315424574fe3e7429f967f7b94f9d9db7be628f9cb411d789085711"
"hash": "ae8d7c07a4027bccf404951e75992bf3048c58bdf6fc67cb344eeb6cf72a9156"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO mqtt_trigger (\n mqtt_resource_path,\n subscribe_topics,\n client_version,\n client_id,\n v3_config,\n v5_config,\n workspace_id,\n path,\n script_path,\n is_flow,\n email,\n mode,\n edited_by,\n error_handler_path,\n error_handler_args,\n retry\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16\n )",
"query": "\n INSERT INTO mqtt_trigger (\n mqtt_resource_path,\n subscribe_topics,\n client_version,\n client_id,\n v3_config,\n v5_config,\n workspace_id,\n path,\n script_path,\n is_flow,\n permissioned_as,\n mode,\n edited_by,\n error_handler_path,\n error_handler_args,\n retry\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16\n )",
"describe": {
"columns": [],
"parameters": {
@@ -46,5 +46,5 @@
},
"nullable": []
},
"hash": "e25aa749cfc9f0bb1649d162e36f2c0ce2187e47d745aa5ba96f9453c722750c"
"hash": "c301e668a5b88741e6c20700909be55518fabafcadaf6458fdcf1095643534c5"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE\n http_trigger\n SET\n wrap_body = $1,\n raw_string = $2,\n authentication_resource_path = $3,\n script_path = $4,\n path = $5,\n is_flow = $6,\n mode = $7,\n http_method = $8,\n static_asset_config = $9,\n edited_by = $10,\n email = $11,\n request_type = $12,\n authentication_method = $13,\n summary = $14,\n description = $15,\n edited_at = now(),\n is_static_website = $16,\n error_handler_path = $17,\n error_handler_args = $18,\n retry = $19\n WHERE\n workspace_id = $20 AND\n path = $21\n ",
"query": "\n UPDATE\n http_trigger\n SET\n wrap_body = $1,\n raw_string = $2,\n authentication_resource_path = $3,\n script_path = $4,\n path = $5,\n is_flow = $6,\n mode = $7,\n http_method = $8,\n static_asset_config = $9,\n edited_by = $10,\n permissioned_as = $11,\n request_type = $12,\n authentication_method = $13,\n summary = $14,\n description = $15,\n edited_at = now(),\n is_static_website = $16,\n error_handler_path = $17,\n error_handler_args = $18,\n retry = $19\n WHERE\n workspace_id = $20 AND\n path = $21\n ",
"describe": {
"columns": [],
"parameters": {
@@ -79,5 +79,5 @@
},
"nullable": []
},
"hash": "888f7e82c25b677172a276d042ae7066113f2a522068f04c8cee0895a49b787f"
"hash": "c5b650748a4ac0d0a04e287d1b3c89a5cc2af0439fe3c3c5829fd0776ec0734e"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE\n websocket_trigger\n SET\n url = $1,\n script_path = $2,\n path = $3,\n is_flow = $4,\n filters = $5,\n initial_messages = $6,\n url_runnable_args = $7,\n edited_by = $8,\n email = $9,\n can_return_message = $10,\n can_return_error_result = $11,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $14,\n error_handler_args = $15,\n retry = $16\n WHERE\n workspace_id = $12 AND path = $13\n ",
"query": "\n UPDATE\n websocket_trigger\n SET\n url = $1,\n script_path = $2,\n path = $3,\n is_flow = $4,\n filters = $5,\n initial_messages = $6,\n url_runnable_args = $7,\n edited_by = $8,\n permissioned_as = $9,\n can_return_message = $10,\n can_return_error_result = $11,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $14,\n error_handler_args = $15,\n retry = $16\n WHERE\n workspace_id = $12 AND path = $13\n ",
"describe": {
"columns": [],
"parameters": {
@@ -25,5 +25,5 @@
},
"nullable": []
},
"hash": "6c3c38ed5a0e6de0c97954fe4581daa788a7551b4a00dd33f78d36e246b98dd7"
"hash": "c7aed7fe3b6774477d403bc3e7fcbce7cdbdd1feb553718cbde60bb8ccff4733"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO kafka_trigger (\n path, kafka_resource_path, topics, group_id,\n script_path, is_flow, workspace_id, edited_by, email\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ",
"query": "\n INSERT INTO kafka_trigger (\n path, kafka_resource_path, topics, group_id,\n script_path, is_flow, workspace_id, edited_by, permissioned_as\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -18,5 +18,5 @@
},
"nullable": []
},
"hash": "8c3cc09c1bbb6209467c75723dd02e97dddb99789422012e85e86c3151a9f2e9"
"hash": "ccd76be88fa9c11b3dc2e6d7711437ab3e02d8c3c10c53decc664533b8d04bc0"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n script_path, \n is_flow, \n workspace_id, \n mode as \"mode: _\",\n edited_by, \n email, \n path, \n error_handler_path as \"error_handler_path: _\", \n error_handler_args as \"error_handler_args: _\", \n retry as \"retry: _\" \n FROM email_trigger \n WHERE local_part = $1 \n AND workspaced_local_part = FALSE\n AND (mode = 'enabled'::TRIGGER_MODE OR mode = 'suspended'::TRIGGER_MODE)\n ",
"query": "\n SELECT \n script_path, \n is_flow, \n workspace_id, \n mode as \"mode: _\",\n permissioned_as,\n path,\n error_handler_path as \"error_handler_path: _\",\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM email_trigger\n WHERE local_part = $1\n AND workspaced_local_part = FALSE\n AND (mode = 'enabled'::TRIGGER_MODE OR mode = 'suspended'::TRIGGER_MODE)\n ",
"describe": {
"columns": [
{
@@ -36,31 +36,26 @@
},
{
"ordinal": 4,
"name": "edited_by",
"name": "permissioned_as",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 7,
"ordinal": 6,
"name": "error_handler_path: _",
"type_info": "Varchar"
},
{
"ordinal": 8,
"ordinal": 7,
"name": "error_handler_args: _",
"type_info": "Jsonb"
},
{
"ordinal": 9,
"ordinal": 8,
"name": "retry: _",
"type_info": "Jsonb"
}
@@ -77,11 +72,10 @@
false,
false,
false,
false,
true,
true,
true
]
},
"hash": "de656102e898ebc90e53af8d36c882cf47e798567bb733e8b528dcedc48ad4bf"
"hash": "cd410aa458ad23d47945084a0b9614babda8e6f9fe1ce71224773b2d5f307161"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE postgres_trigger\n SET\n postgres_resource_path = $1,\n replication_slot_name = $2,\n publication_name = $3,\n script_path = $4,\n path = $5,\n is_flow = $6,\n edited_by = $7,\n email = $8,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $11,\n error_handler_args = $12,\n retry = $13\n WHERE\n workspace_id = $9 AND path = $10\n ",
"query": "\n UPDATE postgres_trigger\n SET\n postgres_resource_path = $1,\n replication_slot_name = $2,\n publication_name = $3,\n script_path = $4,\n path = $5,\n is_flow = $6,\n edited_by = $7,\n permissioned_as = $8,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $11,\n error_handler_args = $12,\n retry = $13\n WHERE\n workspace_id = $9 AND path = $10\n ",
"describe": {
"columns": [],
"parameters": {
@@ -22,5 +22,5 @@
},
"nullable": []
},
"hash": "f4340e31adddc9a37bab64957b2b1b9b29f978c73f003dfbf81d5c487f666358"
"hash": "d082ff0fcc2bb02257ab82376d87ff8535dfcb7c2b1143b7016fa17bc42a595d"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE\n gcp_trigger\n SET\n gcp_resource_path = $1,\n subscription_id = $2,\n topic_id = $3,\n delivery_type = $4,\n delivery_config = $5,\n is_flow = $6,\n edited_by = $7,\n email = $8,\n script_path = $9,\n path = $10,\n mode = $11,\n edited_at = now(),\n error = NULL,\n server_id = NULL,\n error_handler_path = $14,\n error_handler_args = $15,\n retry = $16,\n auto_acknowledge_msg = $17,\n ack_deadline = $18\n WHERE\n workspace_id = $12 AND\n path = $13\n ",
"query": "\n UPDATE\n gcp_trigger\n SET\n gcp_resource_path = $1,\n subscription_id = $2,\n topic_id = $3,\n delivery_type = $4,\n delivery_config = $5,\n is_flow = $6,\n edited_by = $7,\n permissioned_as = $8,\n script_path = $9,\n path = $10,\n mode = $11,\n edited_at = now(),\n error = NULL,\n server_id = NULL,\n error_handler_path = $14,\n error_handler_args = $15,\n retry = $16,\n auto_acknowledge_msg = $17,\n ack_deadline = $18\n WHERE\n workspace_id = $12 AND\n path = $13\n ",
"describe": {
"columns": [],
"parameters": {
@@ -48,5 +48,5 @@
},
"nullable": []
},
"hash": "8642e4fc3efb011a8b7f98d2080a165ec4046c3a7172b5635b818c0fa133411f"
"hash": "d35300ab94202c181ee5d1e1c76ec9cf1044ff173375a82c529babe053acb24f"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO nats_trigger (\n workspace_id,\n path,\n nats_resource_path,\n subjects,\n stream_name,\n consumer_name,\n use_jetstream,\n script_path,\n is_flow,\n mode,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now(), $13, $14, $15\n )\n ",
"query": "\n INSERT INTO nats_trigger (\n workspace_id,\n path,\n nats_resource_path,\n subjects,\n stream_name,\n consumer_name,\n use_jetstream,\n script_path,\n is_flow,\n mode,\n edited_by,\n permissioned_as,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now(), $13, $14, $15\n )\n ",
"describe": {
"columns": [],
"parameters": {
@@ -35,5 +35,5 @@
},
"nullable": []
},
"hash": "1bf8dc01326ebf6b8faa04e418b781e37bb9cedd1a89bf71a969b6db8cace48e"
"hash": "d56a8a7291ce3141c06d79ef854ac2cf970c3e41a223e31d3991276742d2afe1"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO sqs_trigger (\n path, queue_url, aws_resource_path, script_path,\n is_flow, workspace_id, edited_by, email\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ",
"query": "\n INSERT INTO sqs_trigger (\n path, queue_url, aws_resource_path, script_path,\n is_flow, workspace_id, edited_by, permissioned_as\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -17,5 +17,5 @@
},
"nullable": []
},
"hash": "c659cfe81441bb1b62a9f23f5d9b7f5b1f354b6bb0a78d007114fed084cfe292"
"hash": "db9957ea9b5b326c27ff62f96a3981787a91ac587b1936c774de7a2a2094bf91"
}
@@ -1,11 +1,11 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email, edited_by FROM websocket_trigger WHERE path = $1 AND workspace_id = $2",
"query": "SELECT permissioned_as, edited_by FROM http_trigger WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"name": "permissioned_as",
"type_info": "Varchar"
},
{
@@ -25,5 +25,5 @@
false
]
},
"hash": "075d4749299af2cb81162bf396bec6aa89de43ec201c911196763e03e644ca7a"
"hash": "dbb16284b9dd98b9339816e43eebf0fef488102cd7b3fd38d8af3148545bf1a4"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO schedule (\n workspace_id, path, schedule, timezone, edited_by, script_path,\n is_flow, args, enabled, email,\n on_failure, on_failure_times, on_failure_exact, on_failure_extra_args,\n on_recovery, on_recovery_times, on_recovery_extra_args,\n on_success, on_success_extra_args,\n ws_error_handler_muted, retry, summary, no_flow_overlap,\n tag, paused_until, cron_version, description, dynamic_skip\n ) VALUES (\n $1, $2, $3, $4, $5, $6,\n $7, $8, $9, $10,\n $11, $12, $13, $14,\n $15, $16, $17,\n $18, $19,\n $20, $21, $22, $23,\n $24, $25, $26, $27, $28\n )\n RETURNING\n workspace_id,\n path,\n edited_by,\n edited_at,\n schedule,\n timezone,\n enabled,\n script_path,\n is_flow,\n args AS \"args: _\",\n extra_perms,\n email,\n error,\n on_failure,\n on_failure_times,\n on_failure_exact,\n on_failure_extra_args AS \"on_failure_extra_args: _\",\n on_recovery,\n on_recovery_times,\n on_recovery_extra_args AS \"on_recovery_extra_args: _\",\n on_success,\n on_success_extra_args AS \"on_success_extra_args: _\",\n ws_error_handler_muted,\n retry,\n no_flow_overlap,\n summary,\n description,\n tag,\n paused_until,\n cron_version,\n dynamic_skip\n ",
"query": "\n INSERT INTO schedule (\n workspace_id, path, schedule, timezone, edited_by, script_path,\n is_flow, args, enabled, email, permissioned_as,\n on_failure, on_failure_times, on_failure_exact, on_failure_extra_args,\n on_recovery, on_recovery_times, on_recovery_extra_args,\n on_success, on_success_extra_args,\n ws_error_handler_muted, retry, summary, no_flow_overlap,\n tag, paused_until, cron_version, description, dynamic_skip\n ) VALUES (\n $1, $2, $3, $4, $5, $6,\n $7, $8, $9, $10, $11,\n $12, $13, $14, $15,\n $16, $17, $18,\n $19, $20,\n $21, $22, $23, $24,\n $25, $26, $27, $28, $29\n )\n RETURNING\n workspace_id,\n path,\n edited_by,\n edited_at,\n schedule,\n timezone,\n enabled,\n script_path,\n is_flow,\n args AS \"args: _\",\n extra_perms,\n email,\n permissioned_as,\n error,\n on_failure,\n on_failure_times,\n on_failure_exact,\n on_failure_extra_args AS \"on_failure_extra_args: _\",\n on_recovery,\n on_recovery_times,\n on_recovery_extra_args AS \"on_recovery_extra_args: _\",\n on_success,\n on_success_extra_args AS \"on_success_extra_args: _\",\n ws_error_handler_muted,\n retry,\n no_flow_overlap,\n summary,\n description,\n tag,\n paused_until,\n cron_version,\n dynamic_skip\n ",
"describe": {
"columns": [
{
@@ -65,96 +65,101 @@
},
{
"ordinal": 12,
"name": "permissioned_as",
"type_info": "Varchar"
},
{
"ordinal": 13,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 13,
"ordinal": 14,
"name": "on_failure",
"type_info": "Varchar"
},
{
"ordinal": 14,
"ordinal": 15,
"name": "on_failure_times",
"type_info": "Int4"
},
{
"ordinal": 15,
"ordinal": 16,
"name": "on_failure_exact",
"type_info": "Bool"
},
{
"ordinal": 16,
"ordinal": 17,
"name": "on_failure_extra_args: _",
"type_info": "Jsonb"
},
{
"ordinal": 17,
"ordinal": 18,
"name": "on_recovery",
"type_info": "Varchar"
},
{
"ordinal": 18,
"ordinal": 19,
"name": "on_recovery_times",
"type_info": "Int4"
},
{
"ordinal": 19,
"ordinal": 20,
"name": "on_recovery_extra_args: _",
"type_info": "Jsonb"
},
{
"ordinal": 20,
"ordinal": 21,
"name": "on_success",
"type_info": "Varchar"
},
{
"ordinal": 21,
"ordinal": 22,
"name": "on_success_extra_args: _",
"type_info": "Jsonb"
},
{
"ordinal": 22,
"ordinal": 23,
"name": "ws_error_handler_muted",
"type_info": "Bool"
},
{
"ordinal": 23,
"ordinal": 24,
"name": "retry",
"type_info": "Jsonb"
},
{
"ordinal": 24,
"ordinal": 25,
"name": "no_flow_overlap",
"type_info": "Bool"
},
{
"ordinal": 25,
"ordinal": 26,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 26,
"ordinal": 27,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 27,
"ordinal": 28,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 28,
"ordinal": 29,
"name": "paused_until",
"type_info": "Timestamptz"
},
{
"ordinal": 29,
"ordinal": 30,
"name": "cron_version",
"type_info": "Text"
},
{
"ordinal": 30,
"ordinal": 31,
"name": "dynamic_skip",
"type_info": "Varchar"
}
@@ -172,6 +177,7 @@
"Bool",
"Varchar",
"Varchar",
"Varchar",
"Int4",
"Bool",
"Jsonb",
@@ -204,6 +210,7 @@
true,
false,
false,
false,
true,
true,
true,
@@ -225,5 +232,5 @@
true
]
},
"hash": "23e4c6e3dc6a48f702c2b26a6b1f94668e086caaa0093a3b685f87483513b0d2"
"hash": "dd20f94d560238096390371c98ded1f80825a11cd61c0bb431678ad9ab4a138e"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO sqs_trigger (\n workspace_id,\n path,\n queue_url,\n aws_resource_path,\n message_attributes,\n aws_auth_resource_type,\n script_path,\n is_flow,\n mode,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, now(), $12, $13, $14\n )\n ",
"query": "\n INSERT INTO sqs_trigger (\n workspace_id,\n path,\n queue_url,\n aws_resource_path,\n message_attributes,\n aws_auth_resource_type,\n script_path,\n is_flow,\n mode,\n edited_by,\n permissioned_as,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, now(), $12, $13, $14\n )\n ",
"describe": {
"columns": [],
"parameters": {
@@ -44,5 +44,5 @@
},
"nullable": []
},
"hash": "cad914554762b6be72b289814f77072600f8f1bc1fa73ef6f63972775aedf044"
"hash": "de4879b8e3002ca48005406c4e107df2663aca35b60e3c528f564da3114f0af1"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n script_path, \n is_flow, \n workspace_id, \n mode as \"mode: _\",\n edited_by, \n email, \n path, \n error_handler_path as \"error_handler_path: _\", \n error_handler_args as \"error_handler_args: _\", \n retry as \"retry: _\" \n FROM email_trigger \n WHERE workspace_id = $1 \n AND local_part = $2 \n AND (workspaced_local_part = TRUE OR $3 IS TRUE)\n AND (mode = 'enabled'::TRIGGER_MODE OR mode = 'suspended'::TRIGGER_MODE)\n ",
"query": "\n SELECT \n script_path, \n is_flow, \n workspace_id, \n mode as \"mode: _\",\n permissioned_as,\n path,\n error_handler_path as \"error_handler_path: _\",\n error_handler_args as \"error_handler_args: _\",\n retry as \"retry: _\"\n FROM email_trigger\n WHERE workspace_id = $1\n AND local_part = $2\n AND (workspaced_local_part = TRUE OR $3 IS TRUE)\n AND (mode = 'enabled'::TRIGGER_MODE OR mode = 'suspended'::TRIGGER_MODE)\n ",
"describe": {
"columns": [
{
@@ -36,31 +36,26 @@
},
{
"ordinal": 4,
"name": "edited_by",
"name": "permissioned_as",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "path",
"type_info": "Varchar"
},
{
"ordinal": 7,
"ordinal": 6,
"name": "error_handler_path: _",
"type_info": "Varchar"
},
{
"ordinal": 8,
"ordinal": 7,
"name": "error_handler_args: _",
"type_info": "Jsonb"
},
{
"ordinal": 9,
"ordinal": 8,
"name": "retry: _",
"type_info": "Jsonb"
}
@@ -79,11 +74,10 @@
false,
false,
false,
false,
true,
true,
true
]
},
"hash": "3aad6340ea1f8dc8411742ef5de5a77d5de903845da9e686aef804bc09db1687"
"hash": "e3f09fe777cbbc009ca1af31989b542ec2d585ef672f0e1496944f4dd0d082f4"
}
@@ -0,0 +1,18 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO kafka_trigger (path, kafka_resource_path, group_id, topics, script_path, is_flow, workspace_id, edited_by, permissioned_as) VALUES ($1, $2, $3, $4, $5, false, 'test-workspace', 'test-user', 'u/test-user')",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"VarcharArray",
"Varchar"
]
},
"nullable": []
},
"hash": "e998bb8195b4fafe6de20fb0bf4315b3a10db9aca72447b8b8afd9474baa0614"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO websocket_trigger (\n path, url, script_path, is_flow, workspace_id,\n edited_by, email, initial_messages\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ",
"query": "\n INSERT INTO websocket_trigger (\n path, url, script_path, is_flow, workspace_id,\n edited_by, permissioned_as, initial_messages\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -17,5 +17,5 @@
},
"nullable": []
},
"hash": "e485c82978d10379c6d1b7cd850f3bf764e6c5bf775f3d710ba36a79f5e424eb"
"hash": "f57a891d1b507ec6767099a388be7deb97a89168fd7f74040e73033075140655"
}
@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO sqs_trigger (path, queue_url, aws_resource_path, script_path, is_flow, workspace_id, edited_by, permissioned_as) VALUES ($1, $2, $3, $4, false, 'test-workspace', 'test-user', 'u/test-user')",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "f5f18e6c5b7376b2760915ee3c859fb2306f16c0c601668adaf30baf66f0fd0d"
}
+10
View File
@@ -15787,6 +15787,7 @@ dependencies = [
"uuid",
"windmill-api",
"windmill-api-agent-workers",
"windmill-api-auth",
"windmill-api-client",
"windmill-api-settings",
"windmill-autoscaling",
@@ -15800,6 +15801,14 @@ dependencies = [
"windmill-queue",
"windmill-runtime-nativets",
"windmill-test-utils",
"windmill-trigger",
"windmill-trigger-gcp",
"windmill-trigger-kafka",
"windmill-trigger-mqtt",
"windmill-trigger-nats",
"windmill-trigger-postgres",
"windmill-trigger-sqs",
"windmill-trigger-websocket",
"windmill-types",
"windmill-worker",
"windmill-worker-volumes",
@@ -16477,6 +16486,7 @@ dependencies = [
"cron",
"croner",
"datafusion",
"equivalent",
"futures",
"futures-core",
"gethostname",
+9
View File
@@ -260,6 +260,15 @@ windmill-dep-map.workspace = true
windmill-test-utils.workspace = true
windmill-worker-volumes.workspace = true
windmill-types.workspace = true
windmill-trigger.workspace = true
windmill-trigger-websocket.workspace = true
windmill-trigger-postgres.workspace = true
windmill-trigger-mqtt.workspace = true
windmill-trigger-kafka.workspace = true
windmill-trigger-nats.workspace = true
windmill-trigger-sqs.workspace = true
windmill-trigger-gcp.workspace = true
windmill-api-auth.workspace = true
axum.workspace = true
serde.workspace = true
windmill-api-client.workspace = true
+1 -1
View File
@@ -1 +1 @@
b3b8005d45e3f2aa7228c61d2e4ae86a17d89a30
bda51bc33bcb573659e7ff07d0a23ff6e23b8148
@@ -0,0 +1,30 @@
-- Revert: re-add email columns to trigger tables, drop permissioned_as
ALTER TABLE http_trigger ADD COLUMN email VARCHAR(255) NOT NULL DEFAULT '';
ALTER TABLE http_trigger DROP COLUMN permissioned_as;
ALTER TABLE websocket_trigger ADD COLUMN email VARCHAR(255) NOT NULL DEFAULT '';
ALTER TABLE websocket_trigger DROP COLUMN permissioned_as;
ALTER TABLE postgres_trigger ADD COLUMN email VARCHAR(255) NOT NULL DEFAULT '';
ALTER TABLE postgres_trigger DROP COLUMN permissioned_as;
ALTER TABLE mqtt_trigger ADD COLUMN email VARCHAR(255) NOT NULL DEFAULT '';
ALTER TABLE mqtt_trigger DROP COLUMN permissioned_as;
ALTER TABLE kafka_trigger ADD COLUMN email VARCHAR(255) NOT NULL DEFAULT '';
ALTER TABLE kafka_trigger DROP COLUMN permissioned_as;
ALTER TABLE nats_trigger ADD COLUMN email VARCHAR(255) NOT NULL DEFAULT '';
ALTER TABLE nats_trigger DROP COLUMN permissioned_as;
ALTER TABLE sqs_trigger ADD COLUMN email VARCHAR(255) NOT NULL DEFAULT '';
ALTER TABLE sqs_trigger DROP COLUMN permissioned_as;
ALTER TABLE gcp_trigger ADD COLUMN email VARCHAR(255) NOT NULL DEFAULT '';
ALTER TABLE gcp_trigger DROP COLUMN permissioned_as;
ALTER TABLE email_trigger ADD COLUMN email VARCHAR(255) NOT NULL DEFAULT '';
ALTER TABLE email_trigger DROP COLUMN permissioned_as;
ALTER TABLE schedule DROP COLUMN permissioned_as;
@@ -0,0 +1,64 @@
-- Add permissioned_as column to all trigger tables and schedule
-- permissioned_as stores 'u/{username}', 'g/{group}', or raw email
-- We add nullable first, populate, then set NOT NULL to avoid a DEFAULT '' that could mask bugs.
-- Trigger tables: add permissioned_as, drop email
ALTER TABLE http_trigger ADD COLUMN permissioned_as VARCHAR(255);
UPDATE http_trigger SET permissioned_as = CASE WHEN edited_by LIKE '%@%' THEN edited_by ELSE 'u/' || edited_by END;
ALTER TABLE http_trigger ALTER COLUMN permissioned_as SET NOT NULL;
ALTER TABLE http_trigger DROP COLUMN email;
ALTER TABLE websocket_trigger ADD COLUMN permissioned_as VARCHAR(255);
UPDATE websocket_trigger SET permissioned_as = CASE WHEN edited_by LIKE '%@%' THEN edited_by ELSE 'u/' || edited_by END;
ALTER TABLE websocket_trigger ALTER COLUMN permissioned_as SET NOT NULL;
ALTER TABLE websocket_trigger DROP COLUMN email;
ALTER TABLE postgres_trigger ADD COLUMN permissioned_as VARCHAR(255);
UPDATE postgres_trigger SET permissioned_as = CASE WHEN edited_by LIKE '%@%' THEN edited_by ELSE 'u/' || edited_by END;
ALTER TABLE postgres_trigger ALTER COLUMN permissioned_as SET NOT NULL;
ALTER TABLE postgres_trigger DROP COLUMN email;
ALTER TABLE mqtt_trigger ADD COLUMN permissioned_as VARCHAR(255);
UPDATE mqtt_trigger SET permissioned_as = CASE WHEN edited_by LIKE '%@%' THEN edited_by ELSE 'u/' || edited_by END;
ALTER TABLE mqtt_trigger ALTER COLUMN permissioned_as SET NOT NULL;
ALTER TABLE mqtt_trigger DROP COLUMN email;
ALTER TABLE kafka_trigger ADD COLUMN permissioned_as VARCHAR(255);
UPDATE kafka_trigger SET permissioned_as = CASE WHEN edited_by LIKE '%@%' THEN edited_by ELSE 'u/' || edited_by END;
ALTER TABLE kafka_trigger ALTER COLUMN permissioned_as SET NOT NULL;
ALTER TABLE kafka_trigger DROP COLUMN email;
ALTER TABLE nats_trigger ADD COLUMN permissioned_as VARCHAR(255);
UPDATE nats_trigger SET permissioned_as = CASE WHEN edited_by LIKE '%@%' THEN edited_by ELSE 'u/' || edited_by END;
ALTER TABLE nats_trigger ALTER COLUMN permissioned_as SET NOT NULL;
ALTER TABLE nats_trigger DROP COLUMN email;
ALTER TABLE sqs_trigger ADD COLUMN permissioned_as VARCHAR(255);
UPDATE sqs_trigger SET permissioned_as = CASE WHEN edited_by LIKE '%@%' THEN edited_by ELSE 'u/' || edited_by END;
ALTER TABLE sqs_trigger ALTER COLUMN permissioned_as SET NOT NULL;
ALTER TABLE sqs_trigger DROP COLUMN email;
ALTER TABLE gcp_trigger ADD COLUMN permissioned_as VARCHAR(255);
UPDATE gcp_trigger SET permissioned_as = CASE WHEN edited_by LIKE '%@%' THEN edited_by ELSE 'u/' || edited_by END;
ALTER TABLE gcp_trigger ALTER COLUMN permissioned_as SET NOT NULL;
ALTER TABLE gcp_trigger DROP COLUMN email;
ALTER TABLE email_trigger ADD COLUMN permissioned_as VARCHAR(255);
UPDATE email_trigger SET permissioned_as = CASE WHEN edited_by LIKE '%@%' THEN edited_by ELSE 'u/' || edited_by END;
ALTER TABLE email_trigger ALTER COLUMN permissioned_as SET NOT NULL;
ALTER TABLE email_trigger DROP COLUMN email;
-- Schedule table: add permissioned_as, keep email for backwards compat with old workers
-- For superadmin-owned schedules, use the email to find the actual username (since edited_by
-- may have been overwritten by a later edit). Otherwise use edited_by as the source.
ALTER TABLE schedule ADD COLUMN permissioned_as VARCHAR(255);
UPDATE schedule SET permissioned_as = CASE
WHEN EXISTS (
SELECT 1 FROM password p WHERE p.email = schedule.email AND p.super_admin = true
) THEN COALESCE(
'u/' || (SELECT u.username FROM usr u WHERE u.email = schedule.email AND u.workspace_id = schedule.workspace_id LIMIT 1),
schedule.email
)
ELSE CASE WHEN edited_by LIKE '%@%' THEN edited_by ELSE 'u/' || edited_by END
END;
ALTER TABLE schedule ALTER COLUMN permissioned_as SET NOT NULL;
+4 -4
View File
@@ -204,14 +204,14 @@ VALUES ('test-workspace', 'u/bob/my_variable', 'bob_value', false,
-- ============================================
-- Alice's personal schedule
INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, enabled, script_path, args, is_flow, email, timezone, extra_perms)
INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, enabled, script_path, args, is_flow, email, timezone, extra_perms, permissioned_as)
VALUES ('test-workspace', 'u/alice/my_schedule', 'alice', NOW(), '0 * * * *', false,
'u/alice/my_script', '{}', false, 'alice@windmill.dev', 'UTC', '{}');
'u/alice/my_script', '{}', false, 'alice@windmill.dev', 'UTC', '{}', 'u/alice');
-- Bob's personal schedule
INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, enabled, script_path, args, is_flow, email, timezone, extra_perms)
INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, enabled, script_path, args, is_flow, email, timezone, extra_perms, permissioned_as)
VALUES ('test-workspace', 'u/bob/my_schedule', 'bob', NOW(), '0 * * * *', false,
'u/bob/my_script', '{}', false, 'bob@windmill.dev', 'UTC', '{}');
'u/bob/my_script', '{}', false, 'bob@windmill.dev', 'UTC', '{}', 'u/bob');
-- ============================================
-- APPS (with app_versions)
+8
View File
@@ -65,6 +65,11 @@ INSERT INTO usr_to_group(workspace_id, group_, usr) VALUES
('test-workspace', 'wm_deployers', 'deployer-user')
ON CONFLICT DO NOTHING;
-- Superadmin NOT in workspace (only in password table, not in usr)
INSERT INTO password(email, password_hash, login_type, super_admin, verified, name, username)
VALUES ('superadmin-external@windmill.dev', 'not-a-real-hash', 'password', true, true, 'External Superadmin', 'superadmin-external')
ON CONFLICT DO NOTHING;
-- Tokens for all users (token_hash = sha256 hex, token_prefix = first 10 chars)
-- NOTE: plaintext `token` column is included for backward compat during transition.
-- Remove it once the `token` column is dropped from the schema.
@@ -80,6 +85,9 @@ ON CONFLICT DO NOTHING;
INSERT INTO token(token_hash, token_prefix, token, email, label, super_admin)
VALUES (encode(sha256('ORIGINAL_TOKEN'::bytea), 'hex'), 'ORIGINAL_T', 'ORIGINAL_TOKEN', 'original@windmill.dev', 'original token', false)
ON CONFLICT DO NOTHING;
INSERT INTO token(token_hash, token_prefix, token, email, label, super_admin)
VALUES (encode(sha256('EXTERNAL_SUPERADMIN_TOKEN'::bytea), 'hex'), 'EXTERNAL_S', 'EXTERNAL_SUPERADMIN_TOKEN', 'superadmin-external@windmill.dev', 'external superadmin token', true)
ON CONFLICT DO NOTHING;
GRANT ALL PRIVILEGES ON TABLE workspace_key TO windmill_admin;
GRANT ALL PRIVILEGES ON TABLE workspace_key TO windmill_user;
File diff suppressed because it is too large Load Diff
+385
View File
@@ -0,0 +1,385 @@
//! Tests that call trigger trait methods directly to verify
//! all dynamic SQL correctly references the permissioned_as column.
use serde_json::json;
use sqlx::{Pool, Postgres};
use windmill_common::DB;
use windmill_trigger::handler::TriggerCrud;
use windmill_trigger::listener::Listener;
use windmill_trigger::types::TriggerMode;
/// Helper to insert a minimal trigger row.
async fn insert_trigger(db: &DB, table: &str, path: &str, extra_cols: &str, extra_vals: &str) {
let sql = format!(
"INSERT INTO {} (path, script_path, is_flow, workspace_id, edited_by, permissioned_as{}) \
VALUES ($1, 'f/test/handler', false, 'test-workspace', 'test-user', 'u/test-user'{})",
table, extra_cols, extra_vals
);
sqlx::query(&sql).bind(path).execute(db).await.unwrap();
}
#[cfg(feature = "websocket")]
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_listener_query_websocket(db: Pool<Postgres>) -> anyhow::Result<()> {
insert_trigger(
&db,
"websocket_trigger",
"f/test/listener_ws",
", url",
", 'wss://example.com'",
)
.await;
let triggers = windmill_trigger_websocket::WebsocketTrigger
.fetch_enabled_unlistened_triggers(&db)
.await?;
assert!(triggers.iter().any(|t| t.path == "f/test/listener_ws"));
Ok(())
}
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_listener_query_postgres(db: Pool<Postgres>) -> anyhow::Result<()> {
insert_trigger(
&db,
"postgres_trigger",
"f/test/listener_pg",
", postgres_resource_path, replication_slot_name, publication_name",
", 'u/test/pg', 'slot', 'pub'",
)
.await;
let triggers = windmill_trigger_postgres::PostgresTrigger
.fetch_enabled_unlistened_triggers(&db)
.await?;
assert!(triggers.iter().any(|t| t.path == "f/test/listener_pg"));
Ok(())
}
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_listener_query_mqtt(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO mqtt_trigger (path, mqtt_resource_path, subscribe_topics, client_version, \
script_path, is_flow, workspace_id, edited_by, permissioned_as) \
VALUES ($1, $2, ARRAY[$3::jsonb], $4::mqtt_client_version, $5, false, 'test-workspace', 'test-user', 'u/test-user')"
)
.bind("f/test/listener_mqtt").bind("u/test/mqtt")
.bind(json!({"topic": "t", "qos": "qos0"})).bind("v5").bind("f/test/handler")
.execute(&db).await?;
let triggers = windmill_trigger_mqtt::MqttTrigger
.fetch_enabled_unlistened_triggers(&db)
.await?;
assert!(triggers.iter().any(|t| t.path == "f/test/listener_mqtt"));
Ok(())
}
#[cfg(feature = "private")]
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_listener_query_kafka(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
"INSERT INTO kafka_trigger (path, kafka_resource_path, group_id, topics, \
script_path, is_flow, workspace_id, edited_by, permissioned_as) \
VALUES ($1, $2, $3, $4, $5, false, 'test-workspace', 'test-user', 'u/test-user')",
"f/test/listener_kafka",
"u/test/kafka",
"grp",
&["topic"] as &[&str],
"f/test/handler"
)
.execute(&db)
.await?;
let triggers = windmill_trigger_kafka::KafkaTrigger
.fetch_enabled_unlistened_triggers(&db)
.await?;
assert!(triggers.iter().any(|t| t.path == "f/test/listener_kafka"));
Ok(())
}
#[cfg(feature = "private")]
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_listener_query_nats(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
"INSERT INTO nats_trigger (path, nats_resource_path, subjects, use_jetstream, \
script_path, is_flow, workspace_id, edited_by, permissioned_as) \
VALUES ($1, $2, $3, $4, $5, false, 'test-workspace', 'test-user', 'u/test-user')",
"f/test/listener_nats",
"u/test/nats",
&["subj"] as &[&str],
false,
"f/test/handler"
)
.execute(&db)
.await?;
let triggers = windmill_trigger_nats::NatsTrigger
.fetch_enabled_unlistened_triggers(&db)
.await?;
assert!(triggers.iter().any(|t| t.path == "f/test/listener_nats"));
Ok(())
}
#[cfg(feature = "private")]
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_listener_query_sqs(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
"INSERT INTO sqs_trigger (path, queue_url, aws_resource_path, \
script_path, is_flow, workspace_id, edited_by, permissioned_as) \
VALUES ($1, $2, $3, $4, false, 'test-workspace', 'test-user', 'u/test-user')",
"f/test/listener_sqs",
"https://sqs.example.com/q",
"u/test/aws",
"f/test/handler"
)
.execute(&db)
.await?;
let triggers = windmill_trigger_sqs::SqsTrigger
.fetch_enabled_unlistened_triggers(&db)
.await?;
assert!(triggers.iter().any(|t| t.path == "f/test/listener_sqs"));
Ok(())
}
#[cfg(feature = "private")]
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_listener_query_gcp(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO gcp_trigger (path, gcp_resource_path, topic_id, subscription_id, \
delivery_type, subscription_mode, \
script_path, is_flow, workspace_id, edited_by, permissioned_as) \
VALUES ($1, $2, $3, $4, $5::delivery_mode, $6::gcp_subscription_mode, $7, false, 'test-workspace', 'test-user', 'u/test-user')"
)
.bind("f/test/listener_gcp").bind("u/test/gcp").bind("topic").bind("sub")
.bind("pull").bind("existing").bind("f/test/handler")
.execute(&db).await?;
let triggers = windmill_trigger_gcp::GcpTrigger
.fetch_enabled_unlistened_triggers(&db)
.await?;
assert!(triggers.iter().any(|t| t.path == "f/test/listener_gcp"));
Ok(())
}
// ============================================================================
// Handler trait method tests (get_trigger_by_path, list_triggers, set_trigger_mode)
// ============================================================================
fn make_authed() -> windmill_api_auth::ApiAuthed {
windmill_api_auth::ApiAuthed {
email: "test@windmill.dev".to_string(),
username: "test-user".to_string(),
is_admin: true,
is_operator: false,
groups: vec![],
folders: vec![],
scopes: None,
username_override: None,
token_prefix: None,
}
}
/// Tests get_trigger_by_path, list_triggers, set_trigger_mode for websocket (server_state=true).
#[cfg(feature = "websocket")]
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_handler_queries_websocket(db: Pool<Postgres>) -> anyhow::Result<()> {
insert_trigger(
&db,
"websocket_trigger",
"f/test/handler_ws",
", url",
", 'wss://example.com'",
)
.await;
let handler = windmill_trigger_websocket::WebsocketTrigger;
let mut conn = db.acquire().await?;
let trigger = handler
.get_trigger_by_path(&mut *conn, "test-workspace", "f/test/handler_ws")
.await?;
assert_eq!(trigger.base.permissioned_as, "u/test-user");
let triggers = handler
.list_triggers(&mut *conn, "test-workspace", None)
.await?;
assert!(triggers.iter().any(|t| t.base.path == "f/test/handler_ws"));
let authed = make_authed();
let updated = handler
.set_trigger_mode(
&authed,
&mut *conn,
"test-workspace",
"f/test/handler_ws",
&TriggerMode::Disabled,
)
.await?;
assert!(updated);
Ok(())
}
/// Tests get_trigger_by_path, list_triggers, set_trigger_mode for mqtt (server_state=true).
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_handler_queries_mqtt(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO mqtt_trigger (path, mqtt_resource_path, subscribe_topics, client_version, \
script_path, is_flow, workspace_id, edited_by, permissioned_as) \
VALUES ($1, $2, ARRAY[$3::jsonb], $4::mqtt_client_version, $5, false, 'test-workspace', 'test-user', 'u/test-user')",
)
.bind("f/test/handler_mqtt").bind("u/test/mqtt")
.bind(json!({"topic": "t", "qos": "qos0"})).bind("v5").bind("f/test/handler")
.execute(&db).await?;
let handler = windmill_trigger_mqtt::MqttTrigger;
let mut conn = db.acquire().await?;
let trigger = handler
.get_trigger_by_path(&mut *conn, "test-workspace", "f/test/handler_mqtt")
.await?;
assert_eq!(trigger.base.permissioned_as, "u/test-user");
let authed = make_authed();
let updated = handler
.set_trigger_mode(
&authed,
&mut *conn,
"test-workspace",
"f/test/handler_mqtt",
&TriggerMode::Disabled,
)
.await?;
assert!(updated);
Ok(())
}
/// Tests handler queries for kafka trigger (EE only — OSS stub returns ()).
#[cfg(feature = "private")]
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_handler_queries_kafka(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
"INSERT INTO kafka_trigger (path, kafka_resource_path, group_id, topics, \
script_path, is_flow, workspace_id, edited_by, permissioned_as) \
VALUES ($1, $2, $3, $4, $5, false, 'test-workspace', 'test-user', 'u/test-user')",
"f/test/handler_kafka",
"u/test/kafka",
"grp",
&["topic"] as &[&str],
"f/test/handler"
)
.execute(&db)
.await?;
let handler = windmill_trigger_kafka::KafkaTrigger;
let mut conn = db.acquire().await?;
let trigger = handler
.get_trigger_by_path(&mut *conn, "test-workspace", "f/test/handler_kafka")
.await?;
assert_eq!(trigger.base.permissioned_as, "u/test-user");
Ok(())
}
/// Tests handler queries for postgres trigger.
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_handler_queries_postgres(db: Pool<Postgres>) -> anyhow::Result<()> {
insert_trigger(
&db,
"postgres_trigger",
"f/test/handler_pg",
", postgres_resource_path, replication_slot_name, publication_name",
", 'u/test/pg', 'slot', 'pub'",
)
.await;
let handler = windmill_trigger_postgres::PostgresTrigger;
let mut conn = db.acquire().await?;
let trigger = handler
.get_trigger_by_path(&mut *conn, "test-workspace", "f/test/handler_pg")
.await?;
assert_eq!(trigger.base.permissioned_as, "u/test-user");
Ok(())
}
/// Tests handler queries for nats trigger (EE only).
#[cfg(feature = "private")]
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_handler_queries_nats(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
"INSERT INTO nats_trigger (path, nats_resource_path, subjects, use_jetstream, \
script_path, is_flow, workspace_id, edited_by, permissioned_as) \
VALUES ($1, $2, $3, $4, $5, false, 'test-workspace', 'test-user', 'u/test-user')",
"f/test/handler_nats",
"u/test/nats",
&["subj"] as &[&str],
false,
"f/test/handler"
)
.execute(&db)
.await?;
let handler = windmill_trigger_nats::NatsTrigger;
let mut conn = db.acquire().await?;
let trigger = handler
.get_trigger_by_path(&mut *conn, "test-workspace", "f/test/handler_nats")
.await?;
assert_eq!(trigger.base.permissioned_as, "u/test-user");
Ok(())
}
/// Tests handler queries for sqs trigger (EE only).
#[cfg(feature = "private")]
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_handler_queries_sqs(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query!(
"INSERT INTO sqs_trigger (path, queue_url, aws_resource_path, \
script_path, is_flow, workspace_id, edited_by, permissioned_as) \
VALUES ($1, $2, $3, $4, false, 'test-workspace', 'test-user', 'u/test-user')",
"f/test/handler_sqs",
"https://sqs.example.com/q",
"u/test/aws",
"f/test/handler"
)
.execute(&db)
.await?;
let handler = windmill_trigger_sqs::SqsTrigger;
let mut conn = db.acquire().await?;
let trigger = handler
.get_trigger_by_path(&mut *conn, "test-workspace", "f/test/handler_sqs")
.await?;
assert_eq!(trigger.base.permissioned_as, "u/test-user");
Ok(())
}
/// Tests handler queries for gcp trigger (EE only).
#[cfg(feature = "private")]
#[sqlx::test(fixtures("preserve_on_behalf_of"))]
async fn test_handler_queries_gcp(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO gcp_trigger (path, gcp_resource_path, topic_id, subscription_id, \
delivery_type, subscription_mode, \
script_path, is_flow, workspace_id, edited_by, permissioned_as) \
VALUES ($1, $2, $3, $4, $5::delivery_mode, $6::gcp_subscription_mode, $7, false, 'test-workspace', 'test-user', 'u/test-user')",
)
.bind("f/test/handler_gcp").bind("u/test/gcp").bind("topic").bind("sub")
.bind("pull").bind("existing").bind("f/test/handler")
.execute(&db).await?;
let handler = windmill_trigger_gcp::GcpTrigger;
let mut conn = db.acquire().await?;
let trigger = handler
.get_trigger_by_path(&mut *conn, "test-workspace", "f/test/handler_gcp")
.await?;
assert_eq!(trigger.base.permissioned_as, "u/test-user");
Ok(())
}
+8 -13
View File
@@ -280,9 +280,7 @@ impl AuthCache {
folders,
scopes: None,
username_override,
token_prefix: Some(
safe_token_prefix(token),
),
token_prefix: Some(safe_token_prefix(token)),
})
} else {
let groups = vec![name.to_string()];
@@ -297,16 +295,17 @@ impl AuthCache {
.unwrap_or_default();
Some(ApiAuthed {
email: email,
username: format!("group-{name}"),
username: format!(
"{}{name}",
windmill_common::users::USERNAME_GROUP_PREFIX
),
is_admin: false,
groups,
is_operator: false,
folders,
scopes: None,
username_override,
token_prefix: Some(
safe_token_prefix(token),
),
token_prefix: Some(safe_token_prefix(token)),
})
}
} else {
@@ -369,9 +368,7 @@ impl AuthCache {
folders,
scopes,
username_override,
token_prefix: Some(
safe_token_prefix(token),
),
token_prefix: Some(safe_token_prefix(token)),
})
}
None if super_admin => Some(ApiAuthed {
@@ -383,9 +380,7 @@ impl AuthCache {
folders: vec![],
scopes,
username_override,
token_prefix: Some(
safe_token_prefix(token),
),
token_prefix: Some(safe_token_prefix(token)),
}),
None => None,
}
@@ -204,14 +204,14 @@ VALUES ('test-workspace', 'u/bob/my_variable', 'bob_value', false,
-- ============================================
-- Alice's personal schedule
INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, enabled, script_path, args, is_flow, email, timezone, extra_perms)
INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, enabled, script_path, args, is_flow, email, timezone, extra_perms, permissioned_as)
VALUES ('test-workspace', 'u/alice/my_schedule', 'alice', NOW(), '0 * * * *', false,
'u/alice/my_script', '{}', false, 'alice@windmill.dev', 'UTC', '{}');
'u/alice/my_script', '{}', false, 'alice@windmill.dev', 'UTC', '{}', 'u/alice');
-- Bob's personal schedule
INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, enabled, script_path, args, is_flow, email, timezone, extra_perms)
INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, enabled, script_path, args, is_flow, email, timezone, extra_perms, permissioned_as)
VALUES ('test-workspace', 'u/bob/my_schedule', 'bob', NOW(), '0 * * * *', false,
'u/bob/my_script', '{}', false, 'bob@windmill.dev', 'UTC', '{}');
'u/bob/my_script', '{}', false, 'bob@windmill.dev', 'UTC', '{}', 'u/bob');
-- ============================================
-- APPS (with app_versions)
@@ -158,7 +158,7 @@ async fn test_mqtt_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO mqtt_trigger (
path, mqtt_resource_path, subscribe_topics, client_version,
script_path, is_flow, workspace_id, edited_by, email
script_path, is_flow, workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, ARRAY[$3::jsonb], $4::mqtt_client_version, $5, $6, $7, $8, $9)
"#,
@@ -171,7 +171,7 @@ async fn test_mqtt_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
.bind(false)
.bind("test-workspace")
.bind("test-user")
.bind("test@windmill.dev")
.bind("u/test-user")
.execute(&db)
.await?;
@@ -240,7 +240,7 @@ async fn test_websocket_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO websocket_trigger (
path, url, script_path, is_flow, workspace_id,
edited_by, email, initial_messages
edited_by, permissioned_as, initial_messages
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
"#,
@@ -250,7 +250,7 @@ async fn test_websocket_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
&[json!({"type": "RawMessage", "content": "hello from e2e test"})]
as &[serde_json::Value],
)
@@ -338,7 +338,7 @@ async fn test_postgres_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
r#"
INSERT INTO postgres_trigger (
path, script_path, is_flow, workspace_id, edited_by, email,
path, script_path, is_flow, workspace_id, edited_by, permissioned_as,
postgres_resource_path, replication_slot_name, publication_name
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
@@ -349,7 +349,7 @@ async fn test_postgres_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
.bind(false)
.bind("test-workspace")
.bind("test-user")
.bind("test@windmill.dev")
.bind("u/test-user")
.bind("u/test-user/pg_res")
.bind(&slot_name)
.bind(&pub_name)
@@ -410,7 +410,7 @@ async fn test_kafka_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO kafka_trigger (
path, kafka_resource_path, topics, group_id,
script_path, is_flow, workspace_id, edited_by, email
script_path, is_flow, workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
"#,
@@ -422,7 +422,7 @@ async fn test_kafka_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
)
.execute(&db)
.await?;
@@ -502,7 +502,7 @@ async fn test_nats_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO nats_trigger (
path, nats_resource_path, subjects, script_path,
is_flow, workspace_id, edited_by, email, use_jetstream
is_flow, workspace_id, edited_by, permissioned_as, use_jetstream
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
"#,
@@ -513,7 +513,7 @@ async fn test_nats_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
false,
)
.execute(&db)
@@ -580,7 +580,7 @@ async fn test_sqs_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO sqs_trigger (
path, queue_url, aws_resource_path, script_path,
is_flow, workspace_id, edited_by, email
is_flow, workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
"#,
@@ -591,7 +591,7 @@ async fn test_sqs_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
)
.execute(&db)
.await?;
@@ -675,7 +675,7 @@ async fn test_gcp_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
INSERT INTO gcp_trigger (
path, gcp_resource_path, topic_id, subscription_id,
delivery_type, subscription_mode, script_path, is_flow,
workspace_id, edited_by, email
workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, $3, $4, $5::delivery_mode, $6::gcp_subscription_mode, $7, $8, $9, $10, $11)
"#,
@@ -690,7 +690,7 @@ async fn test_gcp_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
.bind(false)
.bind("test-workspace")
.bind("test-user")
.bind("test@windmill.dev")
.bind("u/test-user")
.execute(&db)
.await?;
@@ -557,7 +557,7 @@ async fn test_http_trigger_insert_and_query(db: Pool<Postgres>) -> anyhow::Resul
r#"
INSERT INTO http_trigger (
path, route_path, route_path_key, script_path, is_flow,
workspace_id, edited_by, email, http_method,
workspace_id, edited_by, permissioned_as, http_method,
authentication_method, is_static_website, workspaced_route,
wrap_body, raw_string
)
@@ -571,7 +571,7 @@ async fn test_http_trigger_insert_and_query(db: Pool<Postgres>) -> anyhow::Resul
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
"post" as _,
"none" as _,
false,
@@ -615,7 +615,7 @@ async fn test_http_trigger_multiple_methods(db: Pool<Postgres>) -> anyhow::Resul
r#"
INSERT INTO http_trigger (
path, route_path, route_path_key, script_path, is_flow,
workspace_id, edited_by, email, http_method,
workspace_id, edited_by, permissioned_as, http_method,
authentication_method, is_static_website, workspaced_route,
wrap_body, raw_string
)
@@ -629,7 +629,7 @@ async fn test_http_trigger_multiple_methods(db: Pool<Postgres>) -> anyhow::Resul
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
*method as _,
"none" as _,
false,
@@ -662,7 +662,7 @@ async fn test_http_trigger_authentication_methods(db: Pool<Postgres>) -> anyhow:
r#"
INSERT INTO http_trigger (
path, route_path, route_path_key, script_path, is_flow,
workspace_id, edited_by, email, http_method,
workspace_id, edited_by, permissioned_as, http_method,
authentication_method, is_static_website, workspaced_route,
wrap_body, raw_string
)
@@ -676,7 +676,7 @@ async fn test_http_trigger_authentication_methods(db: Pool<Postgres>) -> anyhow:
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
"get" as _,
*auth as _,
false,
@@ -710,7 +710,7 @@ async fn test_http_trigger_update(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO http_trigger (
path, route_path, route_path_key, script_path, is_flow,
workspace_id, edited_by, email, http_method,
workspace_id, edited_by, permissioned_as, http_method,
authentication_method, is_static_website, workspaced_route,
wrap_body, raw_string
)
@@ -724,7 +724,7 @@ async fn test_http_trigger_update(db: Pool<Postgres>) -> anyhow::Result<()> {
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
"get" as _,
"none" as _,
false,
@@ -763,7 +763,7 @@ async fn test_http_trigger_delete(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO http_trigger (
path, route_path, route_path_key, script_path, is_flow,
workspace_id, edited_by, email, http_method,
workspace_id, edited_by, permissioned_as, http_method,
authentication_method, is_static_website, workspaced_route,
wrap_body, raw_string
)
@@ -777,7 +777,7 @@ async fn test_http_trigger_delete(db: Pool<Postgres>) -> anyhow::Result<()> {
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
"get" as _,
"none" as _,
false,
@@ -818,7 +818,7 @@ async fn test_http_trigger_mode_filtering(db: Pool<Postgres>) -> anyhow::Result<
r#"
INSERT INTO http_trigger (
path, route_path, route_path_key, script_path, is_flow,
workspace_id, edited_by, email, http_method,
workspace_id, edited_by, permissioned_as, http_method,
authentication_method, is_static_website, workspaced_route,
wrap_body, raw_string, mode
)
@@ -832,7 +832,7 @@ async fn test_http_trigger_mode_filtering(db: Pool<Postgres>) -> anyhow::Result<
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
"get" as _,
"none" as _,
false,
@@ -872,7 +872,7 @@ async fn test_websocket_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()>
r#"
INSERT INTO websocket_trigger (
path, url, script_path, is_flow, workspace_id,
edited_by, email
edited_by, permissioned_as
)
VALUES ($1, $2, $3, $4, $5, $6, $7)
"#,
@@ -882,7 +882,7 @@ async fn test_websocket_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()>
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
)
.execute(&db)
.await?;
@@ -911,7 +911,7 @@ async fn test_kafka_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO kafka_trigger (
path, kafka_resource_path, topics, group_id, script_path,
is_flow, workspace_id, edited_by, email
is_flow, workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
"#,
@@ -923,7 +923,7 @@ async fn test_kafka_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
)
.execute(&db)
.await?;
@@ -958,7 +958,7 @@ async fn test_kafka_trigger_insert_auto_commit_disabled(db: Pool<Postgres>) -> a
r#"
INSERT INTO kafka_trigger (
path, kafka_resource_path, topics, group_id, script_path,
is_flow, workspace_id, edited_by, email, auto_commit
is_flow, workspace_id, edited_by, permissioned_as, auto_commit
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
"#,
@@ -970,7 +970,7 @@ async fn test_kafka_trigger_insert_auto_commit_disabled(db: Pool<Postgres>) -> a
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
false,
)
.execute(&db)
@@ -998,7 +998,7 @@ async fn test_postgres_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()>
sqlx::query!(
r#"
INSERT INTO postgres_trigger (
path, script_path, is_flow, workspace_id, edited_by, email,
path, script_path, is_flow, workspace_id, edited_by, permissioned_as,
postgres_resource_path, replication_slot_name, publication_name
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
@@ -1008,7 +1008,7 @@ async fn test_postgres_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()>
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
"u/admin/pg_resource",
"test_slot",
"test_publication",
@@ -1042,7 +1042,7 @@ async fn test_nats_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO nats_trigger (
path, nats_resource_path, subjects, script_path,
is_flow, workspace_id, edited_by, email, use_jetstream
is_flow, workspace_id, edited_by, permissioned_as, use_jetstream
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
"#,
@@ -1053,7 +1053,7 @@ async fn test_nats_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
false,
)
.execute(&db)
@@ -1085,7 +1085,7 @@ async fn test_sqs_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO sqs_trigger (
path, queue_url, aws_resource_path, script_path,
is_flow, workspace_id, edited_by, email
is_flow, workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
"#,
@@ -1096,7 +1096,7 @@ async fn test_sqs_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
)
.execute(&db)
.await?;
@@ -1136,7 +1136,7 @@ async fn test_trigger_server_state_tracking(db: Pool<Postgres>) -> anyhow::Resul
r#"
INSERT INTO websocket_trigger (
path, url, script_path, is_flow, workspace_id,
edited_by, email, server_id, error
edited_by, permissioned_as, server_id, error
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
"#,
@@ -1146,7 +1146,7 @@ async fn test_trigger_server_state_tracking(db: Pool<Postgres>) -> anyhow::Resul
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
"server-abc-123",
"connection refused",
)
@@ -1194,7 +1194,7 @@ async fn test_trigger_mode_filtering(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO websocket_trigger (
path, url, script_path, is_flow, workspace_id,
edited_by, email, mode
edited_by, permissioned_as, mode
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::trigger_mode)
"#,
@@ -1204,7 +1204,7 @@ async fn test_trigger_mode_filtering(db: Pool<Postgres>) -> anyhow::Result<()> {
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
*mode as _,
)
.execute(&db)
@@ -1276,9 +1276,9 @@ async fn test_schedule_insert_and_query(db: Pool<Postgres>) -> anyhow::Result<()
r#"
INSERT INTO schedule (
workspace_id, path, edited_by, schedule, enabled,
script_path, is_flow, email, timezone
script_path, is_flow, email, timezone, permissioned_as
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
"#,
"test-workspace",
"f/test/my_schedule",
@@ -1289,6 +1289,7 @@ async fn test_schedule_insert_and_query(db: Pool<Postgres>) -> anyhow::Result<()
false,
"test@windmill.dev",
"UTC",
"u/test-user",
)
.execute(&db)
.await?;
@@ -1324,7 +1325,7 @@ async fn test_mqtt_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO mqtt_trigger (
path, mqtt_resource_path, subscribe_topics, client_version,
script_path, is_flow, workspace_id, edited_by, email
script_path, is_flow, workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, ARRAY[$3::jsonb], $4::mqtt_client_version, $5, $6, $7, $8, $9)
"#,
@@ -1337,7 +1338,7 @@ async fn test_mqtt_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
.bind(false)
.bind("test-workspace")
.bind("test-user")
.bind("test@windmill.dev")
.bind("u/test-user")
.execute(&db)
.await?;
@@ -1368,7 +1369,7 @@ async fn test_mqtt_trigger_update(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO mqtt_trigger (
path, mqtt_resource_path, subscribe_topics, client_version,
script_path, is_flow, workspace_id, edited_by, email
script_path, is_flow, workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, ARRAY[$3::jsonb], $4::mqtt_client_version, $5, $6, $7, $8, $9)
"#,
@@ -1381,7 +1382,7 @@ async fn test_mqtt_trigger_update(db: Pool<Postgres>) -> anyhow::Result<()> {
.bind(false)
.bind("test-workspace")
.bind("test-user")
.bind("test@windmill.dev")
.bind("u/test-user")
.execute(&db)
.await?;
@@ -1413,7 +1414,7 @@ async fn test_mqtt_trigger_delete(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO mqtt_trigger (
path, mqtt_resource_path, subscribe_topics, client_version,
script_path, is_flow, workspace_id, edited_by, email
script_path, is_flow, workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, ARRAY[$3::jsonb], $4::mqtt_client_version, $5, $6, $7, $8, $9)
"#,
@@ -1426,7 +1427,7 @@ async fn test_mqtt_trigger_delete(db: Pool<Postgres>) -> anyhow::Result<()> {
.bind(false)
.bind("test-workspace")
.bind("test-user")
.bind("test@windmill.dev")
.bind("u/test-user")
.execute(&db)
.await?;
@@ -1462,7 +1463,7 @@ async fn test_gcp_trigger_insert_pull(db: Pool<Postgres>) -> anyhow::Result<()>
INSERT INTO gcp_trigger (
path, gcp_resource_path, topic_id, subscription_id,
delivery_type, subscription_mode, script_path, is_flow,
workspace_id, edited_by, email
workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, $3, $4, $5::delivery_mode, $6::gcp_subscription_mode, $7, $8, $9, $10, $11)
"#,
@@ -1477,7 +1478,7 @@ async fn test_gcp_trigger_insert_pull(db: Pool<Postgres>) -> anyhow::Result<()>
.bind(false)
.bind("test-workspace")
.bind("test-user")
.bind("test@windmill.dev")
.bind("u/test-user")
.execute(&db)
.await?;
@@ -1513,7 +1514,7 @@ async fn test_gcp_trigger_insert_push(db: Pool<Postgres>) -> anyhow::Result<()>
INSERT INTO gcp_trigger (
path, gcp_resource_path, topic_id, subscription_id,
delivery_type, delivery_config, subscription_mode,
script_path, is_flow, workspace_id, edited_by, email
script_path, is_flow, workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, $3, $4, $5::delivery_mode, $6::jsonb, $7::gcp_subscription_mode, $8, $9, $10, $11, $12)
"#,
@@ -1529,7 +1530,7 @@ async fn test_gcp_trigger_insert_push(db: Pool<Postgres>) -> anyhow::Result<()>
.bind(false)
.bind("test-workspace")
.bind("test-user")
.bind("test@windmill.dev")
.bind("u/test-user")
.execute(&db)
.await?;
@@ -1562,7 +1563,7 @@ async fn test_gcp_trigger_unique_constraint(db: Pool<Postgres>) -> anyhow::Resul
INSERT INTO gcp_trigger (
path, gcp_resource_path, topic_id, subscription_id,
delivery_type, subscription_mode, script_path, is_flow,
workspace_id, edited_by, email
workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, $3, $4, $5::delivery_mode, $6::gcp_subscription_mode, $7, $8, $9, $10, $11)
"#;
@@ -1578,7 +1579,7 @@ async fn test_gcp_trigger_unique_constraint(db: Pool<Postgres>) -> anyhow::Resul
.bind(false)
.bind("test-workspace")
.bind("test-user")
.bind("test@windmill.dev")
.bind("u/test-user")
.execute(&db)
.await?;
@@ -1594,7 +1595,7 @@ async fn test_gcp_trigger_unique_constraint(db: Pool<Postgres>) -> anyhow::Resul
.bind(false)
.bind("test-workspace")
.bind("test-user")
.bind("test@windmill.dev")
.bind("u/test-user")
.execute(&db)
.await;
@@ -1616,7 +1617,7 @@ async fn test_email_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO email_trigger (
path, local_part, workspaced_local_part, script_path,
is_flow, workspace_id, edited_by, email
is_flow, workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
"#,
@@ -1627,7 +1628,7 @@ async fn test_email_trigger_insert(db: Pool<Postgres>) -> anyhow::Result<()> {
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
)
.execute(&db)
.await?;
@@ -1659,7 +1660,7 @@ async fn test_email_trigger_update(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO email_trigger (
path, local_part, workspaced_local_part, script_path,
is_flow, workspace_id, edited_by, email
is_flow, workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
"#,
@@ -1670,7 +1671,7 @@ async fn test_email_trigger_update(db: Pool<Postgres>) -> anyhow::Result<()> {
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
)
.execute(&db)
.await?;
@@ -1705,7 +1706,7 @@ async fn test_email_trigger_delete(db: Pool<Postgres>) -> anyhow::Result<()> {
r#"
INSERT INTO email_trigger (
path, local_part, workspaced_local_part, script_path,
is_flow, workspace_id, edited_by, email
is_flow, workspace_id, edited_by, permissioned_as
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
"#,
@@ -1716,7 +1717,7 @@ async fn test_email_trigger_delete(db: Pool<Postgres>) -> anyhow::Result<()> {
false,
"test-workspace",
"test-user",
"test@windmill.dev",
"u/test-user",
)
.execute(&db)
.await?;
+77 -62
View File
@@ -33,42 +33,22 @@ use windmill_common::{
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
use windmill_queue::schedule::push_scheduled_job;
/// Resolves the email to use for a schedule based on preservation settings.
/// When preserving, looks up the email from the provided username.
async fn resolve_email(
username: Option<&String>,
preserve_email: Option<bool>,
authed: &ApiAuthed,
db: &DB,
w_id: &str,
) -> Result<String> {
if let Some(username) = username {
if preserve_email.unwrap_or(false) && can_preserve_on_behalf_of(authed) {
let email = sqlx::query_scalar!(
"SELECT email FROM usr WHERE username = $1 AND workspace_id = $2",
username,
w_id
)
.fetch_optional(db)
.await?;
if let Some(email) = email {
return Ok(email);
}
}
}
Ok(authed.email.clone())
}
fn resolve_edited_by(
username: Option<&String>,
preserve_edited_by: Option<bool>,
/// Resolves the permissioned_as value for a schedule.
/// When preserving, uses the provided permissioned_as value directly.
fn resolve_permissioned_as(
permissioned_as: Option<&String>,
preserve_permissioned_as: Option<bool>,
authed: &ApiAuthed,
) -> String {
if let Some(username) = username {
if preserve_edited_by.unwrap_or(false) && can_preserve_on_behalf_of(authed) {
return username.clone();
if let Some(permissioned_as) = permissioned_as {
if preserve_permissioned_as.unwrap_or(false) && can_preserve_on_behalf_of(authed) {
return permissioned_as.clone();
}
}
windmill_common::users::username_to_permissioned_as(&authed.username)
}
fn resolve_edited_by(authed: &ApiAuthed) -> String {
authed.username.clone()
}
@@ -117,8 +97,8 @@ pub struct NewSchedule {
pub paused_until: Option<DateTime<Utc>>,
pub cron_version: Option<String>,
pub dynamic_skip: Option<String>,
pub email: Option<String>,
pub preserve_email: Option<bool>,
pub permissioned_as: Option<String>,
pub preserve_permissioned_as: Option<bool>,
}
#[derive(Serialize, Deserialize)]
@@ -244,14 +224,26 @@ async fn create_schedule(
validate_dynamic_skip(&mut tx, &w_id, handler_path).await?;
}
let resolved_edited_by = resolve_edited_by(ns.email.as_ref(), ns.preserve_email, &authed);
let resolved_edited_by = resolve_edited_by(&authed);
let resolved_permissioned_as = resolve_permissioned_as(
ns.permissioned_as.as_ref(),
ns.preserve_permissioned_as,
&authed,
);
// email is still written for backwards compat with old workers that don't know about permissioned_as
let resolved_email = windmill_common::users::get_email_from_permissioned_as(
&resolved_permissioned_as,
&w_id,
&db,
)
.await?;
let schedule = sqlx::query_as!(
Schedule,
r#"
INSERT INTO schedule (
workspace_id, path, schedule, timezone, edited_by, script_path,
is_flow, args, enabled, email,
is_flow, args, enabled, email, permissioned_as,
on_failure, on_failure_times, on_failure_exact, on_failure_extra_args,
on_recovery, on_recovery_times, on_recovery_extra_args,
on_success, on_success_extra_args,
@@ -259,12 +251,12 @@ async fn create_schedule(
tag, paused_until, cron_version, description, dynamic_skip
) VALUES (
$1, $2, $3, $4, $5, $6,
$7, $8, $9, $10,
$11, $12, $13, $14,
$15, $16, $17,
$18, $19,
$20, $21, $22, $23,
$24, $25, $26, $27, $28
$7, $8, $9, $10, $11,
$12, $13, $14, $15,
$16, $17, $18,
$19, $20,
$21, $22, $23, $24,
$25, $26, $27, $28, $29
)
RETURNING
workspace_id,
@@ -279,6 +271,7 @@ async fn create_schedule(
args AS "args: _",
extra_perms,
email,
permissioned_as,
error,
on_failure,
on_failure_times,
@@ -309,7 +302,8 @@ async fn create_schedule(
to_json_raw_opt(ns.args.as_ref())
as Option<sqlx::types::Json<Box<serde_json::value::RawValue>>>,
ns.enabled.unwrap_or(false),
resolve_email(ns.email.as_ref(), ns.preserve_email, &authed, &db, &w_id).await?,
resolved_email,
resolved_permissioned_as,
ns.on_failure,
ns.on_failure_times,
ns.on_failure_exact,
@@ -355,8 +349,8 @@ async fn create_schedule(
)
.await?;
if let Some(on_behalf_of) = windmill_common::check_on_behalf_of_preservation(
ns.email.as_deref(),
ns.preserve_email.unwrap_or(false),
ns.permissioned_as.as_deref(),
ns.preserve_permissioned_as.unwrap_or(false),
&authed,
&authed.username,
) {
@@ -421,7 +415,28 @@ async fn edit_schedule(
clear_schedule(&mut tx, path, &w_id).await?;
let resolved_edited_by = resolve_edited_by(es.email.as_ref(), es.preserve_email, &authed);
let resolved_edited_by = resolve_edited_by(&authed);
let resolved_permissioned_as = resolve_permissioned_as(
es.permissioned_as.as_ref(),
es.preserve_permissioned_as,
&authed,
);
// email is still written for backwards compat with old workers that don't know about permissioned_as.
// When permissioned_as is preserved to a different user, derive email from it.
let resolved_email = if resolved_permissioned_as
!= windmill_common::users::username_to_permissioned_as(&authed.username)
{
windmill_common::users::get_email_from_permissioned_as(
&resolved_permissioned_as,
&w_id,
&db,
)
.await?
} else {
authed.email.clone()
};
let schedule = sqlx::query_as!(
Schedule,
@@ -450,8 +465,9 @@ async fn edit_schedule(
cron_version = COALESCE($21, cron_version),
description = $22,
dynamic_skip = $23,
email = COALESCE($24, email),
edited_by = $25
email = $24,
edited_by = $25,
permissioned_as = $26
WHERE path = $19 AND workspace_id = $20
RETURNING
workspace_id,
@@ -466,6 +482,7 @@ async fn edit_schedule(
args AS "args: _",
extra_perms,
email,
permissioned_as,
error,
on_failure,
on_failure_times,
@@ -513,8 +530,9 @@ async fn edit_schedule(
es.cron_version,
es.description,
es.dynamic_skip,
Some(resolve_email(es.email.as_ref(), es.preserve_email, &authed, &db, &w_id).await?),
resolved_edited_by
resolved_email,
resolved_edited_by,
resolved_permissioned_as
)
.fetch_one(&mut *tx)
.await
@@ -535,9 +553,10 @@ async fn edit_schedule(
),
)
.await?;
if let Some(on_behalf_of) = windmill_common::check_on_behalf_of_preservation(
es.email.as_deref(),
es.preserve_email.unwrap_or(false),
es.permissioned_as.as_deref(),
es.preserve_permissioned_as.unwrap_or(false),
&authed,
&authed.username,
) {
@@ -547,14 +566,8 @@ async fn edit_schedule(
"schedule.on_behalf_of",
ActionKind::Update,
&w_id,
Some(path),
Some(
[
("on_behalf_of", on_behalf_of.as_str()),
("action", "update"),
]
.into(),
),
Some(&path.to_string()),
Some([("on_behalf_of", on_behalf_of.as_str()), ("action", "edit")].into()),
)
.await?;
}
@@ -788,6 +801,7 @@ pub async fn set_enabled(
let mut tx = user_db.begin(&authed).await?;
let path = path.to_path();
check_scopes(&authed, || format!("schedules:write:{}", path))?;
// email is still written for backwards compat with old workers that don't know about permissioned_as
let schedule_o = sqlx::query_as!(
Schedule,
r#"
@@ -808,6 +822,7 @@ pub async fn set_enabled(
args AS "args: _",
extra_perms,
email,
permissioned_as,
error,
on_failure,
on_failure_times,
@@ -1179,8 +1194,8 @@ pub struct EditSchedule {
pub paused_until: Option<DateTime<Utc>>,
pub cron_version: Option<String>,
pub dynamic_skip: Option<String>,
pub email: Option<String>,
pub preserve_email: Option<bool>,
pub permissioned_as: Option<String>,
pub preserve_permissioned_as: Option<bool>,
}
pub use windmill_queue::schedule::clear_schedule;
+85 -79
View File
@@ -20483,6 +20483,9 @@ components:
email:
type: string
description: Email of the user who owns this schedule, used for permissioned_as
permissioned_as:
type: string
description: The user or group this schedule runs as (e.g., 'u/admin' or 'g/mygroup')
error:
type: string
nullable: true
@@ -20565,6 +20568,7 @@ components:
- is_flow
- enabled
- email
- permissioned_as
ScheduleWJobs:
allOf:
@@ -20686,12 +20690,12 @@ components:
type: string
nullable: true
description: Path to a script that validates scheduled datetimes. Receives scheduled_for datetime and returns boolean to skip (true) or run (false)
email:
permissioned_as:
type: string
description: Email of the user who the scheduled jobs run as. Used during deployment to preserve the original schedule owner.
preserve_email:
description: The user or group this schedule runs as. Used during deployment to preserve the original schedule owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
- schedule
@@ -20779,12 +20783,14 @@ components:
type: string
nullable: true
description: Path to a script that validates scheduled datetimes. Receives scheduled_for datetime and returns boolean to skip (true) or run (false)
email:
permissioned_as:
type: string
description: Email of the user who the scheduled jobs run as. Used during deployment to preserve the original schedule owner.
preserve_email:
nullable: true
description: "The user or group this schedule runs as (e.g., 'u/admin' or 'g/mygroup'). Only admins and wm_deployers can set this via preserve_permissioned_as."
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
nullable: true
description: "If true and user is admin/wm_deployers, preserve the provided permissioned_as instead of using the deploying user's identity"
required:
- schedule
- timezone
@@ -20825,9 +20831,9 @@ components:
script_path:
type: string
description: Path to the script or flow to execute when triggered
email:
permissioned_as:
type: string
description: Email of the user who owns this trigger, used for permissioned_as
description: The user or group this trigger runs as (permissioned_as)
extra_perms:
type: object
description: Additional permissions for this trigger
@@ -20852,7 +20858,7 @@ components:
required:
- path
- script_path
- email
- permissioned_as
- extra_perms
- workspace_id
- edited_by
@@ -21141,12 +21147,12 @@ components:
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: Retry configuration for failed executions
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
@@ -21233,12 +21239,12 @@ components:
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: Retry configuration for failed executions
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
- script_path
@@ -21401,12 +21407,12 @@ components:
retry:
description: Retry configuration for failed executions
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
@@ -21469,12 +21475,12 @@ components:
retry:
description: Retry configuration for failed executions
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
@@ -21644,12 +21650,12 @@ components:
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: Retry configuration for failed executions
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
- script_path
@@ -21704,12 +21710,12 @@ components:
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: Retry configuration for failed executions
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
- script_path
@@ -21846,12 +21852,12 @@ components:
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: "Retry configuration for failed executions."
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
- script_path
@@ -22014,12 +22020,12 @@ components:
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: Retry configuration for failed executions
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- queue_url
- aws_resource_path
@@ -22066,12 +22072,12 @@ components:
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: Retry configuration for failed executions
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- queue_url
- aws_resource_path
@@ -22230,12 +22236,12 @@ components:
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: Retry configuration for failed executions
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
- script_path
@@ -22278,12 +22284,12 @@ components:
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: Retry configuration for failed executions
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
- script_path
@@ -22413,12 +22419,12 @@ components:
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: Retry configuration for failed executions
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
@@ -22483,12 +22489,12 @@ components:
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: Retry configuration for failed executions
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
@@ -22590,12 +22596,12 @@ components:
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: Retry configuration for failed executions
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
@@ -22645,12 +22651,12 @@ components:
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
description: Retry configuration for failed executions
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
- script_path
@@ -22699,12 +22705,12 @@ components:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
mode:
$ref: "#/components/schemas/TriggerMode"
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
@@ -22731,12 +22737,12 @@ components:
$ref: "#/components/schemas/ScriptArgs"
retry:
$ref: "../../openflow.openapi.yaml#/components/schemas/Retry"
email:
permissioned_as:
type: string
description: Email of the user who triggered jobs run as. Used during deployment to preserve the original trigger owner.
preserve_email:
description: The user or group this trigger runs as. Used during deployment to preserve the original trigger owner.
preserve_permissioned_as:
type: boolean
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original email value instead of overwriting it."
description: "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it."
required:
- path
- script_path
@@ -10,7 +10,6 @@ use crate::{
get_runnable_format, trigger_runnable, trigger_runnable_and_wait_for_result,
trigger_runnable_inner, RunnableId,
},
users::fetch_api_authed,
utils::{check_scopes, ExpiringCacheEntry},
};
use axum::{
@@ -214,9 +213,15 @@ async fn get_http_route_trigger(
None
};
let authed = fetch_api_authed(
trigger.edited_by.clone(),
trigger.email.clone(),
let email = windmill_common::users::get_email_from_permissioned_as(
&trigger.permissioned_as,
&trigger.workspace_id,
&db,
)
.await?;
let authed = windmill_api_auth::fetch_api_authed_from_permissioned_as(
trigger.permissioned_as.clone(),
email,
&trigger.workspace_id,
&db,
Some(username_override.unwrap_or(format!("HTTP-{}", trigger.path))),
@@ -210,6 +210,7 @@ where
"updated_by",
"edited_at",
"edited_by",
"permissioned_as",
"archived",
"has_draft",
"error",
+1
View File
@@ -124,6 +124,7 @@ opentelemetry = { workspace = true, optional = true }
tracing-opentelemetry = { workspace = true, optional = true }
opentelemetry-appender-tracing = { workspace = true, optional = true }
tonic = { workspace = true, optional = true }
equivalent = "1.0.2"
[target.'cfg(not(target_env = "msvc"))'.dependencies]
tikv-jemalloc-ctl = { optional = true, workspace = true }
+14 -15
View File
@@ -35,10 +35,7 @@ pub const TOKEN_PREFIX_LEN: usize = 10;
/// Safely extract the token prefix (first TOKEN_PREFIX_LEN chars).
/// Returns the full token if it's shorter than TOKEN_PREFIX_LEN, preventing panics.
pub fn safe_token_prefix(token: &str) -> String {
token
.get(..TOKEN_PREFIX_LEN)
.unwrap_or(token)
.to_string()
token.get(..TOKEN_PREFIX_LEN).unwrap_or(token).to_string()
}
lazy_static::lazy_static! {
@@ -121,10 +118,11 @@ pub fn check_extra_perms(
groups: &[String],
) -> Option<bool> {
// Check direct user permission
let user_key = if username.starts_with("u/") {
use crate::users::{PERMISSIONED_AS_GROUP_PREFIX, PERMISSIONED_AS_USER_PREFIX};
let user_key = if username.starts_with(PERMISSIONED_AS_USER_PREFIX) {
username.to_string()
} else {
format!("u/{username}")
format!("{PERMISSIONED_AS_USER_PREFIX}{username}")
};
if let Some(v) = extra_perms.get(&user_key) {
return Some(v.as_bool().unwrap_or(false));
@@ -134,10 +132,10 @@ pub fn check_extra_perms(
let mut found = false;
let mut write = false;
for g in groups {
let key = if g.starts_with("g/") {
let key = if g.starts_with(PERMISSIONED_AS_GROUP_PREFIX) {
g.to_string()
} else {
format!("g/{g}")
format!("{PERMISSIONED_AS_GROUP_PREFIX}{g}")
};
if let Some(v) = extra_perms.get(&key) {
found = true;
@@ -293,12 +291,13 @@ pub async fn is_devops_email(db: &DB, email: &str) -> Result<bool> {
}
pub fn permissioned_as_to_username(permissioned_as: &str) -> String {
if let Some((prefix, name)) = permissioned_as.split_once('/') {
if prefix == "u" {
name.to_string()
} else {
format!("group-{}", name)
}
use crate::users::{PERMISSIONED_AS_USER_PREFIX, USERNAME_GROUP_PREFIX};
if let Some(name) = permissioned_as.strip_prefix(PERMISSIONED_AS_USER_PREFIX) {
name.to_string()
} else if let Some(name) =
permissioned_as.strip_prefix(crate::users::PERMISSIONED_AS_GROUP_PREFIX)
{
format!("{}{}", USERNAME_GROUP_PREFIX, name)
} else {
permissioned_as.to_string()
}
@@ -379,7 +378,7 @@ async fn fetch_authed_from_permissioned_as_inner(
let folders = get_folders_for_user(&w_id, "", &groups, &mut *conn).await?;
Ok(Authed {
email: email.to_string(),
username: format!("group-{name}"),
username: format!("{}{name}", crate::users::USERNAME_GROUP_PREFIX),
is_admin: false,
groups,
is_operator: false,
+78 -1
View File
@@ -12,11 +12,72 @@ pub const SUPERADMIN_SYNC_EMAIL: &str = "superadmin_sync@windmill.dev";
pub const COOKIE_NAME: &str = "token";
/// Prefix for user-based permissioned_as values: "u/"
pub const PERMISSIONED_AS_USER_PREFIX: &str = "u/";
/// Prefix for group-based permissioned_as values: "g/"
pub const PERMISSIONED_AS_GROUP_PREFIX: &str = "g/";
/// Prefix for group-based usernames: "group-"
pub const USERNAME_GROUP_PREFIX: &str = "group-";
pub fn username_to_permissioned_as(user: &str) -> String {
if user.contains('@') {
user.to_string()
} else if let Some(group) = user.strip_prefix(USERNAME_GROUP_PREFIX) {
format!("{}{}", PERMISSIONED_AS_GROUP_PREFIX, group)
} else {
format!("u/{}", user)
format!("{}{}", PERMISSIONED_AS_USER_PREFIX, user)
}
}
/// Borrowed key for zero-allocation cache lookups via `Equivalent<(String, String)>`.
#[derive(Hash)]
struct EmailCacheKey<'a>(&'a str, &'a str);
impl equivalent::Equivalent<(String, String)> for EmailCacheKey<'_> {
fn equivalent(&self, key: &(String, String)) -> bool {
self.0 == key.0 && self.1 == key.1
}
}
lazy_static::lazy_static! {
static ref EMAIL_CACHE: quick_cache::sync::Cache<(String, String), (String, std::time::Instant)> =
quick_cache::sync::Cache::new(500);
}
const EMAIL_CACHE_TTL_SECS: u64 = 60;
/// Get email from permissioned_as string.
/// - "u/{username}" → lookup email from usr table (cached)
/// - "g/{group}" → "group-{group}@windmill.dev"
/// - raw email → return as-is
pub async fn get_email_from_permissioned_as(
permissioned_as: &str,
workspace_id: &str,
db: &sqlx::Pool<sqlx::Postgres>,
) -> crate::error::Result<String> {
if let Some(username) = permissioned_as.strip_prefix(PERMISSIONED_AS_USER_PREFIX) {
let lookup = EmailCacheKey(workspace_id, username);
if let Some((email, cached_at)) = EMAIL_CACHE.get(&lookup) {
if cached_at.elapsed().as_secs() < EMAIL_CACHE_TTL_SECS {
return Ok(email);
}
}
let email = sqlx::query_scalar!(
"SELECT email FROM usr WHERE username = $1 AND workspace_id = $2",
username,
workspace_id
)
.fetch_optional(db)
.await?
.unwrap_or_else(|| format!("{}@unknown.windmill.dev", username));
let key = (workspace_id.to_string(), username.to_string());
EMAIL_CACHE.insert(key, (email.clone(), std::time::Instant::now()));
Ok(email)
} else if let Some(group) = permissioned_as.strip_prefix(PERMISSIONED_AS_GROUP_PREFIX) {
Ok(format!("{}{}@windmill.dev", USERNAME_GROUP_PREFIX, group))
} else {
// raw email
Ok(permissioned_as.to_string())
}
}
@@ -29,3 +90,19 @@ pub fn truncate_token(token: &str) -> String {
token.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_username_to_permissioned_as() {
assert_eq!(username_to_permissioned_as("alice"), "u/alice");
assert_eq!(
username_to_permissioned_as("alice@example.com"),
"alice@example.com"
);
assert_eq!(username_to_permissioned_as("group-all"), "g/all");
assert_eq!(username_to_permissioned_as("group-my-team"), "g/my-team");
}
}
@@ -459,8 +459,8 @@ async fn test_trigger_notify_http_trigger_change(db: Pool<Postgres>) {
// Insert an HTTP trigger
sqlx::query(
"INSERT INTO http_trigger (path, route_path, route_path_key, script_path, is_flow, workspace_id, edited_by, email, http_method, authentication_method)
VALUES ($1, '/test/route', '/test/route', 'test/script', false, 'test-workspace', 'test-user', 'test@test.com', 'get', 'none')",
"INSERT INTO http_trigger (path, route_path, route_path_key, script_path, is_flow, workspace_id, edited_by, permissioned_as, http_method, authentication_method)
VALUES ($1, '/test/route', '/test/route', 'test/script', false, 'test-workspace', 'test-user', 'u/test-user', 'get', 'none')",
)
.bind(&trigger_path)
.execute(&db)
+14 -3
View File
@@ -1869,9 +1869,20 @@ pub async fn try_schedule_next_job<'c>(
&job.workspace_id
);
let permissioned_as = schedule.permissioned_as.clone();
let email = match windmill_common::users::get_email_from_permissioned_as(
&permissioned_as,
&job.workspace_id,
db,
)
.await
{
Ok(email) => email,
Err(e) => return (tx, Some(e)),
};
let schedule_authed = windmill_common::auth::fetch_authed_from_permissioned_as(
&windmill_common::users::username_to_permissioned_as(&schedule.edited_by),
&schedule.email,
&permissioned_as,
&email,
&job.workspace_id,
&mut *tx,
)
@@ -5832,7 +5843,7 @@ async fn push_inner<'c, 'd>(
let audit_author = if format!("u/{user}") != permissioned_as && user != permissioned_as {
AuditAuthor {
email: email.to_string(),
username: permissioned_as.trim_start_matches("u/").to_string(),
username: windmill_common::auth::permissioned_as_to_username(&permissioned_as),
username_override: Some(user.to_string()),
token_prefix: token_prefix.map(|s| s.to_string()),
}
+16 -11
View File
@@ -344,8 +344,11 @@ pub async fn push_scheduled_job<'c>(
.await?;
let (debouncing_settings, concurrency_settings) =
windmill_common::runnable_settings::prefetch_cached_from_handle(runnable_settings_handle, db)
.await?;
windmill_common::runnable_settings::prefetch_cached_from_handle(
runnable_settings_handle,
db,
)
.await?;
if schedule.retry.is_some() {
let parsed_retry = serde_json::from_value::<Retry>(schedule.retry.clone().unwrap())
@@ -450,18 +453,20 @@ pub async fn push_scheduled_job<'c>(
.await?;
}
(
email,
email.clone(),
username_to_permissioned_as(&created_by),
None,
is_windmill_user,
)
} else {
(
&schedule.email,
username_to_permissioned_as(&schedule.edited_by),
authed,
false,
let permissioned_as = schedule.permissioned_as.clone();
let resolved_email = windmill_common::users::get_email_from_permissioned_as(
&permissioned_as,
&schedule.workspace_id,
db,
)
.await?;
(resolved_email, permissioned_as, authed, false)
};
let obo_authed;
@@ -470,7 +475,7 @@ pub async fn push_scheduled_job<'c>(
None => {
obo_authed = windmill_common::auth::fetch_authed_from_permissioned_as(
&permissioned_as,
email,
&email,
&schedule.workspace_id,
&mut *tx,
)
@@ -485,7 +490,7 @@ pub async fn push_scheduled_job<'c>(
&db,
&schedule.workspace_id,
&tag,
email,
&email,
None, // no token for schedules so no scopes so no scope_tags
)
.warn_after_seconds_with_sql(1, "check_tag_available_for_workspace_internal".to_string())
@@ -506,7 +511,7 @@ pub async fn push_scheduled_job<'c>(
payload,
crate::PushArgs { args: &args, extra: None },
&schedule_to_user(&schedule.path),
email,
&email,
permissioned_as,
Some(&schedule.path),
Some(next),
+141 -233
View File
@@ -23,6 +23,7 @@ mod schedule_push {
args: None,
extra_perms: serde_json::json!({}),
email: "test@windmill.dev".to_string(),
permissioned_as: "u/test-user".to_string(),
error: None,
on_failure: None,
on_failure_times: None,
@@ -423,8 +424,7 @@ mod schedule_push {
// Pass a now_cutoff far in the future — simulates clock shift
let future_cutoff = Utc::now() + chrono::Duration::hours(24);
let tx = db.begin().await?;
let tx =
push_scheduled_job(&db, tx, &schedule, Some(&authed), Some(future_cutoff)).await?;
let tx = push_scheduled_job(&db, tx, &schedule, Some(&authed), Some(future_cutoff)).await?;
tx.commit().await?;
assert_eq!(count_queued_jobs(&db).await, 1);
@@ -451,14 +451,8 @@ mod schedule_push {
let job = make_completed_job(&schedule);
let tx = db.begin().await?;
let (tx, err) = try_schedule_next_job(
&db,
tx,
&job,
&schedule,
&schedule.script_path,
)
.await;
let (tx, err) =
try_schedule_next_job(&db, tx, &job, &schedule, &schedule.script_path).await;
tx.commit().await?;
assert!(err.is_none());
assert_eq!(count_queued_jobs(&db).await, 0);
@@ -475,14 +469,8 @@ mod schedule_push {
let job = make_completed_job(&schedule);
let tx = db.begin().await?;
let (tx, err) = try_schedule_next_job(
&db,
tx,
&job,
&schedule,
"f/system/different_script",
)
.await;
let (tx, err) =
try_schedule_next_job(&db, tx, &job, &schedule, "f/system/different_script").await;
tx.commit().await?;
assert!(err.is_none());
assert_eq!(count_queued_jobs(&db).await, 0);
@@ -499,14 +487,8 @@ mod schedule_push {
let job = make_completed_job(&schedule);
let tx = db.begin().await?;
let (tx, err) = try_schedule_next_job(
&db,
tx,
&job,
&schedule,
&schedule.script_path,
)
.await;
let (tx, err) =
try_schedule_next_job(&db, tx, &job, &schedule, &schedule.script_path).await;
tx.commit().await?;
assert!(err.is_none());
assert_eq!(count_queued_jobs(&db).await, 1);
@@ -531,14 +513,8 @@ mod schedule_push {
let job = make_completed_job(&schedule);
let tx = db.begin().await?;
let (tx, err) = try_schedule_next_job(
&db,
tx,
&job,
&schedule,
&schedule.script_path,
)
.await;
let (tx, err) =
try_schedule_next_job(&db, tx, &job, &schedule, &schedule.script_path).await;
tx.commit().await?;
assert!(err.is_none());
assert_eq!(count_queued_jobs(&db).await, 1);
@@ -560,8 +536,8 @@ mod schedule_push {
#[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))]
async fn test_handle_push_failure_disables_schedule(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap)
VALUES ('test-workspace', 'f/system/bad_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/nonexistent', false, 'test@windmill.dev', '{}', false, false)"
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap, permissioned_as)
VALUES ('test-workspace', 'f/system/bad_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/nonexistent', false, 'test@windmill.dev', '{}', false, false, 'u/test-user')"
)
.execute(&db)
.await?;
@@ -573,14 +549,8 @@ mod schedule_push {
let job = make_completed_job(&schedule);
let tx = db.begin().await?;
let (tx, err) = try_schedule_next_job(
&db,
tx,
&job,
&schedule,
&schedule.script_path,
)
.await;
let (tx, err) =
try_schedule_next_job(&db, tx, &job, &schedule, &schedule.script_path).await;
// NotFound: schedule disabled internally, no error returned (caller commits)
assert!(err.is_none());
tx.commit().await?;
@@ -605,8 +575,8 @@ mod schedule_push {
#[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))]
async fn test_success_atomic_with_commit(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap)
VALUES ('test-workspace', 'f/system/test_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/test_script', false, 'test@windmill.dev', '{}', false, false)"
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap, permissioned_as)
VALUES ('test-workspace', 'f/system/test_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/test_script', false, 'test@windmill.dev', '{}', false, false, 'u/test-user')"
)
.execute(&db)
.await?;
@@ -615,14 +585,8 @@ mod schedule_push {
let job = make_completed_job(&schedule);
let tx = db.begin().await?;
let (tx, err) = try_schedule_next_job(
&db,
tx,
&job,
&schedule,
&schedule.script_path,
)
.await;
let (tx, err) =
try_schedule_next_job(&db, tx, &job, &schedule, &schedule.script_path).await;
assert!(err.is_none());
tx.commit().await?;
@@ -650,14 +614,8 @@ mod schedule_push {
let job = make_completed_job(&schedule);
let tx = db.begin().await?;
let (tx, err) = try_schedule_next_job(
&db,
tx,
&job,
&schedule,
&schedule.script_path,
)
.await;
let (tx, err) =
try_schedule_next_job(&db, tx, &job, &schedule, &schedule.script_path).await;
assert!(err.is_none());
// Intentionally drop tx without committing (simulates caller failure)
@@ -676,8 +634,8 @@ mod schedule_push {
#[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))]
async fn test_failure_disable_rolls_back_on_drop(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap)
VALUES ('test-workspace', 'f/system/bad_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/nonexistent', false, 'test@windmill.dev', '{}', false, false)"
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap, permissioned_as)
VALUES ('test-workspace', 'f/system/bad_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/nonexistent', false, 'test@windmill.dev', '{}', false, false, 'u/test-user')"
)
.execute(&db)
.await?;
@@ -689,14 +647,8 @@ mod schedule_push {
let job = make_completed_job(&schedule);
let tx = db.begin().await?;
let (tx, err) = try_schedule_next_job(
&db,
tx,
&job,
&schedule,
&schedule.script_path,
)
.await;
let (tx, err) =
try_schedule_next_job(&db, tx, &job, &schedule, &schedule.script_path).await;
// NotFound: schedule disabled in tx, no error returned
assert!(err.is_none());
@@ -725,29 +677,24 @@ mod schedule_push {
let job = make_completed_job(&schedule);
let tx = db.begin().await?;
let (mut tx, err) = try_schedule_next_job(
&db,
tx,
&job,
&schedule,
&schedule.script_path,
)
.await;
let (mut tx, err) =
try_schedule_next_job(&db, tx, &job, &schedule, &schedule.script_path).await;
assert!(err.is_none());
// Write something else on the same tx
sqlx::query("INSERT INTO global_settings (name, value) VALUES ('_test_after_push', '42'::jsonb)")
.execute(&mut *tx)
.await?;
sqlx::query(
"INSERT INTO global_settings (name, value) VALUES ('_test_after_push', '42'::jsonb)",
)
.execute(&mut *tx)
.await?;
tx.commit().await?;
// Both the pushed job and the extra write should be visible
assert_eq!(count_queued_jobs(&db).await, 1);
let val: serde_json::Value = sqlx::query_scalar(
"SELECT value FROM global_settings WHERE name = '_test_after_push'",
)
.fetch_one(&db)
.await?;
let val: serde_json::Value =
sqlx::query_scalar("SELECT value FROM global_settings WHERE name = '_test_after_push'")
.fetch_one(&db)
.await?;
assert_eq!(val, serde_json::json!(42));
Ok(())
}
@@ -760,8 +707,8 @@ mod schedule_push {
#[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))]
async fn test_tx_usable_after_failure(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap)
VALUES ('test-workspace', 'f/system/bad_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/nonexistent', false, 'test@windmill.dev', '{}', false, false)"
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap, permissioned_as)
VALUES ('test-workspace', 'f/system/bad_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/nonexistent', false, 'test@windmill.dev', '{}', false, false, 'u/test-user')"
)
.execute(&db)
.await?;
@@ -773,21 +720,17 @@ mod schedule_push {
let job = make_completed_job(&schedule);
let tx = db.begin().await?;
let (mut tx, err) = try_schedule_next_job(
&db,
tx,
&job,
&schedule,
&schedule.script_path,
)
.await;
let (mut tx, err) =
try_schedule_next_job(&db, tx, &job, &schedule, &schedule.script_path).await;
// NotFound: schedule disabled in tx, no error returned
assert!(err.is_none());
// Write something else on the returned tx — tx is still usable
sqlx::query("INSERT INTO global_settings (name, value) VALUES ('_test_after_fail', '99'::jsonb)")
.execute(&mut *tx)
.await?;
sqlx::query(
"INSERT INTO global_settings (name, value) VALUES ('_test_after_fail', '99'::jsonb)",
)
.execute(&mut *tx)
.await?;
tx.commit().await?;
// Schedule should be disabled (NotFound is non-retryable)
@@ -800,11 +743,10 @@ mod schedule_push {
assert!(error.is_some());
// Extra write should still be committed (tx is usable after push failure)
let val: serde_json::Value = sqlx::query_scalar(
"SELECT value FROM global_settings WHERE name = '_test_after_fail'",
)
.fetch_one(&db)
.await?;
let val: serde_json::Value =
sqlx::query_scalar("SELECT value FROM global_settings WHERE name = '_test_after_fail'")
.fetch_one(&db)
.await?;
assert_eq!(val, serde_json::json!(99));
Ok(())
}
@@ -823,14 +765,8 @@ mod schedule_push {
let job = make_completed_job(&schedule);
let tx = db.begin().await?;
let (tx, err) = try_schedule_next_job(
&db,
tx,
&job,
&schedule,
&schedule.script_path,
)
.await;
let (tx, err) =
try_schedule_next_job(&db, tx, &job, &schedule, &schedule.script_path).await;
tx.commit().await?;
assert!(err.is_none());
assert_eq!(count_queued_jobs(&db).await, 1);
@@ -855,14 +791,8 @@ mod schedule_push {
let job = make_completed_job(&schedule);
let tx = db.begin().await?;
let (tx, err) = try_schedule_next_job(
&db,
tx,
&job,
&schedule,
&schedule.script_path,
)
.await;
let (tx, err) =
try_schedule_next_job(&db, tx, &job, &schedule, &schedule.script_path).await;
tx.commit().await?;
assert!(err.is_none());
assert_eq!(count_queued_jobs(&db).await, 1);
@@ -883,8 +813,8 @@ mod schedule_push {
#[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))]
async fn test_push_failure_stores_error_message(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap)
VALUES ('test-workspace', 'f/system/bad_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/nonexistent', false, 'test@windmill.dev', '{}', false, false)"
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap, permissioned_as)
VALUES ('test-workspace', 'f/system/bad_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/nonexistent', false, 'test@windmill.dev', '{}', false, false, 'u/test-user')"
)
.execute(&db)
.await?;
@@ -896,14 +826,8 @@ mod schedule_push {
let job = make_completed_job(&schedule);
let tx = db.begin().await?;
let (tx, err) = try_schedule_next_job(
&db,
tx,
&job,
&schedule,
&schedule.script_path,
)
.await;
let (tx, err) =
try_schedule_next_job(&db, tx, &job, &schedule, &schedule.script_path).await;
// NotFound: schedule disabled in tx, no error returned (caller commits)
assert!(err.is_none());
tx.commit().await?;
@@ -928,8 +852,8 @@ mod schedule_push {
#[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))]
async fn test_disabled_schedule_no_side_effects(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap)
VALUES ('test-workspace', 'f/system/disabled_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', false, 'f/system/test_script', false, 'test@windmill.dev', '{}', false, false)"
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap, permissioned_as)
VALUES ('test-workspace', 'f/system/disabled_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', false, 'f/system/test_script', false, 'test@windmill.dev', '{}', false, false, 'u/test-user')"
)
.execute(&db)
.await?;
@@ -941,14 +865,8 @@ mod schedule_push {
let job = make_completed_job(&schedule);
let tx = db.begin().await?;
let (tx, err) = try_schedule_next_job(
&db,
tx,
&job,
&schedule,
&schedule.script_path,
)
.await;
let (tx, err) =
try_schedule_next_job(&db, tx, &job, &schedule, &schedule.script_path).await;
tx.commit().await?;
assert!(err.is_none());
@@ -959,7 +877,10 @@ mod schedule_push {
.fetch_one(&db)
.await?;
assert!(!enabled);
assert!(error.is_none(), "disabled schedule should not get an error set");
assert!(
error.is_none(),
"disabled schedule should not get an error set"
);
assert_eq!(count_queued_jobs(&db).await, 0);
Ok(())
}
@@ -971,8 +892,8 @@ mod schedule_push {
#[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))]
async fn test_path_mismatch_no_side_effects(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap)
VALUES ('test-workspace', 'f/system/test_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/test_script', false, 'test@windmill.dev', '{}', false, false)"
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap, permissioned_as)
VALUES ('test-workspace', 'f/system/test_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/test_script', false, 'test@windmill.dev', '{}', false, false, 'u/test-user')"
)
.execute(&db)
.await?;
@@ -981,14 +902,8 @@ mod schedule_push {
let job = make_completed_job(&schedule);
let tx = db.begin().await?;
let (tx, err) = try_schedule_next_job(
&db,
tx,
&job,
&schedule,
"f/system/different_script",
)
.await;
let (tx, err) =
try_schedule_next_job(&db, tx, &job, &schedule, "f/system/different_script").await;
tx.commit().await?;
assert!(err.is_none());
@@ -1020,14 +935,8 @@ mod schedule_push {
let job = make_completed_job(&schedule);
let tx = db.begin().await?;
let (tx, err) = try_schedule_next_job(
&db,
tx,
&job,
&schedule,
&schedule.script_path,
)
.await;
let (tx, err) =
try_schedule_next_job(&db, tx, &job, &schedule, &schedule.script_path).await;
drop(tx);
// NotFound: disable succeeds (UPDATE 0 rows is not an error), no error returned
assert!(err.is_none());
@@ -1045,8 +954,8 @@ mod schedule_push {
#[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))]
async fn test_invariant_success_means_tick_committed(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap)
VALUES ('test-workspace', 'f/system/test_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/test_script', false, 'test@windmill.dev', '{}', false, false)"
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap, permissioned_as)
VALUES ('test-workspace', 'f/system/test_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/test_script', false, 'test@windmill.dev', '{}', false, false, 'u/test-user')"
)
.execute(&db)
.await?;
@@ -1055,14 +964,8 @@ mod schedule_push {
let job = make_completed_job(&schedule);
let tx = db.begin().await?;
let (tx, err) = try_schedule_next_job(
&db,
tx,
&job,
&schedule,
&schedule.script_path,
)
.await;
let (tx, err) =
try_schedule_next_job(&db, tx, &job, &schedule, &schedule.script_path).await;
assert!(err.is_none());
tx.commit().await?;
@@ -1092,8 +995,8 @@ mod schedule_push {
db: Pool<Postgres>,
) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap)
VALUES ('test-workspace', 'f/system/bad_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/nonexistent', false, 'test@windmill.dev', '{}', false, false)"
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap, permissioned_as)
VALUES ('test-workspace', 'f/system/bad_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/nonexistent', false, 'test@windmill.dev', '{}', false, false, 'u/test-user')"
)
.execute(&db)
.await?;
@@ -1105,14 +1008,8 @@ mod schedule_push {
let job = make_completed_job(&schedule);
let tx = db.begin().await?;
let (tx, err) = try_schedule_next_job(
&db,
tx,
&job,
&schedule,
&schedule.script_path,
)
.await;
let (tx, err) =
try_schedule_next_job(&db, tx, &job, &schedule, &schedule.script_path).await;
// NotFound: schedule disabled in tx, no error returned (caller commits)
assert!(err.is_none());
tx.commit().await?;
@@ -1142,8 +1039,8 @@ mod schedule_push {
db: Pool<Postgres>,
) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap)
VALUES ('test-workspace', 'f/system/bad_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/nonexistent', false, 'test@windmill.dev', '{}', false, false)"
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap, permissioned_as)
VALUES ('test-workspace', 'f/system/bad_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/nonexistent', false, 'test@windmill.dev', '{}', false, false, 'u/test-user')"
)
.execute(&db)
.await?;
@@ -1155,14 +1052,8 @@ mod schedule_push {
let job = make_completed_job(&schedule);
let tx = db.begin().await?;
let (tx, err) = try_schedule_next_job(
&db,
tx,
&job,
&schedule,
&schedule.script_path,
)
.await;
let (tx, err) =
try_schedule_next_job(&db, tx, &job, &schedule, &schedule.script_path).await;
// NotFound: schedule disabled in tx, no error returned
assert!(err.is_none());
@@ -1195,10 +1086,12 @@ mod schedule_push {
// ---------------------------------------------------------------
#[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))]
async fn test_failpoint_savepoint_create_disables(db: Pool<Postgres>) -> anyhow::Result<()> {
async fn test_failpoint_savepoint_create_disables(
db: Pool<Postgres>,
) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap)
VALUES ('test-workspace', 'f/system/test_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/test_script', false, 'test@windmill.dev', '{}', false, false)"
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap, permissioned_as)
VALUES ('test-workspace', 'f/system/test_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/test_script', false, 'test@windmill.dev', '{}', false, false, 'u/test-user')"
)
.execute(&db)
.await?;
@@ -1234,8 +1127,8 @@ mod schedule_push {
#[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))]
async fn test_failpoint_push_disables(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap)
VALUES ('test-workspace', 'f/system/test_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/test_script', false, 'test@windmill.dev', '{}', false, false)"
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap, permissioned_as)
VALUES ('test-workspace', 'f/system/test_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/test_script', false, 'test@windmill.dev', '{}', false, false, 'u/test-user')"
)
.execute(&db)
.await?;
@@ -1269,10 +1162,12 @@ mod schedule_push {
// ---------------------------------------------------------------
#[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))]
async fn test_failpoint_savepoint_commit_disables(db: Pool<Postgres>) -> anyhow::Result<()> {
async fn test_failpoint_savepoint_commit_disables(
db: Pool<Postgres>,
) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap)
VALUES ('test-workspace', 'f/system/test_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/test_script', false, 'test@windmill.dev', '{}', false, false)"
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap, permissioned_as)
VALUES ('test-workspace', 'f/system/test_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/test_script', false, 'test@windmill.dev', '{}', false, false, 'u/test-user')"
)
.execute(&db)
.await?;
@@ -1306,10 +1201,12 @@ mod schedule_push {
// ---------------------------------------------------------------
#[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))]
async fn test_failpoint_schedule_disable_returns_err(db: Pool<Postgres>) -> anyhow::Result<()> {
async fn test_failpoint_schedule_disable_returns_err(
db: Pool<Postgres>,
) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap)
VALUES ('test-workspace', 'f/system/bad_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/nonexistent', false, 'test@windmill.dev', '{}', false, false)"
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap, permissioned_as)
VALUES ('test-workspace', 'f/system/bad_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/nonexistent', false, 'test@windmill.dev', '{}', false, false, 'u/test-user')"
)
.execute(&db)
.await?;
@@ -1320,13 +1217,15 @@ mod schedule_push {
});
let job = make_completed_job(&schedule);
ACTIVE.scope(ScheduleFailPoint::ScheduleDisable, async {
let tx = db.begin().await.unwrap();
let (_tx, err) = try_schedule_next_job(
&db, tx, &job, &schedule, &schedule.script_path,
).await;
assert!(err.is_some(), "must return Some(err) when disable fails");
}).await;
ACTIVE
.scope(ScheduleFailPoint::ScheduleDisable, async {
let tx = db.begin().await.unwrap();
let (_tx, err) =
try_schedule_next_job(&db, tx, &job, &schedule, &schedule.script_path)
.await;
assert!(err.is_some(), "must return Some(err) when disable fails");
})
.await;
Ok(())
}
@@ -1337,8 +1236,8 @@ mod schedule_push {
#[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))]
async fn test_failpoint_disable_failure_rollback(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap)
VALUES ('test-workspace', 'f/system/bad_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/nonexistent', false, 'test@windmill.dev', '{}', false, false)"
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap, permissioned_as)
VALUES ('test-workspace', 'f/system/bad_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/nonexistent', false, 'test@windmill.dev', '{}', false, false, 'u/test-user')"
)
.execute(&db)
.await?;
@@ -1375,10 +1274,12 @@ mod schedule_push {
// ---------------------------------------------------------------
#[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))]
async fn test_failpoint_push_quota_exceeded_script(db: Pool<Postgres>) -> anyhow::Result<()> {
async fn test_failpoint_push_quota_exceeded_script(
db: Pool<Postgres>,
) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap)
VALUES ('test-workspace', 'f/system/test_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/test_script', false, 'test@windmill.dev', '{}', false, false)"
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap, permissioned_as)
VALUES ('test-workspace', 'f/system/test_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/test_script', false, 'test@windmill.dev', '{}', false, false, 'u/test-user')"
)
.execute(&db)
.await?;
@@ -1416,8 +1317,8 @@ mod schedule_push {
#[sqlx::test(migrations = "../migrations", fixtures("base", "schedule_push"))]
async fn test_failpoint_push_quota_exceeded_flow(db: Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap)
VALUES ('test-workspace', 'f/system/flow_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/test_flow', true, 'test@windmill.dev', '{}', false, false)"
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap, permissioned_as)
VALUES ('test-workspace', 'f/system/flow_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/test_flow', true, 'test@windmill.dev', '{}', false, false, 'u/test-user')"
)
.execute(&db)
.await?;
@@ -1481,8 +1382,8 @@ mod schedule_push {
// Schedule still enabled — simulates both push and disable failing
sqlx::query(
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap)
VALUES ('test-workspace', 'f/system/test_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/test_flow', true, 'test@windmill.dev', '{}', false, false)"
"INSERT INTO schedule (workspace_id, path, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, email, extra_perms, ws_error_handler_muted, no_flow_overlap, permissioned_as)
VALUES ('test-workspace', 'f/system/test_schedule', 'test-user', now(), '0 0 */5 * * *', 'UTC', true, 'f/system/test_flow', true, 'test@windmill.dev', '{}', false, false, 'u/test-user')"
)
.execute(&db)
.await?;
@@ -1583,14 +1484,16 @@ mod schedule_push {
.await?;
// Flow is re-queued for processing
let (running, started_at): (bool, Option<chrono::DateTime<Utc>>) = sqlx::query_as(
"SELECT running, started_at FROM v2_job_queue WHERE id = $1",
)
.bind(flow_job_id)
.fetch_one(&db)
.await?;
let (running, started_at): (bool, Option<chrono::DateTime<Utc>>) =
sqlx::query_as("SELECT running, started_at FROM v2_job_queue WHERE id = $1")
.bind(flow_job_id)
.fetch_one(&db)
.await?;
assert!(!running, "flow must not be running after zombie restart");
assert!(started_at.is_none(), "started_at must be null after zombie restart");
assert!(
started_at.is_none(),
"started_at must be null after zombie restart"
);
// Schedule still enabled — will be retried when flow re-executes
let enabled: bool = sqlx::query_scalar(
@@ -1598,16 +1501,21 @@ mod schedule_push {
)
.fetch_one(&db)
.await?;
assert!(enabled, "schedule must remain enabled for retry after zombie restart");
assert!(
enabled,
"schedule must remain enabled for retry after zombie restart"
);
// Flow job is NOT in v2_job_completed (it was never completed with error)
let completed_count: i64 = sqlx::query_scalar(
"SELECT COUNT(*) FROM v2_job_completed WHERE id = $1",
)
.bind(flow_job_id)
.fetch_one(&db)
.await?;
assert_eq!(completed_count, 0, "flow must not be in completed_job — it's a zombie, not an error");
let completed_count: i64 =
sqlx::query_scalar("SELECT COUNT(*) FROM v2_job_completed WHERE id = $1")
.bind(flow_job_id)
.fetch_one(&db)
.await?;
assert_eq!(
completed_count, 0,
"flow must not be in completed_job — it's a zombie, not an error"
);
Ok(())
}
+9 -9
View File
@@ -138,7 +138,7 @@ fn check_no_duplicates(
pub async fn insert_new_trigger_into_db(
authed: &ApiAuthed,
db: &DB,
_db: &DB,
tx: &mut PgConnection,
w_id: &str,
trigger: &TriggerData<HttpConfigRequest>,
@@ -148,7 +148,7 @@ pub async fn insert_new_trigger_into_db(
let request_type = trigger.config.request_type;
let resolved_edited_by = trigger.base.resolve_edited_by(authed);
let resolved_email = trigger.base.resolve_email(authed, db, w_id).await?;
let resolved_permissioned_as = trigger.base.resolve_permissioned_as(authed);
sqlx::query!(
r#"
@@ -171,7 +171,7 @@ pub async fn insert_new_trigger_into_db(
http_method,
static_asset_config,
edited_by,
email,
permissioned_as,
edited_at,
is_static_website,
error_handler_path,
@@ -200,7 +200,7 @@ pub async fn insert_new_trigger_into_db(
trigger.config.http_method as _,
trigger.config.static_asset_config as _,
&resolved_edited_by,
resolved_email,
resolved_permissioned_as,
trigger.config.is_static_website,
trigger.error_handling.error_handler_path,
trigger.error_handling.error_handler_args as _,
@@ -430,7 +430,7 @@ impl TriggerCrud for HttpTrigger {
trigger: TriggerData<Self::TriggerConfigRequest>,
) -> Result<()> {
let resolved_edited_by = trigger.base.resolve_edited_by(authed);
let resolved_email = trigger.base.resolve_email(authed, db, workspace_id).await?;
let resolved_permissioned_as = trigger.base.resolve_permissioned_as(authed);
if authed.is_admin {
if trigger.config.route_path.is_empty() {
@@ -465,7 +465,7 @@ impl TriggerCrud for HttpTrigger {
http_method = $11,
static_asset_config = $12,
edited_by = $13,
email = $14,
permissioned_as = $14,
request_type = $15,
authentication_method = $16,
summary = $17,
@@ -492,7 +492,7 @@ impl TriggerCrud for HttpTrigger {
trigger.config.http_method as _,
trigger.config.static_asset_config as _,
&resolved_edited_by,
resolved_email,
resolved_permissioned_as,
request_type as _,
trigger.config.authentication_method as _,
trigger.config.summary,
@@ -524,7 +524,7 @@ impl TriggerCrud for HttpTrigger {
http_method = $8,
static_asset_config = $9,
edited_by = $10,
email = $11,
permissioned_as = $11,
request_type = $12,
authentication_method = $13,
summary = $14,
@@ -548,7 +548,7 @@ impl TriggerCrud for HttpTrigger {
trigger.config.http_method as _,
trigger.config.static_asset_config as _,
&resolved_edited_by,
resolved_email,
resolved_permissioned_as,
request_type as _,
trigger.config.authentication_method as _,
trigger.config.summary,
@@ -858,7 +858,13 @@ mod tests {
let sig = calculate_hmac_signature(HmacAlgorithm::Sha256, secret, payload);
let encoded = encode_hmac_signature(Encoding::Hex, &sig);
let data = make_auth_data(payload, &encoded, None, HmacAlgorithm::Sha256, Encoding::Hex);
let data = make_auth_data(
payload,
&encoded,
None,
HmacAlgorithm::Sha256,
Encoding::Hex,
);
assert!(verify_hmac_signature(data, secret).is_ok());
}
@@ -886,7 +892,13 @@ mod tests {
let sig = calculate_hmac_signature(HmacAlgorithm::Sha512, secret, payload);
let encoded = encode_hmac_signature(Encoding::Hex, &sig);
let data = make_auth_data(payload, &encoded, None, HmacAlgorithm::Sha512, Encoding::Hex);
let data = make_auth_data(
payload,
&encoded,
None,
HmacAlgorithm::Sha512,
Encoding::Hex,
);
assert!(verify_hmac_signature(data, secret).is_ok());
}
@@ -939,7 +951,13 @@ mod tests {
let sig = calculate_hmac_signature(HmacAlgorithm::Sha256, secret, payload);
let encoded = encode_hmac_signature(Encoding::Hex, &sig);
let data = make_auth_data(payload, &encoded, None, HmacAlgorithm::Sha256, Encoding::Hex);
let data = make_auth_data(
payload,
&encoded,
None,
HmacAlgorithm::Sha256,
Encoding::Hex,
);
let result = verify_hmac_signature(data, "wrong_key");
assert!(matches!(result, Err(AuthenticationError::InvalidSignature)));
}
@@ -1045,10 +1063,7 @@ mod tests {
#[test]
fn test_hmac_algorithm_serde() {
assert_eq!(
serde_json::to_value(HmacAlgorithm::Sha1).unwrap(),
"sha1"
);
assert_eq!(serde_json::to_value(HmacAlgorithm::Sha1).unwrap(), "sha1");
assert_eq!(
serde_json::to_value(HmacAlgorithm::Sha256).unwrap(),
"sha256"
@@ -1339,7 +1354,13 @@ mod tests {
fn test_twitch_authenticate_valid_notification() {
let secret = "twitch_secret";
let payload = r#"{"subscription":{},"event":{"user_id":"123"}}"#.to_string();
let headers = twitch_headers(secret, &payload, "msg-123", "2024-01-01T00:00:00Z", "notification");
let headers = twitch_headers(
secret,
&payload,
"msg-123",
"2024-01-01T00:00:00Z",
"notification",
);
let method = AuthenticationMethod::Signature(SignatureAuthentication {
signature_provider: WebhookType::Twitch,
@@ -1441,7 +1462,8 @@ mod tests {
#[test]
fn test_zoom_non_challenge_returns_none() {
let payload = r#"{"event":"meeting.started","event_ts":1234567890,"payload":{"plainToken":"abc"}}"#;
let payload =
r#"{"event":"meeting.started","event_ts":1234567890,"payload":{"plainToken":"abc"}}"#;
let handler = WebhookType::Zoom.get_webhook_handler().unwrap();
let config_data = SignatureConfigData { secret_key: "secret" };
@@ -1461,10 +1483,7 @@ mod tests {
let encoded = encode_hmac_signature(Encoding::Hex, &sig);
let mut headers = HeaderMap::new();
headers.insert(
"X-My-Signature",
HeaderValue::from_str(&encoded).unwrap(),
);
headers.insert("X-My-Signature", HeaderValue::from_str(&encoded).unwrap());
let method = AuthenticationMethod::Signature(SignatureAuthentication {
signature_provider: WebhookType::Custom,
@@ -1736,8 +1755,7 @@ mod tests {
#[test]
fn test_error_missing_header_is_400() {
let response =
AuthenticationError::MissingHeader("X-Sig".to_string()).into_response();
let response = AuthenticationError::MissingHeader("X-Sig".to_string()).into_response();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
+20 -6
View File
@@ -39,7 +39,7 @@ pub struct TriggerRoute {
pub request_type: RequestType,
pub authentication_method: AuthenticationMethod,
pub edited_by: String,
pub email: String,
pub permissioned_as: String,
pub static_asset_config: Option<sqlx::types::Json<S3Object>>,
pub is_static_website: bool,
pub authentication_resource_path: Option<String>,
@@ -251,7 +251,7 @@ pub async fn refresh_routers(db: &DB) -> Result<(bool, RwLockReadGuard<'_, Route
request_type AS "request_type: _",
authentication_method AS "authentication_method: _",
edited_by,
email,
permissioned_as,
static_asset_config AS "static_asset_config: _",
wrap_body,
raw_string,
@@ -459,7 +459,13 @@ mod tests {
#[test]
fn test_http_method_serde_roundtrip() {
for method in [HttpMethod::Get, HttpMethod::Post, HttpMethod::Put, HttpMethod::Delete, HttpMethod::Patch] {
for method in [
HttpMethod::Get,
HttpMethod::Post,
HttpMethod::Put,
HttpMethod::Delete,
HttpMethod::Patch,
] {
let json = serde_json::to_value(method).unwrap();
let deserialized: HttpMethod = serde_json::from_value(json).unwrap();
assert_eq!(method, deserialized);
@@ -487,7 +493,10 @@ mod tests {
fn test_request_type_serialize_values() {
assert_eq!(serde_json::to_value(RequestType::Sync).unwrap(), "sync");
assert_eq!(serde_json::to_value(RequestType::Async).unwrap(), "async");
assert_eq!(serde_json::to_value(RequestType::SyncSse).unwrap(), "sync_sse");
assert_eq!(
serde_json::to_value(RequestType::SyncSse).unwrap(),
"sync_sse"
);
}
// --- AuthenticationMethod serde ---
@@ -523,8 +532,13 @@ mod tests {
#[test]
fn test_validate_auth_custom_script_requires_raw() {
assert!(validate_authentication_method(AuthenticationMethod::CustomScript, None).is_err());
assert!(validate_authentication_method(AuthenticationMethod::CustomScript, Some(false)).is_err());
assert!(validate_authentication_method(AuthenticationMethod::CustomScript, Some(true)).is_ok());
assert!(
validate_authentication_method(AuthenticationMethod::CustomScript, Some(false))
.is_err()
);
assert!(
validate_authentication_method(AuthenticationMethod::CustomScript, Some(true)).is_ok()
);
}
#[test]
+8 -8
View File
@@ -66,14 +66,14 @@ impl TriggerCrud for MqttTrigger {
async fn create_trigger(
&self,
db: &DB,
_db: &DB,
tx: &mut PgConnection,
authed: &ApiAuthed,
w_id: &str,
trigger: TriggerData<Self::TriggerConfigRequest>,
) -> Result<()> {
let resolved_edited_by = trigger.base.resolve_edited_by(authed);
let resolved_email = trigger.base.resolve_email(authed, db, w_id).await?;
let resolved_permissioned_as = trigger.base.resolve_permissioned_as(authed);
let subscribe_topics = trigger
.config
.subscribe_topics
@@ -96,7 +96,7 @@ impl TriggerCrud for MqttTrigger {
path,
script_path,
is_flow,
email,
permissioned_as,
mode,
edited_by,
error_handler_path,
@@ -116,7 +116,7 @@ impl TriggerCrud for MqttTrigger {
trigger.base.path,
trigger.base.script_path,
trigger.base.is_flow,
resolved_email,
resolved_permissioned_as,
trigger.base.mode() as _,
&resolved_edited_by,
trigger.error_handling.error_handler_path,
@@ -131,7 +131,7 @@ impl TriggerCrud for MqttTrigger {
async fn update_trigger(
&self,
db: &DB,
_db: &DB,
tx: &mut PgConnection,
authed: &ApiAuthed,
workspace_id: &str,
@@ -139,7 +139,7 @@ impl TriggerCrud for MqttTrigger {
trigger: TriggerData<Self::TriggerConfigRequest>,
) -> Result<()> {
let resolved_edited_by = trigger.base.resolve_edited_by(authed);
let resolved_email = trigger.base.resolve_email(authed, db, workspace_id).await?;
let resolved_permissioned_as = trigger.base.resolve_permissioned_as(authed);
let subscribe_topics = trigger
.config
.subscribe_topics
@@ -163,7 +163,7 @@ impl TriggerCrud for MqttTrigger {
v5_config = $6,
is_flow = $7,
edited_by = $8,
email = $9,
permissioned_as = $9,
script_path = $10,
path = $11,
edited_at = now(),
@@ -184,7 +184,7 @@ impl TriggerCrud for MqttTrigger {
v5_config as Option<SqlxJson<MqttV5Config>>,
trigger.base.is_flow,
&resolved_edited_by,
resolved_email,
resolved_permissioned_as,
trigger.base.script_path,
trigger.base.path,
workspace_id,
@@ -71,7 +71,7 @@ impl TriggerCrud for PostgresTrigger {
trigger: TriggerData<Self::TriggerConfigRequest>,
) -> Result<()> {
let resolved_edited_by = trigger.base.resolve_edited_by(authed);
let resolved_email = trigger.base.resolve_email(authed, db, w_id).await?;
let resolved_permissioned_as = trigger.base.resolve_permissioned_as(authed);
let Self::TriggerConfigRequest {
postgres_resource_path,
publication_name,
@@ -122,7 +122,7 @@ impl TriggerCrud for PostgresTrigger {
is_flow,
mode,
edited_by,
email,
permissioned_as,
edited_at,
error_handler_path,
error_handler_args,
@@ -140,7 +140,7 @@ impl TriggerCrud for PostgresTrigger {
trigger.base.is_flow,
trigger.base.mode() as _,
&resolved_edited_by,
resolved_email,
resolved_permissioned_as,
trigger.error_handling.error_handler_path,
trigger.error_handling.error_handler_args as _,
trigger.error_handling.retry as _
@@ -160,7 +160,7 @@ impl TriggerCrud for PostgresTrigger {
trigger: TriggerData<Self::TriggerConfigRequest>,
) -> Result<()> {
let resolved_edited_by = trigger.base.resolve_edited_by(authed);
let resolved_email = trigger.base.resolve_email(authed, db, w_id).await?;
let resolved_permissioned_as = trigger.base.resolve_permissioned_as(authed);
let Self::TriggerConfigRequest {
replication_slot_name,
publication_name,
@@ -223,7 +223,7 @@ impl TriggerCrud for PostgresTrigger {
path = $5,
is_flow = $6,
edited_by = $7,
email = $8,
permissioned_as = $8,
edited_at = now(),
server_id = NULL,
error = NULL,
@@ -240,7 +240,7 @@ impl TriggerCrud for PostgresTrigger {
trigger.base.path,
trigger.base.is_flow,
&resolved_edited_by,
resolved_email,
resolved_permissioned_as,
w_id,
path,
trigger.error_handling.error_handler_path,
@@ -72,14 +72,14 @@ impl TriggerCrud for WebsocketTrigger {
async fn create_trigger(
&self,
db: &DB,
_db: &DB,
tx: &mut PgConnection,
authed: &ApiAuthed,
w_id: &str,
trigger: TriggerData<Self::TriggerConfigRequest>,
) -> Result<()> {
let resolved_edited_by = trigger.base.resolve_edited_by(authed);
let resolved_email = trigger.base.resolve_email(authed, db, w_id).await?;
let resolved_permissioned_as = trigger.base.resolve_permissioned_as(authed);
let filters = trigger
.config
.filters
@@ -108,7 +108,7 @@ impl TriggerCrud for WebsocketTrigger {
edited_by,
can_return_message,
can_return_error_result,
email,
permissioned_as,
edited_at,
error_handler_path,
error_handler_args,
@@ -132,7 +132,7 @@ impl TriggerCrud for WebsocketTrigger {
&resolved_edited_by,
trigger.config.can_return_message,
trigger.config.can_return_error_result,
resolved_email,
resolved_permissioned_as,
trigger.error_handling.error_handler_path,
trigger.error_handling.error_handler_args as _,
trigger.error_handling.retry as _
@@ -144,7 +144,7 @@ impl TriggerCrud for WebsocketTrigger {
async fn update_trigger(
&self,
db: &DB,
_db: &DB,
tx: &mut PgConnection,
authed: &ApiAuthed,
w_id: &str,
@@ -152,7 +152,7 @@ impl TriggerCrud for WebsocketTrigger {
trigger: TriggerData<Self::TriggerConfigRequest>,
) -> Result<()> {
let resolved_edited_by = trigger.base.resolve_edited_by(authed);
let resolved_email = trigger.base.resolve_email(authed, db, w_id).await?;
let resolved_permissioned_as = trigger.base.resolve_permissioned_as(authed);
let filters = trigger
.config
.filters
@@ -181,7 +181,7 @@ impl TriggerCrud for WebsocketTrigger {
initial_messages = $6,
url_runnable_args = $7,
edited_by = $8,
email = $9,
permissioned_as = $9,
can_return_message = $10,
can_return_error_result = $11,
edited_at = now(),
@@ -205,7 +205,7 @@ impl TriggerCrud for WebsocketTrigger {
.map(|v| SqlxJson(serde_json::value::to_raw_value(&v).unwrap()))
as Option<SqlxJson<Box<RawValue>>>,
&resolved_edited_by,
resolved_email,
resolved_permissioned_as,
trigger.config.can_return_message,
trigger.config.can_return_error_result,
w_id,
@@ -23,7 +23,7 @@ pub struct SuspendedTrigger {
pub script_path: String,
pub is_flow: bool,
pub edited_by: String,
pub email: String,
pub permissioned_as: String,
pub edited_at: DateTime<Utc>,
pub error_handler_path: Option<String>,
pub error_handler_args: Option<sqlx::types::Json<HashMap<String, serde_json::Value>>>,
@@ -52,7 +52,7 @@ async fn get_suspended_trigger(
"script_path",
"is_flow",
"edited_by",
"email",
"permissioned_as",
"edited_at",
"error_handler_path",
"error_handler_args",
+11 -10
View File
@@ -144,7 +144,7 @@ pub trait TriggerCrud: Send + Sync + 'static {
"script_path",
"is_flow",
"edited_by",
"email",
"permissioned_as",
"edited_at",
"extra_perms",
"mode",
@@ -222,6 +222,7 @@ pub trait TriggerCrud: Send + Sync + 'static {
path: &str,
mode: &TriggerMode,
) -> Result<bool> {
let permissioned_as = windmill_common::users::username_to_permissioned_as(&authed.username);
let updated = if Self::SUPPORTS_SERVER_STATE {
sqlx::query(&format!(
r#"
@@ -229,7 +230,7 @@ pub trait TriggerCrud: Send + Sync + 'static {
{}
SET
mode = $1,
email = $2,
permissioned_as = $2,
edited_by = $3,
edited_at = now(),
server_id = NULL,
@@ -241,7 +242,7 @@ pub trait TriggerCrud: Send + Sync + 'static {
Self::TABLE_NAME
))
.bind(mode)
.bind(&authed.email)
.bind(&permissioned_as)
.bind(&authed.username)
.bind(workspace_id)
.bind(path)
@@ -255,7 +256,7 @@ pub trait TriggerCrud: Send + Sync + 'static {
{}
SET
mode = $1,
email = $2,
permissioned_as = $2,
edited_by = $3,
edited_at = now()
WHERE
@@ -265,7 +266,7 @@ pub trait TriggerCrud: Send + Sync + 'static {
Self::TABLE_NAME
))
.bind(mode)
.bind(&authed.email)
.bind(&permissioned_as)
.bind(&authed.username)
.bind(workspace_id)
.bind(path)
@@ -322,7 +323,7 @@ pub trait TriggerCrud: Send + Sync + 'static {
"script_path",
"is_flow",
"edited_by",
"email",
"permissioned_as",
"edited_at",
"extra_perms",
"mode",
@@ -417,8 +418,8 @@ async fn create_trigger<T: TriggerCrud>(
let new_path = new_trigger.base.path.clone();
let on_behalf_of_info = windmill_common::check_on_behalf_of_preservation(
new_trigger.base.email.as_deref(),
new_trigger.base.preserve_email.unwrap_or(false),
new_trigger.base.permissioned_as.as_deref(),
new_trigger.base.preserve_permissioned_as.unwrap_or(false),
&authed,
&authed.username,
);
@@ -535,8 +536,8 @@ async fn update_trigger<T: TriggerCrud>(
let new_path = edit_trigger.base.path.to_string();
let on_behalf_of_info = windmill_common::check_on_behalf_of_preservation(
edit_trigger.base.email.as_deref(),
edit_trigger.base.preserve_email.unwrap_or(false),
edit_trigger.base.permissioned_as.as_deref(),
edit_trigger.base.preserve_permissioned_as.unwrap_or(false),
&authed,
&authed.username,
);
+33 -26
View File
@@ -15,7 +15,7 @@ use serde_json::value::RawValue;
use sql_builder::SqlBuilder;
use sqlx::{FromRow, Row};
use tokio::sync::RwLock;
use windmill_api_auth::{fetch_api_authed, ApiAuthed};
use windmill_api_auth::ApiAuthed;
use windmill_common::{
error::{Error, Result},
jobs::JobTriggerKind,
@@ -62,7 +62,7 @@ pub trait Listener: TriggerCrud + TriggerJobArgs {
"script_path",
"is_flow",
"edited_by",
"email",
"permissioned_as",
"edited_at",
"extra_perms",
"mode",
@@ -98,8 +98,8 @@ pub trait Listener: TriggerCrud + TriggerJobArgs {
path: trigger.base.path,
workspace_id: trigger.base.workspace_id,
is_flow: trigger.base.is_flow,
username: trigger.base.edited_by,
email: trigger.base.email,
edited_by: trigger.base.edited_by,
permissioned_as: trigger.base.permissioned_as,
script_path: trigger.base.script_path,
trigger_config: trigger.config,
error_handling: Some(trigger.error_handling),
@@ -120,7 +120,6 @@ pub trait Listener: TriggerCrud + TriggerJobArgs {
"is_flow",
"workspace_id",
"owner AS username",
"email",
"trigger_config",
];
@@ -144,17 +143,21 @@ pub trait Listener: TriggerCrud + TriggerJobArgs {
let captures = captures
.into_iter()
.map(|capture| ListeningTrigger {
username: capture.username,
path: capture.path,
workspace_id: capture.workspace_id,
script_path: "".to_string(),
email: capture.email,
trigger_config: capture.trigger_config,
trigger_mode: false,
is_flow: capture.is_flow,
error_handling: None,
suspended_mode: false,
.map(|capture| {
let permissioned_as =
windmill_common::users::username_to_permissioned_as(&capture.username);
ListeningTrigger {
edited_by: capture.username,
path: capture.path,
workspace_id: capture.workspace_id,
script_path: "".to_string(),
permissioned_as,
trigger_config: capture.trigger_config,
trigger_mode: false,
is_flow: capture.is_flow,
error_handling: None,
suspended_mode: false,
}
})
.collect_vec();
@@ -571,7 +574,7 @@ pub trait Listener: TriggerCrud + TriggerJobArgs {
&Self::TRIGGER_KIND,
main_args,
preprocessor_args,
&listening_trigger.username,
&listening_trigger.edited_by,
)
.await
{
@@ -792,7 +795,6 @@ where
is_flow: bool,
workspace_id: String,
username: String,
email: String,
#[serde(flatten)]
trigger_config: T,
}
@@ -810,7 +812,6 @@ where
is_flow: row.try_get("is_flow")?,
workspace_id: row.try_get("workspace_id")?,
username: row.try_get("username")?,
email: row.try_get("email")?,
trigger_config,
})
}
@@ -821,8 +822,8 @@ pub struct ListeningTrigger<T> {
pub path: String,
pub is_flow: bool,
pub workspace_id: String,
pub username: String,
pub email: String,
pub edited_by: String,
pub permissioned_as: String,
pub trigger_config: T,
pub script_path: String,
pub trigger_mode: bool,
@@ -831,13 +832,19 @@ pub struct ListeningTrigger<T> {
}
impl<T> ListeningTrigger<T> {
pub async fn authed(&self, db: &DB, username: &str) -> Result<ApiAuthed> {
fetch_api_authed(
self.username.clone(),
self.email.clone(),
pub async fn authed(&self, db: &DB, trigger_kind: &str) -> Result<ApiAuthed> {
let email = windmill_common::users::get_email_from_permissioned_as(
&self.permissioned_as,
&self.workspace_id,
db,
Some(format!("{}-{}", username, self.path)),
)
.await?;
windmill_api_auth::fetch_api_authed_from_permissioned_as(
self.permissioned_as.clone(),
email,
&self.workspace_id,
db,
Some(format!("{}-{}", trigger_kind, self.path)),
)
.await
}
+12 -33
View File
@@ -8,9 +8,9 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::{types::Json as SqlxJson, FromRow, Pool, Postgres};
use sqlx::{types::Json as SqlxJson, FromRow};
use std::{collections::HashMap, fmt::Debug};
use windmill_common::{db::Authable, error::Result, jobs::JobTriggerKind};
use windmill_common::{db::Authable, jobs::JobTriggerKind};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
@@ -36,7 +36,7 @@ pub struct BaseTrigger {
pub mode: TriggerMode,
pub is_flow: bool,
pub edited_by: String,
pub email: String,
pub permissioned_as: String,
pub edited_at: DateTime<Utc>,
pub extra_perms: Option<serde_json::Value>,
}
@@ -103,12 +103,12 @@ pub struct BaseTriggerData {
#[deprecated(note = "Use mode instead")]
enabled: Option<bool>, // Kept for backwards compatibility, use mode instead
mode: Option<TriggerMode>,
/// Optional email for deployment - when set, the trigger will run jobs as this user
/// Optional permissioned_as for deployment - when set, the trigger will run jobs as this user
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
/// If true and user is admin/wm_deployers, preserve the provided email instead of using deploying user's email
pub permissioned_as: Option<String>,
/// If true and user is admin/wm_deployers, preserve the provided permissioned_as instead of using deploying user's
#[serde(skip_serializing_if = "Option::is_none")]
pub preserve_email: Option<bool>,
pub preserve_permissioned_as: Option<bool>,
}
impl BaseTriggerData {
@@ -123,39 +123,18 @@ impl BaseTriggerData {
)
}
pub async fn resolve_email(
&self,
authed: &impl Authable,
db: &Pool<Postgres>,
w_id: &str,
) -> Result<String> {
if let Some(ref username) = self.email {
if self.preserve_email.unwrap_or(false)
pub fn resolve_permissioned_as(&self, authed: &impl Authable) -> String {
if let Some(ref permissioned_as) = self.permissioned_as {
if self.preserve_permissioned_as.unwrap_or(false)
&& windmill_common::can_preserve_on_behalf_of(authed)
{
let email = sqlx::query_scalar!(
"SELECT email FROM usr WHERE username = $1 AND workspace_id = $2",
username,
w_id
)
.fetch_optional(db)
.await?;
if let Some(email) = email {
return Ok(email);
}
return permissioned_as.clone();
}
}
Ok(authed.email().to_string())
windmill_common::users::username_to_permissioned_as(authed.username())
}
pub fn resolve_edited_by(&self, authed: &impl Authable) -> String {
if let Some(ref username) = self.email {
if self.preserve_email.unwrap_or(false)
&& windmill_common::can_preserve_on_behalf_of(authed)
{
return username.clone();
}
}
authed.username().to_string()
}
}
+1
View File
@@ -18,6 +18,7 @@ pub struct Schedule {
pub args: Option<sqlx::types::Json<Box<serde_json::value::RawValue>>>,
pub extra_perms: serde_json::Value,
pub email: String,
pub permissioned_as: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
+36
View File
@@ -5479,6 +5479,9 @@ properties:
script_path:
type: string
description: Path to the script or flow to execute when triggered
permissioned_as:
type: string
description: The user or group this trigger runs as (permissioned_as)
is_flow:
type: boolean
description: True if script_path points to a flow, false if it points to a script
@@ -5560,6 +5563,7 @@ properties:
description: Retry configuration for failed module executions
required:
- script_path
- permissioned_as
- is_flow
- gcp_resource_path
- topic_id
@@ -5572,6 +5576,9 @@ properties:
script_path:
type: string
description: Path to the script or flow to execute when triggered
permissioned_as:
type: string
description: The user or group this trigger runs as (permissioned_as)
is_flow:
type: boolean
description: True if script_path points to a flow, false if it points to a script
@@ -5683,6 +5690,7 @@ properties:
description: Retry configuration for failed module executions
required:
- script_path
- permissioned_as
- is_flow
- route_path
- request_type
@@ -5698,6 +5706,9 @@ properties:
script_path:
type: string
description: Path to the script or flow to execute when triggered
permissioned_as:
type: string
description: The user or group this trigger runs as (permissioned_as)
is_flow:
type: boolean
description: True if script_path points to a flow, false if it points to a script
@@ -5775,6 +5786,7 @@ properties:
description: Retry configuration for failed module executions
required:
- script_path
- permissioned_as
- is_flow
- kafka_resource_path
- group_id
@@ -5786,6 +5798,9 @@ properties:
script_path:
type: string
description: Path to the script or flow to execute when triggered
permissioned_as:
type: string
description: The user or group this trigger runs as (permissioned_as)
is_flow:
type: boolean
description: True if script_path points to a flow, false if it points to a script
@@ -5863,6 +5878,7 @@ properties:
description: Retry configuration for failed module executions
required:
- script_path
- permissioned_as
- is_flow
- subscribe_topics
- mqtt_resource_path
@@ -5872,6 +5888,9 @@ properties:
script_path:
type: string
description: Path to the script or flow to execute when triggered
permissioned_as:
type: string
description: The user or group this trigger runs as (permissioned_as)
is_flow:
type: boolean
description: True if script_path points to a flow, false if it points to a script
@@ -5935,6 +5954,7 @@ properties:
description: Retry configuration for failed module executions
required:
- script_path
- permissioned_as
- is_flow
- nats_resource_path
- use_jetstream
@@ -5945,6 +5965,9 @@ properties:
script_path:
type: string
description: Path to the script or flow to execute when triggered
permissioned_as:
type: string
description: The user or group this trigger runs as (permissioned_as)
is_flow:
type: boolean
description: True if script_path points to a flow, false if it points to a script
@@ -6001,6 +6024,7 @@ properties:
description: Retry configuration for failed module executions
required:
- script_path
- permissioned_as
- is_flow
- postgres_resource_path
- replication_slot_name
@@ -6027,6 +6051,9 @@ properties:
args:
type: object
description: The arguments to pass to the script or flow
permissioned_as:
type: string
description: The user or group this schedule runs as (e.g., 'u/admin' or 'g/mygroup')
on_failure:
type: string
description: Path to a script or flow to run when the scheduled job fails
@@ -6128,12 +6155,16 @@ required:
- timezone
- is_flow
- enabled
- permissioned_as
`,
"sqs_trigger": `type: object
properties:
script_path:
type: string
description: Path to the script or flow to execute when triggered
permissioned_as:
type: string
description: The user or group this trigger runs as (permissioned_as)
is_flow:
type: boolean
description: True if script_path points to a flow, false if it points to a script
@@ -6196,6 +6227,7 @@ properties:
description: Retry configuration for failed module executions
required:
- script_path
- permissioned_as
- is_flow
- queue_url
- aws_resource_path
@@ -6206,6 +6238,9 @@ properties:
script_path:
type: string
description: Path to the script or flow to execute when triggered
permissioned_as:
type: string
description: The user or group this trigger runs as (permissioned_as)
is_flow:
type: boolean
description: True if script_path points to a flow, false if it points to a script
@@ -6282,6 +6317,7 @@ properties:
description: Retry configuration for failed module executions
required:
- script_path
- permissioned_as
- is_flow
- url
- filters
@@ -44,11 +44,12 @@
import { userWorkspaces, workspaceStore } from '$lib/stores'
import type { Kind } from '$lib/utils_deployable'
import { deployItem, getItemValue, getOnBehalfOfEmail } from '$lib/utils_workspace_deploy'
import { deployItem, getItemValue, getOnBehalfOf } from '$lib/utils_workspace_deploy'
import Tooltip from './Tooltip.svelte'
import OnBehalfOfSelector, {
needsOnBehalfOfSelection,
type OnBehalfOfChoice
type OnBehalfOfChoice,
type OnBehalfOfDetails
} from './OnBehalfOfSelector.svelte'
import { sendUserToast } from '$lib/toast'
import { deepEqual } from 'fast-equals'
@@ -120,7 +121,7 @@
// Source workspace on_behalf_of emails (keyed by workspace/kind:path)
let onBehalfOfInfo = $state<Record<string, string | undefined>>({})
let onBehalfOfChoice = $state<Record<string, OnBehalfOfChoice>>({})
let customOnBehalfOfEmails = $state<Record<string, string>>({})
let customOnBehalfOf = $state<Record<string, OnBehalfOfDetails>>({})
let deployTargetWorkspace = $derived(mergeIntoParent ? parentWorkspaceId : currentWorkspaceId)
function getItemKey(diff: WorkspaceItemDiff): string {
@@ -191,7 +192,7 @@
if (onBehalfOfInfo[workspacedKey] !== undefined) continue
try {
onBehalfOfInfo[workspacedKey] = await getOnBehalfOfEmail(
onBehalfOfInfo[workspacedKey] = await getOnBehalfOf(
diff.kind as Kind,
diff.path,
workspace
@@ -203,21 +204,21 @@
}
}
// Get source workspace email for an item
function getSourceEmail(itemKey: string): string | undefined {
// Get source workspace on_behalf_of value for an item (email for runnables, permissioned_as for triggers)
function getSourceOnBehalfOf(itemKey: string): string | undefined {
const sourceWorkspace = mergeIntoParent ? currentWorkspaceId : parentWorkspaceId
return onBehalfOfInfo[getWorkspacedKey(sourceWorkspace, itemKey)]
}
// Get target workspace email for an item (existing item in destination)
function getTargetEmail(itemKey: string): string | undefined {
// Get target workspace on_behalf_of value for an item (existing item in destination)
function getTargetOnBehalfOf(itemKey: string): string | undefined {
const targetWorkspace = mergeIntoParent ? parentWorkspaceId : currentWorkspaceId
return onBehalfOfInfo[getWorkspacedKey(targetWorkspace, itemKey)]
}
// Check if an item needs on_behalf_of selection
function itemNeedsOnBehalfOfSelection(itemKey: string, kind: string): boolean {
return needsOnBehalfOfSelection(kind, getSourceEmail(itemKey))
return needsOnBehalfOfSelection(kind, getSourceOnBehalfOf(itemKey))
}
// Check if all required on_behalf_of selections are made
@@ -231,12 +232,18 @@
})
)
// Get the email to use for deployment based on user's choice
function getOnBehalfOfEmailForDeploy(itemKey: string): string | undefined {
/**
* Get the on_behalf_of value for deployment based on user's choice.
* Returns an email for flows/scripts/apps, or permissioned_as (u/username, g/group) for triggers/schedules.
*/
function getOnBehalfOfForDeploy(itemKey: string, kind: Kind): string | undefined {
const choice = onBehalfOfChoice[itemKey]
if (choice === 'target') return getTargetEmail(itemKey)
if (choice === 'custom') return customOnBehalfOfEmails[itemKey]
// 'me' or undefined = don't pass, backend will use deploying user's email
if (choice === 'target') return getTargetOnBehalfOf(itemKey)
if (choice === 'custom') {
const details = customOnBehalfOf[itemKey]
return kind === 'trigger' ? details?.permissionedAs : details?.email
}
// 'me' or undefined = don't pass, backend will use deploying user's identity
return undefined
}
@@ -304,7 +311,7 @@
path,
workspaceFrom,
workspaceTo: workspaceToDeployTo,
onBehalfOfEmail: getOnBehalfOfEmailForDeploy(statusPath)
onBehalfOf: getOnBehalfOfForDeploy(statusPath, kind)
})
if (result.success) {
@@ -868,7 +875,7 @@
{#snippet itemActions(item)}
{@const diff = item.diff as WorkspaceItemDiff}
{@const key = item.key}
{@const targetEmail = getTargetEmail(key)}
{@const targetOnBehalfOf = getTargetOnBehalfOf(key)}
{@const isConflict = diff.ahead > 0 && diff.behind > 0}
{@const existsInBothWorkspaces = !(
(diff.exists_in_fork && !diff.exists_in_source) ||
@@ -878,15 +885,15 @@
{#if itemNeedsOnBehalfOfSelection(key, diff.kind)}
<OnBehalfOfSelector
targetWorkspace={deployTargetWorkspace}
{targetEmail}
targetValue={targetOnBehalfOf}
selected={onBehalfOfChoice[key]}
onSelect={(choice, email) => {
onSelect={(choice, details) => {
onBehalfOfChoice[key] = choice
if (email) customOnBehalfOfEmails[key] = email
if (details) customOnBehalfOf[key] = details
}}
kind={diff.kind}
canPreserve={canPreserveOnBehalfOf}
customEmail={customOnBehalfOfEmails[key]}
customValue={customOnBehalfOf[key]?.permissionedAs}
/>
{/if}
{#if diff.kind === 'raw_app'}
@@ -26,7 +26,7 @@
checkItemExists,
deployItem,
getItemValue,
getOnBehalfOfEmail
getOnBehalfOf
} from '$lib/utils_workspace_deploy'
import type { App } from './apps/types'
import { getAllGridItems } from './apps/editor/appUtils'
@@ -35,7 +35,8 @@
import WorkspaceDeployLayout from './WorkspaceDeployLayout.svelte'
import OnBehalfOfSelector, {
needsOnBehalfOfSelection,
type OnBehalfOfChoice
type OnBehalfOfChoice,
type OnBehalfOfDetails
} from './OnBehalfOfSelector.svelte'
import ParentWorkspaceProtectionAlert from './ParentWorkspaceProtectionAlert.svelte'
@@ -77,7 +78,7 @@
// Target workspace on_behalf_of emails (keyed by kind:path)
let targetOnBehalfOfInfo = $state<Record<string, string | undefined>>({})
let onBehalfOfChoice = $state<Record<string, OnBehalfOfChoice>>({})
let customOnBehalfOfEmails = $state<Record<string, string>>({})
let customOnBehalfOf = $state<Record<string, OnBehalfOfDetails>>({})
let canPreserveOnBehalfOf = $state(false)
// Check if an item needs on_behalf_of selection
@@ -85,12 +86,18 @@
return needsOnBehalfOfSelection(kind, sourceOnBehalfOfInfo[statusPath])
}
// Get the email to use for deployment based on user's choice
function getOnBehalfOfEmailForDeploy(statusPath: string): string | undefined {
/**
* Get the on_behalf_of value for deployment based on user's choice.
* Returns an email for flows/scripts/apps, or permissioned_as (u/username, g/group) for triggers/schedules.
*/
function getOnBehalfOfForDeploy(statusPath: string, kind: Kind): string | undefined {
const choice = onBehalfOfChoice[statusPath]
if (choice === 'target') return targetOnBehalfOfInfo[statusPath]
if (choice === 'custom') return customOnBehalfOfEmails[statusPath]
// 'me' or undefined = don't pass, backend will use deploying user's email
if (choice === 'custom') {
const details = customOnBehalfOf[statusPath]
return kind === 'trigger' ? details?.permissionedAs : details?.email
}
// 'me' or undefined = don't pass, backend will use deploying user's identity
return undefined
}
@@ -145,7 +152,7 @@
)) {
const key = computeStatusPath(dep.kind, dep.path)
try {
sourceOnBehalfOfInfo[key] = await getOnBehalfOfEmail(
sourceOnBehalfOfInfo[key] = await getOnBehalfOf(
dep.kind,
dep.path,
$workspaceStore!,
@@ -155,7 +162,7 @@
sourceOnBehalfOfInfo[key] = undefined
}
try {
targetOnBehalfOfInfo[key] = await getOnBehalfOfEmail(
targetOnBehalfOfInfo[key] = await getOnBehalfOf(
dep.kind,
dep.path,
workspaceToDeployTo!,
@@ -295,7 +302,7 @@
workspaceFrom: $workspaceStore!,
workspaceTo: workspaceToDeployTo!,
additionalInformation,
onBehalfOfEmail: getOnBehalfOfEmailForDeploy(statusPath)
onBehalfOf: getOnBehalfOfForDeploy(statusPath, kind)
})
if (result.success) {
@@ -458,21 +465,21 @@
{@const statusPath = item.key}
{@const exists = allAlreadyExists[statusPath]}
{@const status = deploymentStatus[statusPath]}
{@const targetEmail = targetOnBehalfOfInfo[statusPath]}
{@const targetValue = targetOnBehalfOfInfo[statusPath]}
<!-- On-behalf-of selector -->
{#if itemNeedsOnBehalfOfSelection(statusPath, item.kind)}
<OnBehalfOfSelector
targetWorkspace={workspaceToDeployTo!}
{targetEmail}
{targetValue}
selected={onBehalfOfChoice[statusPath]}
onSelect={(choice, email) => {
onSelect={(choice, details) => {
onBehalfOfChoice[statusPath] = choice
if (email) customOnBehalfOfEmails[statusPath] = email
if (details) customOnBehalfOf[statusPath] = details
}}
kind={item.kind}
canPreserve={canPreserveOnBehalfOf}
customEmail={customOnBehalfOfEmails[statusPath]}
customValue={customOnBehalfOf[statusPath]?.permissionedAs}
/>
{/if}
@@ -537,10 +544,10 @@
{#if hasUnselectedOnBehalfOf}
<span class="text-xs text-yellow-600">
{#if kind === 'trigger'}
You must set the "edited by" user for all triggers before deploying
You must set the "permissioned as" user for all triggers before deploying
<Tooltip class="text-yellow-600">
The "edited by" field defines which user's permissions will be applied when the
trigger runs. Make sure this is set to an appropriate user before deploying.
The "permissioned as" field defines which user's permissions will be applied when
the trigger fires. Make sure this is set appropriately before deploying.
</Tooltip>
{:else}
You must set the "on behalf of" user for all items before deploying
@@ -119,9 +119,9 @@
<div class="flex flex-col gap-6">
{#if name === 'wm_deployers'}
<Alert type="info" title="Deployer permissions">
Members of this group can preserve the original author (on_behalf_of / edited_by) when
deploying scripts, flows, apps, and triggers to this workspace. Without this permission,
deployed items will be reassigned to the deploying user.
Members of this group can preserve the original author (on_behalf_of / permissioned_as) when
deploying scripts, flows, apps, triggers, and schedules to this workspace. Without this
permission, deployed items will be reassigned to the deploying user.
</Alert>
{/if}
<Label label="Summary" for="summary">
@@ -159,14 +159,13 @@
{/if}
{#if members}
<TableCustom>
{#snippet headerRow()}
<tr >
<tr>
<th>user</th>
<th></th>
<th></th>
</tr>
{/snippet}
{/snippet}
{#snippet body()}
<tbody>
{#each members ?? [] as { member_name, role }}<tr>
@@ -303,12 +302,11 @@
{#if instance_group?.emails}
<h2 class="mt-6 text-emphasis text-xs font-semibold">Members from the instance group</h2>
<TableCustom>
{#snippet headerRow()}
<tr >
<tr>
<th>user</th>
</tr>
{/snippet}
{/snippet}
{#snippet body()}
<tbody>
{#each instance_group?.emails ?? [] as email}<tr>
@@ -1,11 +1,16 @@
<script lang="ts" module>
export type OnBehalfOfChoice = 'target' | 'me' | 'custom' | undefined
export interface OnBehalfOfDetails {
email: string
permissionedAs: string
}
/**
* Check if an item needs on_behalf_of selection.
* Shows the selector when the source item has an on_behalf_of_email set.
* Shows the selector when the source item has an on_behalf_of value set.
*/
export function needsOnBehalfOfSelection(kind: string, sourceEmail: string | undefined): boolean {
export function needsOnBehalfOfSelection(kind: string, sourceValue: string | undefined): boolean {
if (
kind !== 'flow' &&
kind !== 'script' &&
@@ -14,7 +19,7 @@
kind !== 'trigger'
)
return false
return !!sourceEmail
return !!sourceValue
}
</script>
@@ -28,31 +33,39 @@
interface Props {
targetWorkspace: string
targetEmail: string | undefined
/** The target value: email for flows/scripts, permissioned_as (u/username) for triggers */
targetValue: string | undefined
selected: OnBehalfOfChoice
onSelect: (choice: OnBehalfOfChoice, email?: string, username?: string) => void
/**
* Called when the user picks a choice.
* For 'custom', `details` contains both `email` and `permissionedAs` (u/{username}).
* Callers pick whichever format they need.
*/
onSelect: (choice: OnBehalfOfChoice, details?: OnBehalfOfDetails) => void
kind: string
canPreserve: boolean
/** The email of the custom-selected user (for display) */
customEmail?: string | undefined
/** The value of the custom-selected user (for display) */
customValue?: string | undefined
/** When false, labels say "current" instead of "target" and modal text refers to "this workspace" */
isDeployment?: boolean
}
let {
targetWorkspace,
targetEmail,
targetValue,
selected,
onSelect,
kind,
canPreserve,
customEmail,
customValue,
isDeployment = true
}: Props = $props()
const isTrigger = $derived(kind === 'trigger')
let label = $derived(
kind === 'trigger'
? 'Set the user this will be recorded as edited by:'
isTrigger
? 'Set the user this will be permissioned as:'
: 'Set the user this will be run on behalf of:'
)
@@ -74,13 +87,17 @@
// Fetch users eagerly so we can resolve usernames for display
loadUsers()
function resolveUsername(email: string | undefined): string | undefined {
if (!email) return undefined
return users.find((u) => u.email === email)?.username ?? email
/** Resolve a value to a display name, always showing u/username format */
function resolveDisplayName(value: string | undefined): string | undefined {
if (!value) return undefined
if (value.startsWith('u/') || value.startsWith('g/')) return value
const username = users.find((u) => u.email === value)?.username
return username ? `u/${username}` : value
}
let targetUsername = $derived(resolveUsername(targetEmail))
let customUsername = $derived(resolveUsername(customEmail))
let targetDisplayName = $derived(resolveDisplayName(targetValue))
let customDisplayName = $derived(resolveDisplayName(customValue))
let myDisplayName = $derived($userStore?.username ? `u/${$userStore.username}` : undefined)
let activeUsers = $derived(users.filter((u) => !u.disabled))
let filteredUsers = $derived(
@@ -98,7 +115,7 @@
// Preselect "target" when available and user has permission to preserve
$effect(() => {
if (selected === undefined && targetEmail && canPreserve) {
if (selected === undefined && targetValue && canPreserve) {
onSelect('target')
}
})
@@ -110,14 +127,14 @@
}
function selectUser(user: User) {
onSelect('custom', user.email, user.username)
onSelect('custom', { email: user.email, permissionedAs: `u/${user.username}` })
modalOpen = false
}
let selectedDisplayName = $derived.by(() => {
if (selected === 'target') return targetUsername
if (selected === 'me') return $userStore?.username
if (selected === 'custom') return customUsername
if (selected === 'target') return targetDisplayName
if (selected === 'me') return myDisplayName
if (selected === 'custom') return customDisplayName
return undefined
})
</script>
@@ -135,7 +152,7 @@
<div class="p-3 flex flex-col gap-2 min-w-48">
<div class="text-xs font-medium text-secondary mb-1">{label}</div>
<!-- Target option -->
{#if targetEmail}
{#if targetValue}
<button
class="flex items-center gap-2 px-2 py-1.5 rounded text-left text-xs hover:bg-surface-hover {!canPreserve
? 'opacity-50 cursor-not-allowed'
@@ -144,7 +161,7 @@
onclick={() => onSelect('target')}
>
<Check class="w-3 h-3 {selected === 'target' ? 'opacity-100' : 'opacity-0'}" />
<span class="truncate max-w-40">{targetUsername}</span>
<span class="truncate max-w-40">{targetDisplayName}</span>
<span class="text-xs text-tertiary">{isDeployment ? '(target)' : '(current)'}</span>
</button>
{/if}
@@ -154,7 +171,7 @@
onclick={() => onSelect('me')}
>
<Check class="w-3 h-3 {selected === 'me' ? 'opacity-100' : 'opacity-0'}" />
<span class="truncate max-w-40">{$userStore?.username}</span>
<span class="truncate max-w-40">{myDisplayName}</span>
<span class="text-xs text-tertiary">(me)</span>
</button>
<!-- Custom / Pick from workspace -->
@@ -168,9 +185,9 @@
openModal()
}}
>
{#if selected === 'custom' && customUsername}
{#if selected === 'custom' && customDisplayName}
<Check class="w-3 h-3 opacity-100" />
<span class="truncate max-w-40">{customUsername}</span>
<span class="truncate max-w-40">{customDisplayName}</span>
<span class="text-xs text-tertiary">(custom)</span>
{:else}
<Check class="w-3 h-3 opacity-0" />
@@ -186,10 +203,11 @@
<Modal title="Select a user" bind:open={modalOpen} kind="X">
<div class="flex flex-col gap-4">
<div class="text-xs text-secondary">
{#if kind === 'trigger'}
Choose the user this trigger will be recorded as edited by {isDeployment
{#if isTrigger}
Choose the user this trigger will be permissioned as {isDeployment
? 'in the target workspace'
: 'in this workspace'}.
: 'in this workspace'}. The selected user's permissions will be used when the trigger
fires.
{:else}
Choose the user this {kind} will run on behalf of {isDeployment
? 'in the target workspace'
@@ -215,10 +233,10 @@
onclick={() => selectUser(user)}
>
<div class="flex flex-col min-w-0">
<span class="font-medium truncate">{user.username}</span>
<span class="font-medium truncate">u/{user.username}</span>
<span class="text-xs text-tertiary truncate">{user.email}</span>
</div>
{#if customEmail === user.email && selected === 'custom'}
{#if selected === 'custom' && (customValue === `u/${user.username}` || customValue === user.email)}
<Check class="w-4 h-4 text-green-500 ml-auto flex-shrink-0" />
{/if}
</button>
@@ -114,7 +114,10 @@
let wacAlphaModalOpen = $state(false)
function showWacAlphaModalIfNeeded() {
if (typeof sessionStorage !== 'undefined' && sessionStorage.getItem(WAC_ALPHA_ACK_KEY) !== 'true') {
if (
typeof sessionStorage !== 'undefined' &&
sessionStorage.getItem(WAC_ALPHA_ACK_KEY) !== 'true'
) {
wacAlphaModalOpen = true
}
}
@@ -380,10 +383,20 @@
if (script.content == '') {
if (template === 'wac_python') {
script.modules = { 'helper.py': { content: 'def main(a: str) -> str:\n return f"hello {a}"\n', language: 'python3' } }
script.modules = {
'helper.py': {
content: 'def main(a: str) -> str:\n return f"hello {a}"\n',
language: 'python3'
}
}
showWacAlphaModalIfNeeded()
} else if (template === 'wac_typescript') {
script.modules = { 'helper.ts': { content: 'export function main(a: string): string {\n return `hello ${a}`\n}\n', language: 'bun' } }
script.modules = {
'helper.ts': {
content: 'export function main(a: string): string {\n return `hello ${a}`\n}\n',
language: 'bun'
}
}
showWacAlphaModalIfNeeded()
}
initContent(script.language, script.kind, template)
@@ -412,7 +425,16 @@
async function initContent(
language: SupportedLanguage,
kind: Script['kind'] | undefined,
template: 'pgsql' | 'mysql' | 'script' | 'docker' | 'powershell' | 'bunnative' | 'claudesandbox' | 'wac_python' | 'wac_typescript'
template:
| 'pgsql'
| 'mysql'
| 'script'
| 'docker'
| 'powershell'
| 'bunnative'
| 'claudesandbox'
| 'wac_python'
| 'wac_typescript'
) {
scriptEditor?.disableCollaboration()
const templateScript = await isTemplateScript()
@@ -618,7 +640,12 @@
if (!disableHistoryChange) {
history.replaceState(history.state, '', `/scripts/edit/${script.path}`)
}
if (stay || (script.auto_kind === 'lib' && script.kind !== 'preprocessor' && !isWorkflowAsCode(script.content, script.language))) {
if (
stay ||
(script.auto_kind === 'lib' &&
script.kind !== 'preprocessor' &&
!isWorkflowAsCode(script.content, script.language))
) {
script.parent_hash = newHash
sendUserToast('Deployed')
} else {
@@ -867,10 +894,7 @@
]
: []
if (
dropdownItems.length === 0 &&
isWorkflowAsCode(script.content, script.language)
) {
if (dropdownItems.length === 0 && isWorkflowAsCode(script.content, script.language)) {
dropdownItems = [
{
label: 'Export as YAML/JSON',
@@ -1217,7 +1241,10 @@
<div class=" grid grid-cols-3 gap-2">
{#each langs as [label, lang] (lang)}
{@const isPicked =
(lang == script.language && template != 'bunnative' && template != 'docker' && template != 'claudesandbox') ||
(lang == script.language &&
template != 'bunnative' &&
template != 'docker' &&
template != 'claudesandbox') ||
(template == 'bunnative' && lang == 'bunnative') ||
(template == 'docker' && lang == 'docker') ||
(template == 'claudesandbox' && lang == 'bun')}
@@ -1283,7 +1310,13 @@
on:click={() => {
template = 'wac_typescript'
script.language = 'bun'
script.modules = { 'helper.ts': { content: 'export function main(a: string): string {\n return `hello ${a}`\n}\n', language: 'bun' } }
script.modules = {
'helper.ts': {
content:
'export function main(a: string): string {\n return `hello ${a}`\n}\n',
language: 'bun'
}
}
initContent('bun', script.kind, template)
showWacAlphaModalIfNeeded()
}}
@@ -1303,7 +1336,12 @@
on:click={() => {
template = 'wac_python'
script.language = 'python3'
script.modules = { 'helper.py': { content: 'def main(a: str) -> str:\n return f"hello {a}"\n', language: 'python3' } }
script.modules = {
'helper.py': {
content: 'def main(a: str) -> str:\n return f"hello {a}"\n',
language: 'python3'
}
}
initContent('python3', script.kind, template)
showWacAlphaModalIfNeeded()
}}
@@ -1745,9 +1783,9 @@
{#if script.on_behalf_of_email && canPreserve}
&rarr; <OnBehalfOfSelector
targetWorkspace={$workspaceStore ?? ''}
targetEmail={originalOnBehalfOfEmail}
targetValue={originalOnBehalfOfEmail}
selected={onBehalfOfChoice}
onSelect={(choice, email) => {
onSelect={(choice, details) => {
onBehalfOfChoice = choice
if (choice === 'me') {
script.on_behalf_of_email = $userStore?.email
@@ -1757,15 +1795,15 @@
script.on_behalf_of_email = originalOnBehalfOfEmail
customOnBehalfOfEmail = ''
preserveOnBehalfOf = true
} else if (choice === 'custom' && email) {
script.on_behalf_of_email = email
customOnBehalfOfEmail = email
} else if (choice === 'custom' && details) {
script.on_behalf_of_email = details.email
customOnBehalfOfEmail = details.email
preserveOnBehalfOf = true
}
}}
kind="script"
{canPreserve}
customEmail={customOnBehalfOfEmail}
customValue={customOnBehalfOfEmail}
isDeployment={false}
/>
{:else if script.on_behalf_of_email && !canPreserve}
@@ -2053,7 +2091,19 @@
<Modal bind:open={wacAlphaModalOpen} title="Workflow-as-Code (Alpha)" kind="X">
<div class="flex flex-col gap-4 pr-4">
<p class="text-sm text-secondary">
Workflow-as-Code is in <strong>alpha</strong> — use in production at your own risk. It is an alternative to the Flow editor for advanced users. Feedback welcome on <a href="https://github.com/windmill-labs/windmill/issues" target="_blank" class="text-blue-600 dark:text-blue-400 hover:underline">GitHub</a> or <a href="https://discord.com/invite/V7PM2YHsPB" target="_blank" class="text-blue-600 dark:text-blue-400 hover:underline">Discord</a>.
Workflow-as-Code is in <strong>alpha</strong> — use in production at your own risk. It is an
alternative to the Flow editor for advanced users. Feedback welcome on
<a
href="https://github.com/windmill-labs/windmill/issues"
target="_blank"
class="text-blue-600 dark:text-blue-400 hover:underline">GitHub</a
>
or
<a
href="https://discord.com/invite/V7PM2YHsPB"
target="_blank"
class="text-blue-600 dark:text-blue-400 hover:underline">Discord</a
>.
</p>
<div class="flex justify-end">
<Button size="sm" on:click={acknowledgeWacAlpha}>Acknowledge</Button>
@@ -54,10 +54,9 @@
} = $props()
let isDeployer = $derived($userStore?.groups?.includes(WM_DEPLOYERS_GROUP) ?? false)
let canPreserve = $derived(
!!$userStore?.is_admin || !!$userStore?.is_super_admin || isDeployer
)
let canPreserve = $derived(!!$userStore?.is_admin || !!$userStore?.is_super_admin || isDeployer)
let savedOnBehalfOfEmail = $derived(savedApp?.policy?.on_behalf_of_email)
let savedOnBehalfOf = $derived(savedApp?.policy?.on_behalf_of)
let onBehalfOfChoice: OnBehalfOfChoice = $state(undefined)
let customOnBehalfOfEmail: string = $state('')
let dirtyCustomPath = $state(false)
@@ -197,12 +196,13 @@
</Tooltip>
{#if canPreserve}
<div class="mt-4">
Because you are either an admin or part of the {WM_DEPLOYERS_GROUP} group, you can select another user to run this app on behalf of. Once deployed the app will be run on behalf of
Because you are either an admin or part of the {WM_DEPLOYERS_GROUP} group, you can select another
user to run this app on behalf of. Once deployed the app will be run on behalf of
<OnBehalfOfSelector
targetWorkspace={$workspaceStore ?? ''}
targetEmail={savedOnBehalfOfEmail}
targetValue={savedOnBehalfOfEmail}
selected={onBehalfOfChoice}
onSelect={(choice, email, username) => {
onSelect={(choice, details) => {
onBehalfOfChoice = choice
if (choice === 'me') {
policy.on_behalf_of_email = $userStore?.email
@@ -211,25 +211,25 @@
preserveOnBehalfOf = false
} else if (choice === 'target') {
policy.on_behalf_of_email = savedOnBehalfOfEmail
policy.on_behalf_of = savedOnBehalfOf
customOnBehalfOfEmail = ''
preserveOnBehalfOf = true
} else if (choice === 'custom' && email) {
policy.on_behalf_of_email = email
policy.on_behalf_of = username ? `u/${username}` : undefined
customOnBehalfOfEmail = email
} else if (choice === 'custom' && details) {
policy.on_behalf_of_email = details.email
policy.on_behalf_of = details.permissionedAs
customOnBehalfOfEmail = details.email
preserveOnBehalfOf = true
}
}}
kind="app"
{canPreserve}
customEmail={customOnBehalfOfEmail}
customValue={customOnBehalfOfEmail}
isDeployment={false}
/>
</div>
{/if}
</Alert>
<div class="mt-10"></div>
{#if !hideSecretUrl}
@@ -409,9 +409,9 @@
{#if flowStore.val.on_behalf_of_email && canPreserve}
&rarr; <OnBehalfOfSelector
targetWorkspace={$workspaceStore ?? ''}
targetEmail={$savedOnBehalfOfEmail}
targetValue={$savedOnBehalfOfEmail}
selected={onBehalfOfChoice}
onSelect={(choice, email) => {
onSelect={(choice, details) => {
onBehalfOfChoice = choice
if (choice === 'me') {
flowStore.val.on_behalf_of_email = $userStore?.email
@@ -421,15 +421,15 @@
flowStore.val.on_behalf_of_email = $savedOnBehalfOfEmail
customOnBehalfOfEmail = ''
$preserveOnBehalfOf = true
} else if (choice === 'custom' && email) {
flowStore.val.on_behalf_of_email = email
customOnBehalfOfEmail = email
} else if (choice === 'custom' && details) {
flowStore.val.on_behalf_of_email = details.email
customOnBehalfOfEmail = details.email
$preserveOnBehalfOf = true
}
}}
kind="flow"
{canPreserve}
customEmail={customOnBehalfOfEmail}
customValue={customOnBehalfOfEmail}
isDeployment={false}
/>
{:else if flowStore.val.on_behalf_of_email && !canPreserve}
+58 -25
View File
@@ -94,16 +94,16 @@ export async function existsTrigger(
}
/**
* Get trigger deployment data with optional email preservation.
* @param onBehalfOfEmail - If set, the trigger will be deployed with this email and preserve_email=true.
* Get trigger deployment data with optional permissioned_as preservation.
* @param onBehalfOf - If set, the trigger will be deployed with this permissioned_as (u/username or g/group) and preserve_permissioned_as=true.
*/
export async function getTriggersDeployData(
kind: TriggerKind,
path: string,
workspace: string,
onBehalfOfEmail?: string
onBehalfOf?: string
) {
const preserveEmail = onBehalfOfEmail !== undefined
const preservePermissionedAs = onBehalfOf !== undefined
if (kind === 'sqs') {
const sqsTrigger = await SqsTriggerService.getSqsTrigger({
@@ -112,7 +112,11 @@ export async function getTriggersDeployData(
})
return {
data: { ...sqsTrigger, email: onBehalfOfEmail, preserve_email: preserveEmail },
data: {
...sqsTrigger,
permissioned_as: onBehalfOf,
preserve_permissioned_as: preservePermissionedAs
},
createFn: SqsTriggerService.createSqsTrigger,
updateFn: SqsTriggerService.updateSqsTrigger
}
@@ -123,7 +127,11 @@ export async function getTriggersDeployData(
})
return {
data: { ...kafkaTrigger, email: onBehalfOfEmail, preserve_email: preserveEmail },
data: {
...kafkaTrigger,
permissioned_as: onBehalfOf,
preserve_permissioned_as: preservePermissionedAs
},
createFn: KafkaTriggerService.createKafkaTrigger,
updateFn: KafkaTriggerService.updateKafkaTrigger
}
@@ -134,7 +142,11 @@ export async function getTriggersDeployData(
})
return {
data: { ...mqttTrigger, email: onBehalfOfEmail, preserve_email: preserveEmail },
data: {
...mqttTrigger,
permissioned_as: onBehalfOf,
preserve_permissioned_as: preservePermissionedAs
},
createFn: MqttTriggerService.createMqttTrigger,
updateFn: MqttTriggerService.updateMqttTrigger
}
@@ -145,7 +157,11 @@ export async function getTriggersDeployData(
})
return {
data: { ...natsTrigger, email: onBehalfOfEmail, preserve_email: preserveEmail },
data: {
...natsTrigger,
permissioned_as: onBehalfOf,
preserve_permissioned_as: preservePermissionedAs
},
createFn: NatsTriggerService.createNatsTrigger,
updateFn: NatsTriggerService.updateNatsTrigger
}
@@ -167,8 +183,8 @@ export async function getTriggersDeployData(
delivery_config: gcpTrigger.delivery_config ?? undefined,
base_endpoint:
gcpTrigger.delivery_type === 'push' ? `${window.location.origin}${base}` : undefined,
email: onBehalfOfEmail,
preserve_email: preserveEmail
permissioned_as: onBehalfOf,
preserve_permissioned_as: preservePermissionedAs
}
return {
@@ -183,7 +199,11 @@ export async function getTriggersDeployData(
})
return {
data: { ...postgresTrigger, email: onBehalfOfEmail, preserve_email: preserveEmail },
data: {
...postgresTrigger,
permissioned_as: onBehalfOf,
preserve_permissioned_as: preservePermissionedAs
},
createFn: PostgresTriggerService.createPostgresTrigger,
updateFn: PostgresTriggerService.updatePostgresTrigger
}
@@ -194,7 +214,11 @@ export async function getTriggersDeployData(
})
return {
data: { ...websocketTrigger, email: onBehalfOfEmail, preserve_email: preserveEmail },
data: {
...websocketTrigger,
permissioned_as: onBehalfOf,
preserve_permissioned_as: preservePermissionedAs
},
createFn: WebsocketTriggerService.createWebsocketTrigger,
updateFn: WebsocketTriggerService.updateWebsocketTrigger
}
@@ -205,7 +229,11 @@ export async function getTriggersDeployData(
})
return {
data: { ...httpTrigger, email: onBehalfOfEmail, preserve_email: preserveEmail },
data: {
...httpTrigger,
permissioned_as: onBehalfOf,
preserve_permissioned_as: preservePermissionedAs
},
createFn: HttpTriggerService.createHttpTrigger,
updateFn: HttpTriggerService.updateHttpTrigger
}
@@ -215,7 +243,12 @@ export async function getTriggersDeployData(
path: path
})
return {
data: { ...schedulesTrigger, email: onBehalfOfEmail, preserve_email: preserveEmail },
data: {
...schedulesTrigger,
// permissioned_as is only set on create, not update
permissioned_as: onBehalfOf,
preserve_permissioned_as: preservePermissionedAs
},
createFn: ScheduleService.createSchedule,
updateFn: ScheduleService.updateSchedule
}
@@ -449,9 +482,9 @@ export async function getTriggerValue(kind: TriggerKind, path: string, workspace
}
/**
* Get the email for a trigger (used for on_behalf_of during deployment).
* Get the permissioned_as for a trigger (used for on_behalf_of during deployment).
*/
export async function getTriggerEmail(
export async function getTriggerPermissionedAs(
kind: TriggerKind,
path: string,
workspace: string
@@ -459,31 +492,31 @@ export async function getTriggerEmail(
try {
if (kind === 'sqs') {
const trigger = await SqsTriggerService.getSqsTrigger({ workspace, path })
return trigger.edited_by
return trigger.permissioned_as
} else if (kind === 'kafka') {
const trigger = await KafkaTriggerService.getKafkaTrigger({ workspace, path })
return trigger.edited_by
return trigger.permissioned_as
} else if (kind === 'mqtt') {
const trigger = await MqttTriggerService.getMqttTrigger({ workspace, path })
return trigger.edited_by
return trigger.permissioned_as
} else if (kind === 'nats') {
const trigger = await NatsTriggerService.getNatsTrigger({ workspace, path })
return trigger.edited_by
return trigger.permissioned_as
} else if (kind === 'gcp') {
const trigger = await GcpTriggerService.getGcpTrigger({ workspace, path })
return trigger.edited_by
return trigger.permissioned_as
} else if (kind === 'postgres') {
const trigger = await PostgresTriggerService.getPostgresTrigger({ workspace, path })
return trigger.edited_by
return trigger.permissioned_as
} else if (kind === 'websockets') {
const trigger = await WebsocketTriggerService.getWebsocketTrigger({ workspace, path })
return trigger.edited_by
return trigger.permissioned_as
} else if (kind === 'routes') {
const trigger = await HttpTriggerService.getHttpTrigger({ workspace, path })
return trigger.edited_by
return trigger.permissioned_as
} else if (kind === 'schedules') {
const trigger = await ScheduleService.getSchedule({ workspace, path })
return trigger.edited_by
return trigger.permissioned_as
}
} catch {
// Trigger may not exist in the workspace
+24 -13
View File
@@ -11,7 +11,7 @@ import { getAllModules } from './components/flows/flowExplorer'
import {
existsTrigger,
getTriggersDeployData,
getTriggerEmail,
getTriggerPermissionedAs,
getTriggerValue,
type AdditionalInformation,
type Kind
@@ -24,8 +24,15 @@ export interface DeployItemParams {
workspaceFrom: string
workspaceTo: string
additionalInformation?: AdditionalInformation
/** The email to use for on_behalf_of. If set, preserve_on_behalf_of will be true. If undefined, deploying user's email is used. */
onBehalfOfEmail?: string
/**
* The value to use for on_behalf_of when deploying.
* Format varies by item kind:
* - For flows/scripts/apps: an email address (on_behalf_of_email)
* - For triggers/schedules: permissioned_as format (u/username or g/group)
* If set, preserve_on_behalf_of / preserve_permissioned_as will be true.
* If undefined, the deploying user's identity is used.
*/
onBehalfOf?: string
}
export interface DeployResult {
@@ -38,9 +45,9 @@ export interface DeployResult {
* Handles all item kinds: flow, script, app, variable, resource, resource_type, folder, trigger.
*/
export async function deployItem(params: DeployItemParams): Promise<DeployResult> {
const { kind, path, workspaceFrom, workspaceTo, additionalInformation, onBehalfOfEmail } = params
// When onBehalfOfEmail is set, we preserve the on_behalf_of setting with the specified email
const preserveOnBehalfOf = onBehalfOfEmail !== undefined
const { kind, path, workspaceFrom, workspaceTo, additionalInformation, onBehalfOf } = params
// When onBehalfOf is set, we preserve the on_behalf_of setting with the specified value
const preserveOnBehalfOf = onBehalfOf !== undefined
try {
const alreadyExists = await checkItemExists(kind, path, workspaceTo, additionalInformation)
@@ -62,7 +69,7 @@ export async function deployItem(params: DeployItemParams): Promise<DeployResult
requestBody: {
...flow,
preserve_on_behalf_of: preserveOnBehalfOf,
on_behalf_of_email: onBehalfOfEmail
on_behalf_of_email: onBehalfOf
}
})
} else {
@@ -71,7 +78,7 @@ export async function deployItem(params: DeployItemParams): Promise<DeployResult
requestBody: {
...flow,
preserve_on_behalf_of: preserveOnBehalfOf,
on_behalf_of_email: onBehalfOfEmail
on_behalf_of_email: onBehalfOf
}
})
}
@@ -94,7 +101,7 @@ export async function deployItem(params: DeployItemParams): Promise<DeployResult
).hash
: undefined,
preserve_on_behalf_of: preserveOnBehalfOf,
on_behalf_of_email: onBehalfOfEmail
on_behalf_of_email: onBehalfOf
}
})
} else if (kind === 'app' || kind === 'raw_app') {
@@ -259,7 +266,7 @@ export async function deployItem(params: DeployItemParams): Promise<DeployResult
additionalInformation.triggers.kind,
path,
workspaceFrom,
onBehalfOfEmail
onBehalfOf
)
if (alreadyExists) {
await updateFn({
@@ -448,9 +455,13 @@ export async function getItemValue(
}
/**
* Get the on_behalf_of_email for a flow, script, app, or trigger (including schedule).
* Get the on_behalf_of value for a deployable item.
*
* Return type varies by item kind:
* - For flows/scripts/apps: returns on_behalf_of_email (an email address)
* - For triggers/schedules: returns permissioned_as (u/username or g/group format)
*/
export async function getOnBehalfOfEmail(
export async function getOnBehalfOf(
kind: Kind,
path: string,
workspace: string,
@@ -467,7 +478,7 @@ export async function getOnBehalfOfEmail(
const app = await AppService.getAppByPath({ workspace, path })
return app.policy.on_behalf_of_email
} else if (kind === 'trigger' && additionalInformation?.triggers) {
return await getTriggerEmail(additionalInformation.triggers.kind, path, workspace)
return await getTriggerPermissionedAs(additionalInformation.triggers.kind, path, workspace)
}
} catch {
// Item may not exist in the workspace
@@ -3,6 +3,9 @@ properties:
script_path:
type: string
description: Path to the script or flow to execute when triggered
permissioned_as:
type: string
description: The user or group this trigger runs as (permissioned_as)
is_flow:
type: boolean
description: True if script_path points to a flow, false if it points to a script
@@ -84,6 +87,7 @@ properties:
description: Retry configuration for failed module executions
required:
- script_path
- permissioned_as
- is_flow
- gcp_resource_path
- topic_id
@@ -3,6 +3,9 @@ properties:
script_path:
type: string
description: Path to the script or flow to execute when triggered
permissioned_as:
type: string
description: The user or group this trigger runs as (permissioned_as)
is_flow:
type: boolean
description: True if script_path points to a flow, false if it points to a script
@@ -114,6 +117,7 @@ properties:
description: Retry configuration for failed module executions
required:
- script_path
- permissioned_as
- is_flow
- route_path
- request_type
@@ -3,6 +3,9 @@ properties:
script_path:
type: string
description: Path to the script or flow to execute when triggered
permissioned_as:
type: string
description: The user or group this trigger runs as (permissioned_as)
is_flow:
type: boolean
description: True if script_path points to a flow, false if it points to a script
@@ -80,6 +83,7 @@ properties:
description: Retry configuration for failed module executions
required:
- script_path
- permissioned_as
- is_flow
- kafka_resource_path
- group_id
@@ -3,6 +3,9 @@ properties:
script_path:
type: string
description: Path to the script or flow to execute when triggered
permissioned_as:
type: string
description: The user or group this trigger runs as (permissioned_as)
is_flow:
type: boolean
description: True if script_path points to a flow, false if it points to a script
@@ -80,6 +83,7 @@ properties:
description: Retry configuration for failed module executions
required:
- script_path
- permissioned_as
- is_flow
- subscribe_topics
- mqtt_resource_path
@@ -3,6 +3,9 @@ properties:
script_path:
type: string
description: Path to the script or flow to execute when triggered
permissioned_as:
type: string
description: The user or group this trigger runs as (permissioned_as)
is_flow:
type: boolean
description: True if script_path points to a flow, false if it points to a script
@@ -66,6 +69,7 @@ properties:
description: Retry configuration for failed module executions
required:
- script_path
- permissioned_as
- is_flow
- nats_resource_path
- use_jetstream

Some files were not shown because too many files have changed in this diff Show More