Compare commits

..

490 Commits

Author SHA1 Message Date
Ruben Fiszel
ece4573076 all 2023-02-01 19:46:48 +01:00
Kai Jellinghaus
833d0df965 Add webhook request histogram 2023-01-26 07:03:49 +01:00
Kai Jellinghaus
1e7b7cc8d6 Update SQLX 2023-01-26 06:59:29 +01:00
Kai Jellinghaus
1de8eefe96 Add workspace webhook 2023-01-26 06:42:10 +01:00
Ruben Fiszel
51a710ffa0 do not copy necessarily for the flow object viewer 2023-01-26 03:20:01 +01:00
Ruben Fiszel
ad0c836129 add instructions for triggers on flows 2023-01-26 01:06:53 +01:00
Ruben Fiszel
be6707b461 for python change default editor tab to 4 2023-01-25 20:40:40 +01:00
Ruben Fiszel
c227ba2794 loader for flowjobresult 2023-01-25 20:33:33 +01:00
Ruben Fiszel
d60a770eb7 flow preview status viewer display more info 2023-01-25 14:14:14 +01:00
Ruben Fiszel
30d83a942c restrict number of jobs in the queue for free users 2023-01-25 13:34:08 +01:00
Ruben Fiszel
98bf9355dc implement MAX_FREE_CONCURRENT_RUNS 2023-01-25 12:58:29 +01:00
Ruben Fiszel
a3928101af worker client should use internal url 2023-01-25 00:10:37 +01:00
Ruben Fiszel
6e77757939 nodraft for editing flow from run 2023-01-24 22:57:34 +01:00
Ruben Fiszel
66c5d6e400 fix arg inputs 2023-01-24 22:44:52 +01:00
Ruben Fiszel
b0c184767c update pricing page 2023-01-24 21:58:44 +01:00
Ruben Fiszel
1165cf6e24 UX improvements for flows 2023-01-24 21:53:41 +01:00
Ruben Fiszel
a9640107f3 background scripts now part of the policy 2023-01-24 15:09:27 +01:00
Faton Ramadani
73bc13bb7d feat(frontend): Open debug runs from component (#1155)
* feat(frontend): Add debug runs

* feat(frontend): Fix build

* feat(frontend): revert implementation

* feat(frontend): Fix error displa

* feat(frontend): Fix error display

* feat(frontend): Fix preview mode

* feat(frontend): Fix error display
2023-01-24 14:34:19 +01:00
Ádám Kovács
e98966283d fix(frontend): App table action button cell (#1149)
Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
2023-01-24 12:20:27 +01:00
Ruben Fiszel
8ad2344ec6 improve cancel approval error message 2023-01-24 12:11:39 +01:00
Ruben Fiszel
b77c239f30 fix: improvements for error handling as first step of flow 2023-01-24 11:16:44 +01:00
Ruben Fiszel
15cb83936b error handler improvements 2023-01-24 07:30:07 +01:00
Ruben Fiszel
e2157ee1c0 fix listing runs from schedule 2023-01-24 06:55:11 +01:00
dependabot[bot]
bdfd50b970 chore(deps): bump @leeoniya/ufuzzy from 0.9.0 to 0.9.1 in /frontend (#1153)
Bumps [@leeoniya/ufuzzy](https://github.com/leeoniya/uFuzzy) from 0.9.0 to 0.9.1.
- [Release notes](https://github.com/leeoniya/uFuzzy/releases)
- [Commits](https://github.com/leeoniya/uFuzzy/compare/0.9.0...0.9.1)

---
updated-dependencies:
- dependency-name: "@leeoniya/ufuzzy"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-24 04:04:34 +01:00
dependabot[bot]
4debfd68dc chore(deps-dev): bump @typescript-eslint/eslint-plugin in /frontend (#1151)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.46.1 to 5.49.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.49.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-24 04:03:59 +01:00
dependabot[bot]
410032060a chore(deps): bump chartjs-adapter-date-fns in /frontend (#1154)
Bumps [chartjs-adapter-date-fns](https://github.com/chartjs/chartjs-adapter-date-fns) from 2.0.0 to 3.0.0.
- [Release notes](https://github.com/chartjs/chartjs-adapter-date-fns/releases)
- [Commits](https://github.com/chartjs/chartjs-adapter-date-fns/compare/v2.0.0...v3.0.0)

---
updated-dependencies:
- dependency-name: chartjs-adapter-date-fns
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-24 04:03:32 +01:00
Faton Ramadani
22b8fed9d9 feat(frontend): Display error as an icon in order to avoid clutter wh… (#1143)
* feat(frontend): Display error as an icon in order to avoid clutter when an error occures

* feat(frontend): Fix build

* feat(frontend): Fix error icon position
2023-01-23 18:03:31 +01:00
Ruben Fiszel
0172587b12 feat: add sql as a valid type in Python 2023-01-23 15:58:33 +01:00
Faton Ramadani
f05f9e4edb feat(frontend): add selectedRowIndex to the table outputs (#1145) 2023-01-23 15:39:15 +01:00
Faton Ramadani
24b1fa0ae3 fix(frontend): Make sure AppSelect items are an array (#1144)
* fix(frontend): Make sure AppSelect items are an array

* fix(frontend): restore placeholder
2023-01-23 05:37:38 -08:00
Ruben Fiszel
fedb9fe74c update pricing page 2023-01-22 18:34:05 +01:00
Ruben Fiszel
5408da829c update pricing page 2023-01-22 18:20:06 +01:00
Ruben Fiszel
448e45b7e4 fix privilege on cloud 2023-01-22 00:53:29 +01:00
Ruben Fiszel
156f257619 add import_map to nsjail 2023-01-21 23:28:11 +01:00
Ruben Fiszel
7f40373fd6 feat(deno): allow overriding deno sandboxing with DENO_FLAGS' 2023-01-21 22:54:14 +01:00
Ruben Fiszel
f2ec03ba1b implement relative imports for deno 2023-01-21 19:12:22 +01:00
Ruben Fiszel
9a79c5537e fix compile 2023-01-21 18:49:26 +01:00
Ruben Fiszel
cf156b0591 support BigInt serialization 2023-01-21 18:41:58 +01:00
Ruben Fiszel
832ddabdf2 feat(deno): add support for DENO_AUTH_TOKENS 2023-01-21 18:36:17 +01:00
Ruben Fiszel
22080312f0 description at bottom 2023-01-21 17:33:23 +01:00
Ruben Fiszel
165095376d description at bottom 2023-01-21 10:19:05 +01:00
Ruben Fiszel
2a816b9e7b remove square from oauth 2023-01-21 09:37:10 +01:00
Ruben Fiszel
a7d03f4e25 square use req_body_auth 2023-01-21 09:26:39 +01:00
Ruben Fiszel
5e18e580f7 fix superadmins permissions 2023-01-21 08:09:49 +01:00
Felipe Albertao
9b7906e43d Added Square API OAuth (#1141) 2023-01-21 07:41:52 +01:00
Ruben Fiszel
2143bbdc94 add support for python import shopifyAPI 2023-01-20 19:52:18 +01:00
Ruben Fiszel
51a8810aa0 feat: add QUEUE_LIMIT_WAIT_RESULT 2023-01-20 19:39:31 +01:00
Faton Ramadani
8f906026b3 fix(frontend): Add default value for text, number and date input + fix issues with number input + add date input in the settings panel (#1135)
Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-01-20 18:31:10 +01:00
Faton Ramadani
aa6de3bb57 fix(frontend): Prevent modal from hijacking all keypress event (#1136) 2023-01-20 15:39:37 +01:00
Faton Ramadani
71a443e3c5 fix(frontend): Check if hiddenInlineScripts are undefined before iterating over them (#1134) 2023-01-20 11:49:43 +01:00
Ruben Fiszel
d1526d9c72 fix deno nsjail vmem 2023-01-20 11:49:08 +01:00
Ruben Fiszel
2456e35908 install nodejs only on heavy 2023-01-19 21:01:30 +01:00
Ruben Fiszel
f359015d89 bump default bash tmp size 2023-01-19 20:32:08 +01:00
Ruben Fiszel
b8155f4d27 update npm used in heavy 2023-01-19 20:00:01 +01:00
Ruben Fiszel
2ce903d7e7 add more details about signals termination 2023-01-19 18:20:24 +01:00
Ruben Fiszel
d4765dee16 add nodejs npm to heavy image 2023-01-19 16:22:33 +01:00
Faton Ramadani
a78486d7e0 fix(frontend): Make sure that old apps are rendering properly (#1132) 2023-01-19 15:14:15 +01:00
Faton Ramadani
e969af9e44 feat(frontend): add support for background scripts + add FormButtonCo… (#1124)
* feat(frontend): add support for background scripts + add FormButtonComponnet

* feat(frontend): add background script fields

* feat(frontend): Handle deletion + display of results

* feat(frontend): Fix background script ids: Runnable ids needs to be valid js expression, because they are used in the evaluator.

* feat(frontend): add a portal to display modal

* feat(frontend): fix build

* feat(frontend): Add missing props

* feat(frontend): Fix run button + add tooltip + fix icon
2023-01-19 14:52:32 +01:00
Ruben Fiszel
e371fda1ce relax nsjail limits 2023-01-19 14:51:43 +01:00
Ruben Fiszel
ee14bed3d9 relax nsjail limits 2023-01-19 14:35:06 +01:00
Faton Ramadani
3d873ed51c fix(frontend): add missing condition to properly select first row (#1128) 2023-01-19 14:08:32 +01:00
Ruben Fiszel
d85f550611 handle empty flow module input transforms 2023-01-19 12:22:37 +01:00
Faton Ramadani
6c9daf7002 fix(frontend): fix first row selection (#1125)
* fix(frontend): fix first row selection

* fix(frontend): fix order of conditions
2023-01-19 11:52:34 +01:00
Ádám Kovács
6a88e8c4f4 feat(frontend): Add app preview lock (#1127)
* feat(frontend): Add locked state to app preview

* change color

* add transition
2023-01-19 11:27:16 +01:00
Ruben Fiszel
99246382ee minor UI fixes 2023-01-18 17:56:40 +01:00
Ruben Fiszel
73e1018f65 minor UI fixes 2023-01-18 17:08:34 +01:00
Ruben Fiszel
f1157e3be2 remove vega lite actions 2023-01-18 16:06:56 +01:00
Ruben Fiszel
bd79938bed feat(frontend): add vega-lite component 2023-01-18 15:49:47 +01:00
Ádám Kovács
173093a403 fix(frontend): Prepare app scripts code for export (#1123) 2023-01-18 12:43:24 +01:00
Ruben Fiszel
ed61df6d8a UX nits 2023-01-18 11:30:08 +01:00
Ruben Fiszel
24feb64dfe fix reporting duration above 60s 2023-01-18 11:23:25 +01:00
Ruben Fiszel
b3eaf86e40 HTMLComponent refresh on content change 2023-01-18 10:24:29 +01:00
Ádám Kovács
4dfdf374af feat(frontend): Add tooltip to app recompute (#1122) 2023-01-18 10:10:47 +01:00
Ruben Fiszel
45191621d2 UX nits on schedule 2023-01-18 09:42:49 +01:00
Ruben Fiszel
4d60be738e UX nits on schedule 2023-01-18 09:38:54 +01:00
Ruben Fiszel
eef3bab6e4 feat(backend): add queue_limit + configurable timeout + fix timeout cancel 2023-01-18 09:02:38 +01:00
Ruben Fiszel
14ebb6dc32 fix all cases where schema has been set to undefined 2023-01-18 06:56:44 +01:00
Ruben Fiszel
9e602691b6 do not cancel the job on leaving the preview page 2023-01-18 00:31:55 +01:00
Ruben Fiszel
02045001d2 do not cancel the job on leaving the preview page 2023-01-18 00:14:29 +01:00
Faton Ramadani
3c483f5337 fix(frontend): Select the first row by default, and remove the abilit… (#1121)
* fix(frontend): Select the first row by default, and remove the ability to deselect + fix table header z-index

* fix(frontend): restore z-index, under the floating tags
2023-01-17 19:10:06 +01:00
Ruben Fiszel
bcb5c3697d inline tailwindcss for html component in apps 2023-01-17 17:48:40 +01:00
Ruben Fiszel
0e50579089 frontend(apps): html component uses iframe for security 2023-01-17 17:29:01 +01:00
Ádám Kovács
59c4cc2058 fix(frontend): App table header z-index (#1120) 2023-01-17 14:00:54 +01:00
Ádám Kovács
462adbe42f fix(frontend): Runnable table overflow (#1119) 2023-01-17 12:51:14 +01:00
Ruben Fiszel
d708b6faa5 fix permissioned_as from username 2023-01-17 12:49:24 +01:00
Ruben Fiszel
40d566ab8e remove unecessary badge width 2023-01-17 12:32:30 +01:00
Ruben Fiszel
c5d712c977 ci: build both amd and arm on same workflow leveraging depot 2023-01-17 12:16:36 +01:00
Ádám Kovács
631a3da17f fix(frontend): Show app builder header always on top (#1118)
* fix(frontend): Keep app header always on top

* various fixes

* fix
2023-01-17 12:02:21 +01:00
Ruben Fiszel
ff5145d7bd UI nits 2023-01-17 12:00:17 +01:00
Ruben Fiszel
d1131c1a16 various fixes 2023-01-17 11:30:37 +01:00
Ruben Fiszel
9b89df9ecf various fixes 2023-01-17 11:30:22 +01:00
Ruben Fiszel
acff87a09f all 2023-01-17 08:09:46 +01:00
Ruben Fiszel
a182ce2d2c all 2023-01-17 08:09:39 +01:00
Ruben Fiszel
0954b5a4f2 ci: npm run check generate backend client 2023-01-17 07:40:40 +01:00
Ruben Fiszel
d297223fa5 ci: npm check on frontend change and uffizzi only on backend changes 2023-01-17 07:38:05 +01:00
dependabot[bot]
9bccb90c7a chore(deps-dev): bump prettier from 2.8.1 to 2.8.3 in /frontend (#1111)
Bumps [prettier](https://github.com/prettier/prettier) from 2.8.1 to 2.8.3.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.8.1...2.8.3)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-01-17 07:17:42 +01:00
dependabot[bot]
6403259e2a chore(deps): bump @tanstack/svelte-table in /frontend (#1112)
Bumps [@tanstack/svelte-table](https://github.com/tanstack/table) from 8.7.2 to 8.7.6.
- [Release notes](https://github.com/tanstack/table/releases)
- [Commits](https://github.com/tanstack/table/compare/v8.7.2...v8.7.6)

---
updated-dependencies:
- dependency-name: "@tanstack/svelte-table"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-01-17 06:53:11 +01:00
Ruben Fiszel
fa367f4128 point uffizzi lsp to 60d image 2023-01-17 06:51:17 +01:00
Ruben Fiszel
164f281dd0 fix uffizzi preview 2023-01-17 06:46:37 +01:00
Adam Vollrath
760cb31bae Use Uffizzi workflow v2 and other minor improvements. (#1114)
* Use Uffizzi workflow `v2` and other minor improvements.

* Specify Uffizzi Preview URL as target for `playwright`.

Answers https://github.com/windmill-labs/windmill/pull/1106#issuecomment-1384334128

* recheck
2023-01-16 20:32:46 +01:00
Faton Ramadani
4f869811fe fix(frontend): Fix id generation when a second action (#1110) 2023-01-16 19:07:22 +01:00
Ruben Fiszel
0f26a3b111 playwright against uffizzi (#1109)
* all

* fix perms

* move build to depot

* login

* revert to docker build

* revert to docker build

* fix playwright
2023-01-16 18:01:45 +01:00
Vibhav Bobade
fadae2d2b7 create preview environments for every PR using Uffizzi (#1106)
* Integrate Uffizzi

* remove lsp build

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-01-16 17:03:31 +01:00
Faton Ramadani
12b00a808d feat(frontend): Add Supabase resource (#1107)
* feat(frontend): Add Supabase resource

* feat(frontend): Added an image
2023-01-16 16:54:06 +01:00
Ádám Kovács
f0435f5f81 fix(frontend): Playwright (#1108)
* fix(frontend): Playwright

* fix(frontend): Update test base url

* feat(ci): Add base url to playwright
2023-01-16 16:51:14 +01:00
Ruben Fiszel
f018d025a2 fix schedule permissions 2023-01-16 16:42:42 +01:00
Ruben Fiszel
5d720e96e6 add row to eval input 2023-01-16 13:23:31 +01:00
Ruben Fiszel
e1b77b0d5f migrate more builds to depot.dev 2023-01-16 10:56:20 +01:00
Ruben Fiszel
2db6424f9d add dynamic disabled field to button 2023-01-16 01:52:18 +01:00
Ruben Fiszel
9a58145f72 add selenium-wire exception 2023-01-15 22:48:49 +01:00
Kyle Galbraith
51c1ae937c add packages: write for ghcr (#1105) 2023-01-15 18:29:18 +01:00
Ruben Fiszel
26bd9ac9aa remove comingsoon for UIs 2023-01-15 17:58:40 +01:00
Kyle Galbraith
3ba28b8156 Switch to Depot for Arm image build (#1104)
* switch to Depot for native arm builds without emulation

* set Depot project id
2023-01-15 17:55:25 +01:00
Ruben Fiszel
3f318b4ac4 add license key check 2023-01-15 12:06:37 +01:00
Ruben Fiszel
452c6de7e3 script_helpers nit 2023-01-15 08:32:37 +01:00
Ruben Fiszel
b1a86db58b nit error handling 2023-01-15 07:22:35 +01:00
Ruben Fiszel
b29eccf4bb : 2023-01-15 01:39:07 +01:00
Ruben Fiszel
ba28f6c62a error improvements 2023-01-15 01:28:13 +01:00
Ruben Fiszel
279f671dcf improve default connection for failure module 2023-01-14 19:22:59 +01:00
Ruben Fiszel
954cc1bb95 fix tests wrt to new error handling 2023-01-14 18:47:37 +01:00
Ruben Fiszel
776d4c9ca1 error handling improvements 2023-01-14 17:46:03 +01:00
Ruben Fiszel
6949516e63 improve error handling 2023-01-14 15:08:47 +01:00
Ruben Fiszel
f6c13b0324 avoid annoying panel resizing on value change 2023-01-14 14:29:12 +01:00
Ádám Kovács
d811675882 feat(frontend): Add surreal db logo (#1102) 2023-01-14 14:17:43 +01:00
Ruben Fiszel
735aee952a add hub compatible json export to apps 2023-01-14 09:10:39 +01:00
Ruben Fiszel
9432f4c1fa minor tooltip fix 2023-01-14 07:45:15 +01:00
Ruben Fiszel
5bf5a4f86e sqlx fix 2023-01-13 22:27:18 +01:00
Ruben Fiszel
60ef8f569b run_wait_result improvements: cancel + early tx drop 2023-01-13 21:30:29 +01:00
Ádám Kovács
7bc59d9d26 fix(frontend): App script list panel overflow (#1101) 2023-01-13 20:24:18 +01:00
Ruben Fiszel
bd4c7b7957 minor improvements 2023-01-13 18:52:34 +01:00
Ádám Kovács
172b5dba8f feat(frontend): Update app table component styles (#1100)
* feat(frontend): Update app table component styles

* feat(frontend): Update table search style
2023-01-13 18:44:21 +01:00
Ruben Fiszel
b478ba8c45 clarify run detail page + allow i64 for MAX_LOG_SIZE 2023-01-13 18:39:17 +01:00
Ruben Fiszel
71708b7566 add MAX_LOG_SIZE env params 2023-01-13 14:46:07 +01:00
Ádám Kovács
c50c7406f2 feat(frontend): Add default codes to app editor (#1099) 2023-01-13 13:48:28 +01:00
Ruben Fiszel
00357261d5 add psycopg-pool to psycopg default install 2023-01-13 12:16:48 +01:00
Ruben Fiszel
c121908716 fix table static exports 2023-01-13 12:08:37 +01:00
Ádám Kovács
2191e85231 feat(frontend): Add more integration icons (#1097)
* feat(frontend): Add more integration icons

* fix

* fix
2023-01-13 08:48:31 +01:00
Ruben Fiszel
a85a964819 add psycopg3 to the list of support imports 2023-01-13 07:54:05 +01:00
Ruben Fiszel
2ee418b6bf add polars to lsp 2023-01-12 23:12:07 +01:00
Ruben Fiszel
8f72c32fbe various improvements 2023-01-12 22:20:54 +01:00
Ruben Fiszel
1cc5059179 fix execute_component tokens 2023-01-12 16:53:54 +01:00
Ruben Fiszel
9280a5132e minor app fixes 2023-01-12 13:04:17 +01:00
Ruben Fiszel
a626aa6175 minor app fixes 2023-01-12 12:51:27 +01:00
Ádám Kovács
339742ca77 fix(frontend): App form component display (#1096) 2023-01-12 11:41:05 +01:00
Ádám Kovács
43e069eb96 fix(frontend): Use absolute path on connect images (#1095) 2023-01-12 11:40:41 +01:00
Ruben Fiszel
8e1fecfda0 make openapi viewer rely on local assets only 2023-01-11 21:02:53 +01:00
Ruben Fiszel
3e1c9463d6 make openapi viewer rely on local assets only 2023-01-11 20:41:52 +01:00
Ruben Fiszel
1337811438 feat: add openapi viewer (#1094)
* all

* all

* fix tests

* fix tests

* fix tests

* fix tests

* fix tests

* fix tests

* fix tests

* fix tests

* fix typecheck

* fix tests

* fix tests

* fix tests

* all
2023-01-11 20:28:50 +01:00
Ruben Fiszel
a028772bf4 fix typecheck 2023-01-11 17:21:02 +01:00
Ádám Kovács
0419e7e1c9 fix(frontend): Revert component input panel change (#1092) 2023-01-11 16:56:58 +01:00
Ádám Kovács
bdfc38d954 feat(frontend): Add copy button option to app text display component (#1090)
* feat(frontend): Add copy button to app text

* feat(frontend): Add tooltip to component configs

* fix(frontend): Remove copy button text
2023-01-11 16:05:19 +01:00
Ruben Fiszel
dbd632375c fix backend tests 2023-01-11 14:56:05 +01:00
Ruben Fiszel
2dfbf84d62 chore(main): release 1.60.0 (#1086)
* chore(main): release 1.60.0

* Update CHANGELOG.md

* Apply automatic changes

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-01-11 12:14:48 +01:00
Ádám Kovács
cb25f88300 fix(frontend): Keep pane resizer under open drawer (#1089) 2023-01-11 12:06:05 +01:00
Ádám Kovács
3cbe5d1f66 fix(frontend): Remove comment remnant (#1088) 2023-01-11 11:38:55 +01:00
Ádám Kovács
e93c511bb5 fix(frontend): App form component (#1087)
* fix(frontend): Make app form usage more clear

* fix(frontend): Form orientation

* fix(frontend): Form message format

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-01-11 11:24:46 +01:00
Ruben Fiszel
6bc0320bff add Use as template for apps 2023-01-11 11:00:10 +01:00
Ruben Fiszel
3418a07c45 various app fixes 2023-01-11 10:57:30 +01:00
Ruben Fiszel
c0df9a5e20 update axum 2023-01-11 08:19:20 +01:00
Ruben Fiszel
b4e9468461 update change-versions 2023-01-11 04:54:55 +01:00
Ruben Fiszel
a14623feaa feat: add 'add user to workspace' 2023-01-11 00:51:04 +01:00
Ádám Kovács
342ea5609b fix(frontend): App minor fixes (#1085)
* fix(frontend): Tooltip contrast

* fix(frontend): App button padding
2023-01-10 22:53:11 +01:00
Ádám Kovács
8e72007368 feat(frontend): Add frost to color palette (#1084) 2023-01-10 22:52:37 +01:00
Ruben Fiszel
48d87ab12c various improvements 2023-01-10 11:21:16 +01:00
dependabot[bot]
4e0c2d06fc chore(deps-dev): bump simple-svelte-autocomplete in /frontend (#1083)
Bumps [simple-svelte-autocomplete](https://github.com/pstanoev/simple-svelte-autocomplete) from 2.5.0 to 2.5.1.
- [Release notes](https://github.com/pstanoev/simple-svelte-autocomplete/releases)
- [Commits](https://github.com/pstanoev/simple-svelte-autocomplete/compare/v2.5.0...v2.5.1)

---
updated-dependencies:
- dependency-name: simple-svelte-autocomplete
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-09 20:00:03 +01:00
Ruben Fiszel
cbddd6b98b chore: update svelte-check 2023-01-09 19:59:17 +01:00
dependabot[bot]
35297d4b52 chore(deps): bump svelte-select from 5.0.1 to 5.0.2 in /frontend (#1081)
Bumps [svelte-select](https://github.com/rob-balfre/svelte-select) from 5.0.1 to 5.0.2.
- [Release notes](https://github.com/rob-balfre/svelte-select/releases)
- [Changelog](https://github.com/rob-balfre/svelte-select/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rob-balfre/svelte-select/compare/v5.0.1...v5.0.2)

---
updated-dependencies:
- dependency-name: svelte-select
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-09 18:54:41 +00:00
Ruben Fiszel
2c717e00e5 fix computing policy prefix for hub scripts 2023-01-09 19:46:24 +01:00
dependabot[bot]
3bc1e6248b chore(deps): bump @leeoniya/ufuzzy from 0.8.0 to 0.9.0 in /frontend (#1080)
Bumps [@leeoniya/ufuzzy](https://github.com/leeoniya/uFuzzy) from 0.8.0 to 0.9.0.
- [Release notes](https://github.com/leeoniya/uFuzzy/releases)
- [Commits](https://github.com/leeoniya/uFuzzy/compare/0.8.0...0.9.0)

---
updated-dependencies:
- dependency-name: "@leeoniya/ufuzzy"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-09 18:30:33 +00:00
Ruben Fiszel
2e2067fe13 compute policy also for action buttons 2023-01-09 19:24:16 +01:00
dependabot[bot]
367903e359 chore(deps-dev): bump @playwright/test in /frontend (#1079)
Bumps [@playwright/test](https://github.com/Microsoft/playwright) from 1.29.1 to 1.29.2.
- [Release notes](https://github.com/Microsoft/playwright/releases)
- [Commits](https://github.com/Microsoft/playwright/compare/v1.29.1...v1.29.2)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-09 18:23:48 +00:00
dependabot[bot]
37f2de6196 chore(deps-dev): bump svelte-dnd-action in /frontend (#1076)
Bumps [svelte-dnd-action](https://github.com/isaacHagoel/svelte-dnd-action) from 0.9.21 to 0.9.22.
- [Release notes](https://github.com/isaacHagoel/svelte-dnd-action/releases)
- [Changelog](https://github.com/isaacHagoel/svelte-dnd-action/blob/master/release-notes.md)
- [Commits](https://github.com/isaacHagoel/svelte-dnd-action/commits)

---
updated-dependencies:
- dependency-name: svelte-dnd-action
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-09 18:21:24 +00:00
Ruben Fiszel
5505d0a7c9 fix infering schema for inlineeditor 2023-01-09 18:57:30 +01:00
Ruben Fiszel
c61f9d4d66 fix infering schema for inlineeditor 2023-01-09 18:46:59 +01:00
Ruben Fiszel
18946918c3 also update schema when moving flow 2023-01-09 17:26:45 +01:00
Ruben Fiszel
e676c18b5f fix approval page approve endpoint empty payload 2023-01-09 17:19:56 +01:00
Ruben Fiszel
a9cce15b0c nit: close drawer when connecting oauth in new window 2023-01-09 16:52:05 +01:00
Ruben Fiszel
eb49dd8216 nits: clarify resume messages 2023-01-09 16:46:36 +01:00
Ruben Fiszel
e2f2db2d7f edit operatorOnly on toggle change 2023-01-09 16:42:02 +01:00
Ruben Fiszel
6e9c61a7fb (frontend) remove duplicate of folders from folder list name 2023-01-09 16:14:38 +01:00
Ruben Fiszel
4d5acf6f52 creator of group is also automatically a member 2023-01-09 16:08:10 +01:00
Ruben Fiszel
55e2bf9fce bump backend versions 2023-01-09 15:35:28 +01:00
Ruben Fiszel
daf8e2e8be fix backend tests 2023-01-09 14:51:27 +01:00
Ruben Fiszel
b5b13518da chore(main): release 1.59.0 (#1074)
* chore(main): release 1.59.0

* Apply automatic changes

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-01-09 14:03:36 +01:00
Ruben Fiszel
d3e2cf0779 fix backend tests 2023-01-09 14:00:17 +01:00
Ruben Fiszel
5347cd46a9 feat: add relative imports for python scripts (#1075)
* all

* all
2023-01-09 13:49:30 +01:00
Ruben Fiszel
32d90d4f18 add svelte as peer dep 2023-01-09 11:48:38 +01:00
Ruben Fiszel
1076c4ffc1 bump versions 2023-01-09 11:46:30 +01:00
Ádám Kovács
a84eb9b1f7 fix(frontend): Iconed resource height issue (#1073) 2023-01-09 11:44:16 +01:00
Ruben Fiszel
9540e6cec3 add appwrite and linkding logos 2023-01-07 19:16:41 +01:00
Ruben Fiszel
54ef5a6d87 chore(main): release 1.58.0 (#1061)
* chore(main): release 1.58.0

* Apply automatic changes

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-01-07 17:00:46 +08:00
Ruben Fiszel
6edf9b9946 feat: add archive/unarchive/delete workspace 2023-01-07 09:39:50 +01:00
Ruben Fiszel
0bcdcaedcf feat: add min/max constraint to number + slider component 2023-01-07 08:51:09 +01:00
Ruben Fiszel
50453ca690 feat: add hub support for apps 2023-01-07 07:44:44 +01:00
Faton Ramadani
b5edb7ed98 App UI improvements (#1072)
* fix(frontend): Improve App UI

* fix(frontend):Align typing

* fix(frontend): Improve App UI

* fix(frontend): remove debug string

* fix(frontend): remove debug string

* fix(frontend): restore code from main
2023-01-06 17:46:45 +01:00
Ruben Fiszel
099c4fd10d add debug Runs to apps 2023-01-05 08:54:18 +01:00
Ruben Fiszel
3fba91084d improve job loader in case of concurrent requests 2023-01-05 05:57:22 +01:00
Ruben Fiszel
aaac41761c ensuring results come-in order for testjobloader 2023-01-04 21:01:40 +01:00
Ruben Fiszel
1967413ad4 remove early return executeComponent 2023-01-04 20:24:01 +01:00
Ruben Fiszel
7aa4751531 fix some apps' components default sizing 2023-01-04 20:05:55 +01:00
Ruben Fiszel
37a8789741 remove spurrious console logs 2023-01-04 19:55:31 +01:00
Ruben Fiszel
3e9a6ebc5a hide spurrious Unexpected usage 2023-01-04 19:03:31 +01:00
Ruben Fiszel
93ce9c42fe fix overflow on component object viewer 2023-01-04 18:55:33 +01:00
Ruben Fiszel
7437dc00aa apps improvements 2023-01-04 18:37:42 +01:00
Ruben Fiszel
47b498da80 fix flow input transform default bug 2023-01-04 15:57:19 +01:00
Ruben Fiszel
1c75bf7bef add more apps components 2023-01-04 15:54:23 +01:00
Ruben Fiszel
ad1ffdb613 fix template editor monaco workers 2023-01-04 10:56:15 +01:00
Ruben Fiszel
5204e4a75d feat: add support for yaml format as a string format 2023-01-04 10:37:13 +01:00
Ruben Fiszel
aaaece99e7 apps UX nits 2023-01-04 10:14:03 +01:00
Ruben Fiszel
03da654714 improve draft logic for scripts/apps/flows 2023-01-04 07:22:30 +01:00
Ruben Fiszel
43a7d9d7b4 apps UX nits 2023-01-04 05:35:01 +01:00
Ruben Fiszel
68a09176d3 add run button directly in inline editor 2023-01-04 05:27:47 +01:00
Ruben Fiszel
8958118bef fix apps UX nits 2023-01-04 05:07:18 +01:00
Ruben Fiszel
82b30f4da6 fix forking hub deno scripts in apps 2023-01-03 15:56:02 +01:00
Ruben Fiszel
2acb7d1774 nit fix incorrect overflow-auto in display result 2023-01-03 11:17:15 +01:00
Ruben Fiszel
976c3d49dd add context to context panel in apps 2023-01-03 10:41:51 +01:00
Ruben Fiszel
fa522da565 apps improvements 2023-01-03 09:46:36 +01:00
Ruben Fiszel
8517c25153 disallow loading public app if not visible anymore 2023-01-03 03:29:35 +01:00
dependabot[bot]
c2a733dc42 chore(deps-dev): bump eslint-config-prettier in /frontend (#1068)
Bumps [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) from 8.5.0 to 8.6.0.
- [Release notes](https://github.com/prettier/eslint-config-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-config-prettier/compare/v8.5.0...v8.6.0)

---
updated-dependencies:
- dependency-name: eslint-config-prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-02 18:20:05 +00:00
dependabot[bot]
d8feaf341a chore(deps-dev): bump @typescript-eslint/parser in /frontend (#1067)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.46.1 to 5.48.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.48.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-02 18:13:16 +00:00
dependabot[bot]
4c25db3891 chore(deps-dev): bump @types/node from 18.11.9 to 18.11.18 in /frontend (#1066)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 18.11.9 to 18.11.18.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-02 18:13:00 +00:00
dependabot[bot]
b0c7d9af27 chore(deps-dev): bump @playwright/test in /frontend (#1065)
Bumps [@playwright/test](https://github.com/Microsoft/playwright) from 1.28.1 to 1.29.1.
- [Release notes](https://github.com/Microsoft/playwright/releases)
- [Commits](https://github.com/Microsoft/playwright/compare/v1.28.1...v1.29.1)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-02 18:12:40 +00:00
Ádám Kovács
76a860fe53 fix(frontend): Remove popover hover styles (#1064) 2023-01-02 19:05:29 +01:00
Ruben Fiszel
5de7dc9248 further public apps improvements 2023-01-02 19:02:40 +01:00
Ádám Kovács
45acb89f87 feat(frontend): Add integration icons (#1063) 2023-01-02 18:54:01 +01:00
Ádám Kovács
34e3331919 feat(frontend): Update app auto-refresh button (#1062)
* feat(frontend): Update interval selector

* fix(frontend): Interval

* feat(frontend): Resource saving

* fix(frontend): Update selector labels

* label
2023-01-02 18:53:47 +01:00
Ruben Fiszel
39e7fc6c28 tighten public apps security using triggerable policy 2023-01-02 18:53:10 +01:00
Ruben Fiszel
39e2bf39e9 make app public page not require login at all 2023-01-02 13:02:45 +01:00
Ádám Kovács
4d6a568820 feat(frontend): Toggle to hide optional inputs (#1060) 2023-01-02 10:09:33 +01:00
Ruben Fiszel
a34ae8b2ae improve apps wiring 2023-01-02 10:07:17 +01:00
Ruben Fiszel
c7e60843d4 improve apps wiring 2023-01-02 10:07:15 +01:00
Ruben Fiszel
160dfa3121 chore(main): release 1.57.1 (#1059)
* chore(main): release 1.57.1

* Apply automatic changes

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-01-02 04:45:09 +01:00
Ruben Fiszel
2c8e98a9c7 fix: preserver order changes for flows' schema 2023-01-02 04:43:28 +01:00
Ruben Fiszel
334ec1a084 remove buggy transition fade on code editor 2023-01-02 04:28:23 +01:00
Ruben Fiszel
ab0aeb0df8 fix: support setting undefined states 2023-01-02 04:22:56 +01:00
Ruben Fiszel
21f98e29ea chore(main): release 1.57.0 (#1043)
* chore(main): release 1.57.0

* Apply automatic changes

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-01-01 19:23:04 +01:00
Ruben Fiszel
be14aab9b1 feat: apps can be published publicly 2023-01-01 19:16:26 +01:00
Ruben Fiszel
e24dc6de4f update all clients to use update resource value 2023-01-01 10:26:11 +01:00
Ruben Fiszel
4bc90e8409 improve design of items picker 2023-01-01 10:02:26 +01:00
Ruben Fiszel
4822c863f8 deprecate starters workspace by pointing resource types to admins 2023-01-01 09:00:56 +01:00
Ruben Fiszel
8f3649e940 fix is_owner for groups/folders + unift summary for apps 2023-01-01 07:23:43 +01:00
Ruben Fiszel
4135b6a4d5 apps improvements (row size, text options) 2023-01-01 06:24:21 +01:00
Ruben Fiszel
5c8bfb4d39 apps nits 2022-12-31 18:55:47 +01:00
Ádám Kovács
72aeba121c fix(frontend): Static inputs overflow (#1057)
* fix(frontend): Static inputs overflow

* Update FlowConstantsItem.svelte

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2022-12-31 13:37:23 +01:00
Ruben Fiszel
9fa2f96b31 overhaul app creation flow 2022-12-31 13:34:59 +01:00
Ruben Fiszel
94e929ddc4 restore 12 columns for apps but introduce fullscreen mode 2022-12-31 10:04:32 +01:00
Ruben Fiszel
5e522c5720 set default for template editor 2022-12-31 07:50:18 +01:00
Ruben Fiszel
4df19ab873 add autosave for apps 2022-12-31 07:36:05 +01:00
Ruben Fiszel
eb359b1a3d re-enable page titles 2022-12-31 05:08:47 +01:00
Ruben Fiszel
58a5889dc7 app improvements 2022-12-30 19:46:31 +01:00
Ruben Fiszel
e9488f1b66 app improvements 2022-12-30 19:04:50 +01:00
Ruben Fiszel
a1fec7c4b7 fix init edit on variable editor 2022-12-30 16:59:04 +01:00
Ruben Fiszel
54a5e73117 fix login redirection 2022-12-30 15:32:51 +01:00
Ádám Kovács
0812f6efd8 feat(frontend): Add customization props to radio (#1056) 2022-12-30 12:40:57 +01:00
Ruben Fiszel
3192ce5d21 more tweaks 2022-12-30 12:11:48 +01:00
Ruben Fiszel
e8fc94d7ef further apps improvements 2022-12-30 11:55:06 +01:00
Ruben Fiszel
aedd02bc2a app reactivity improvements 2022-12-30 09:56:07 +01:00
Ruben Fiszel
d108c3ed07 flow constants page improvements 2022-12-30 06:23:59 +01:00
Ruben Fiszel
3296debfe7 feat: add a All Static Inputs module to the flow editor 2022-12-30 05:31:45 +01:00
Ádám Kovács
2fd730f8d2 fix(frontend): List item overflowing corners (#1055) 2022-12-30 05:15:10 +07:00
Ruben Fiszel
a42f802cfd items-list overflow to let dropdown go through 2022-12-29 19:33:14 +01:00
Ádám Kovács
adc84f06d9 fix(frontend): Minor fixes in editor (#1054)
* fix(frontend): Script arguments window overflow

* fix(frontend): Consistent input editor font size

* fix(frontend): Fix script settings inconsistencies
2022-12-29 18:44:56 +01:00
Ádám Kovács
9d30e5fa57 feat(frontend): Update tooltip and home list dropdown (#1053)
* feat(frontend): Update tooltip

* feat(frontend): Update list dropdown
2022-12-29 18:38:28 +01:00
Ruben Fiszel
6e271d3d61 fix dropdown 2022-12-29 15:57:27 +01:00
Kai Jellinghaus
543490b103 Allow workspace to be newly created (#1052) 2022-12-29 20:31:01 +07:00
Ruben Fiszel
f4c5de3d2b fix app anchors 2022-12-29 13:53:20 +01:00
Ruben Fiszel
647b57b24e add healthchecks to lsp 2022-12-29 12:52:30 +01:00
Ruben Fiszel
7d8cbfe115 delete comments from flow of the hub 2022-12-29 12:49:27 +01:00
Ádám Kovács
0f1b19c7d3 fix(app): Fix inconsistencies in list items and sidebar menus (#1051)
* fix(app): Fix minor issues in home page list

* fix(app): Remove inconsistencies in sidebar menus
2022-12-29 12:48:26 +01:00
Faton Ramadani
fb395655e6 App bugfix (#1049)
* adapt static adapter to cloudflare

* fix(frontend): Various bug fix

* fix typecheck

* numerous fixes

* fix all connects

* fix all connects

* more improvements

* more improvements

* everything work?

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2022-12-29 12:47:53 +01:00
Ádám Kovács
faa046a3fd feat(app): Update sidebar menu (#1050) 2022-12-29 15:55:53 +07:00
Ruben Fiszel
17a5b3ffdb fix testjobloader when running many interleaving computations 2022-12-28 17:11:58 +01:00
Ruben Fiszel
47fa61ba30 fix raw editor height for resources 2022-12-28 13:24:56 +01:00
Ruben Fiszel
efe3973c29 fix test just this step freeze svelte bc of transition:fade 2022-12-28 12:52:21 +01:00
Ruben Fiszel
26ea59b3e9 fix test just this step freeze svelte bc of transition:fade 2022-12-28 12:51:04 +01:00
Ádám Kovács
681e2e824a feat(app): Use consistent styles on settings pages (#1048)
* feat(app): Update login page style

* feat(app): Update pages with centered modal

* fix(app): Update settings

* adapt static adapter to cloudflare

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2022-12-28 00:39:30 +01:00
Ruben Fiszel
2d4a83bf90 adapt static adapter to cloudflare 2022-12-28 00:25:00 +01:00
Ruben Fiszel
8031a7b5ec adapt static adapter to cloudflare 2022-12-27 17:59:12 +01:00
Ruben Fiszel
b3adb7dad8 fix approval page for sveltekit 1.0 2022-12-27 17:21:45 +01:00
Ruben Fiszel
6d40f16695 add auto_https on caddyfileremote 2022-12-27 16:32:41 +01:00
Ruben Fiszel
a29357eb15 fix confirmation modal enter behavior 2022-12-27 16:29:34 +01:00
Ruben Fiszel
b72a3babd8 fix is_owner 2022-12-27 15:52:12 +01:00
Ruben Fiszel
4e1e80e673 nit redirect on logout 2022-12-27 13:38:34 +01:00
Ruben Fiszel
8c268ff34b add dedicated logout page 2022-12-27 13:26:03 +01:00
Ruben Fiszel
1b0bb22da7 fix layout for sveltekit 1.0 2022-12-27 07:27:14 +01:00
dependabot[bot]
b24f3472d1 chore(deps): bump svelte-select from 5.0.0-beta.40 to 5.0.1 in /frontend (#1046)
Bumps [svelte-select](https://github.com/rob-balfre/svelte-select) from 5.0.0-beta.40 to 5.0.1.
- [Release notes](https://github.com/rob-balfre/svelte-select/releases)
- [Changelog](https://github.com/rob-balfre/svelte-select/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rob-balfre/svelte-select/compare/v5.0.0-beta.40...v5.0.1)

---
updated-dependencies:
- dependency-name: svelte-select
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-26 18:11:39 +00:00
dependabot[bot]
50c909d009 chore(deps-dev): bump @types/vscode from 1.68.1 to 1.74.0 in /frontend (#1047)
Bumps [@types/vscode](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/vscode) from 1.68.1 to 1.74.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/vscode)

---
updated-dependencies:
- dependency-name: "@types/vscode"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-26 18:11:03 +00:00
Ruben Fiszel
a82ed3f744 trim imports from monaco 2022-12-25 12:58:14 +01:00
Ruben Fiszel
99fd968eba update fontAwesome icons 2022-12-25 11:55:01 +01:00
Ruben Fiszel
0e97b98d21 update svelte-preprocess to 5.x 2022-12-25 11:46:14 +01:00
dependabot[bot]
37db806723 chore(deps-dev): bump prettier from 2.7.1 to 2.8.1 in /frontend (#1031)
Bumps [prettier](https://github.com/prettier/prettier) from 2.7.1 to 2.8.1.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.7.1...2.8.1)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2022-12-25 10:34:43 +00:00
Ruben Fiszel
b805e28adf update adapter-kit 2022-12-25 08:58:16 +01:00
Ruben Fiszel
972b2affae minor app fixes 2022-12-25 08:39:00 +01:00
Ruben Fiszel
bf86fe5041 update to sveltekit 1.0 2022-12-25 08:05:39 +01:00
Ruben Fiszel
e7cdd72a26 svelte-migrate: renamed files 2022-12-25 05:49:06 +01:00
Ruben Fiszel
3b17820533 svelte-migrate: renamed files 2022-12-25 05:48:44 +01:00
Ruben Fiszel
eee8fcee75 nit: danger zone for flow details 2022-12-24 19:37:38 +01:00
Ruben Fiszel
037de7ec95 improve flow graph rendering behavior 2022-12-24 19:29:50 +01:00
Ruben Fiszel
0e585a81a5 resuming suspended flows as owner is now through flow id 2022-12-24 09:44:05 +01:00
Ruben Fiszel
f1d0db3bc7 make flow status viewer always open 2022-12-24 06:31:22 +01:00
Ruben Fiszel
749db01ab2 allow updating to latest hash from within flow directly if module hash locked 2022-12-24 06:24:27 +01:00
Ruben Fiszel
19ff6faf35 border around template editor 2022-12-23 17:59:42 +01:00
Ruben Fiszel
39b3c57539 fix navigation on index list rows 2022-12-23 17:40:19 +01:00
Faton Ramadani
70e7a5d075 feat(frontend): Fix initial component dimensions + Select select + add spinner when buttons are clicked (#1044) 2022-12-23 21:11:13 +07:00
Ruben Fiszel
2bc297f2e9 allow resuming as owner suspended job only for path owners 2022-12-23 13:44:41 +01:00
Faton Ramadani
65f4e86a22 feat(frontend): Properly support resource (#1039)
* feat(frontend): Properly support resource

* feat(frontend): remove unused import

* feat(frontend): Fix build errors

* feat(frontend): Fix table actions

* feat(frontend): Fix table parameters

* feat(frontend): Fix runnable inputs sync

* feat(frontend): Done

* fix

* fix

* feat(frontend): Fix typing issues

* feat(frontend): Fix id generation

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2022-12-23 13:42:42 +01:00
Ruben Fiszel
40195d42f6 feat(backend): resume from owner directly in flow status viewer (#1042)
* foo

* progress
2022-12-23 13:04:31 +01:00
Ruben Fiszel
2aeab245a1 schedule next job in transaction to avoid race condition 2022-12-23 06:53:26 +01:00
Ruben Fiszel
85e8b7c78d chore(main): release 1.56.1 (#1041)
* chore(main): release 1.56.1

* Apply automatic changes

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2022-12-23 03:33:50 +01:00
Ruben Fiszel
0614ec42ba fix(cli): typo in cli deps 2022-12-23 03:31:58 +01:00
Ruben Fiszel
03c7b38c94 chore(main): release 1.56.0 (#1011)
* chore(main): release 1.56.0

* Apply automatic changes

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2022-12-23 03:10:03 +01:00
Kai Jellinghaus
741646b618 feat!(cli): Folders support & Less Tarball nesting (#1040)
* Basic folder support

* Proper Folder Support + deps.ts

* Upgrade Versions

* Add folder meta to tarball

* Remove tarball folders

* Minor fixes

* Fix typo

* Remove extra_perms check

* Use new endpoint

* Use new untar location

* Fix shrinking files
2022-12-23 03:06:12 +01:00
Ruben Fiszel
067da91463 make ownership check more consistent, expose ownership api, expose update folder api 2022-12-22 14:48:57 +01:00
Ruben Fiszel
c28311242d feat(backend): add SUPERADMIN_SECRET as an env set superadmin 2022-12-22 10:35:10 +01:00
Ruben Fiszel
e228c6448e feat(frontend): templatable editor with autocompletion 2022-12-22 08:38:12 +01:00
Faton Ramadani
cebbc5fbd1 feat(frontend): Fix component synchro (#1038)
* feat(frontend): Fix component synchro

* feat(frontend): Fix isObject
2022-12-21 11:19:27 +01:00
Ruben Fiszel
420230ef17 minor editor improvements 2022-12-21 09:46:12 +01:00
Faton Ramadani
ab13e8cce4 feat(frontend): Fork + Fix table (#1037)
* feat(frontend): WIP

* feat(frontend): WIP

* feat(frontend): add support unused inline scripts

* feat(frontend): fix all interaction

* feat(frontend): Fix pick

* feat(frontend): add ability to fork

* feat(frontend): fix mobile preview

* feat(frontend): fix table

* feat(frontend): fix table actions

* feat(frontend): Fix build error
2022-12-21 08:22:30 +01:00
Ruben Fiszel
f73dbd8039 feat: add move to drawer for script and flows 2022-12-21 06:55:37 +01:00
Faton Ramadani
26a6de247c feat(frontend): Add the ability to lock components so they don't move around (#1035) 2022-12-20 16:53:18 +01:00
Kai Jellinghaus
3b8356cf9a Allow RUST_LOG_STYLE (#1036) 2022-12-20 16:19:18 +01:00
Faton Ramadani
be743117d1 feat(frontend): Fix inline script status (#1034) 2022-12-20 16:17:45 +01:00
Ádám Kovács
4651c9d8cd feat(frontend): App builder password and date input (#1022)
* feat: Add password input to app builder
2022-12-20 15:34:36 +01:00
Ruben Fiszel
b7afe5ecfc improve error message for already existing items 2022-12-20 12:52:18 +01:00
Ruben Fiszel
f195d4c4a5 improve folder UX 2022-12-20 10:47:29 +01:00
Faton Ramadani
95a0b9ceae feat(frontend): Add an input field to edit inline script name (#1033) 2022-12-20 10:34:50 +01:00
Ruben Fiszel
27e840ced1 improve error message for oauth 2022-12-20 08:49:23 +01:00
Ruben Fiszel
f91967c4c8 improve error message for oauth 2022-12-20 08:20:21 +01:00
Ruben Fiszel
803318d0b5 align share with folder permissions 2022-12-20 07:18:10 +01:00
Ruben Fiszel
cd7bc33b6a re-enable picking groups 2022-12-20 07:18:10 +01:00
Ruben Fiszel
92dee8b958 Update README.md 2022-12-20 03:21:03 +01:00
Ruben Fiszel
f8e01257a3 fix folders queries on RDS 2022-12-19 21:01:12 +01:00
Ruben Fiszel
4329d25988 feat: introduce folders, deprecate items owned by groups 2022-12-19 19:11:23 +01:00
Ruben Fiszel
c1b0b64e17 feat: introduce folders, deprecate items owned by groups 2022-12-19 18:59:46 +01:00
Ruben Fiszel
876e140626 change the default number of connections to 3 for workers 2022-12-19 18:59:46 +01:00
Faton Ramadani
b4e2735315 Inline script editor (#1029)
* feat(frontend): WIP

* feat(frontend): WI

* feat(frontend): Fix inline scripts

* feat(frontend): Fix inline scripts
2022-12-19 22:44:28 +07:00
Ruben Fiszel
f5a01fc768 major schedules rewrite 2022-12-18 18:45:32 +01:00
Ruben Fiszel
43732d2fb5 require ownership to move items && automatically edit schedule paths 2022-12-18 18:45:32 +01:00
Jakub Kołodziejczak
f77fe7b6b3 fix(frontend): copy-to-clipnoard url with protocol (#1027) 2022-12-18 15:38:12 +01:00
Ruben Fiszel
42d4711503 path improvements v1 2022-12-18 05:55:29 +01:00
Ruben Fiszel
398c109afe UX improvements 2022-12-17 17:53:32 +01:00
Ruben Fiszel
321f8d28bb add audit log for oauth login 2022-12-17 13:54:46 +01:00
Ruben Fiszel
d2ab628122 fix setting timeout duration error 2022-12-17 13:41:33 +01:00
Ruben Fiszel
576103e3b3 group refactoring 2022-12-16 20:02:13 +01:00
Faton Ramadani
ebca9f39ea feat(frontend): Small UI fixes (#1026) 2022-12-16 14:00:43 +01:00
Faton Ramadani
0e871ca843 feat(frontend): Align output panel UI (#1025)
* feat(frontend): Align output pane UI

* feat(frontend): Align UI
2022-12-16 14:00:26 +01:00
Ruben Fiszel
80c84d190e merge fix 2022-12-16 10:01:53 +01:00
Ruben Fiszel
133d1f3e56 fix v2 2022-12-16 10:01:24 +01:00
Ruben Fiszel
091af3220a reinstate editorService fixing the editor 2022-12-16 09:57:58 +01:00
Ruben Fiszel
3e7751f705 groups 2022-12-16 09:53:17 +01:00
Ruben Fiszel
3735171a0d Flow UX improvements wrt to triggers 2022-12-15 23:09:23 +01:00
Ruben Fiszel
2e7b2a9e20 rework default visibility of runs of scripts not belonging to oneself 2022-12-15 18:55:30 +01:00
Ruben Fiszel
09e38981ed add visible_to_owner 2022-12-15 16:01:17 +01:00
Faton Ramadani
f6df3ae367 feat(frontend): AppTable v2 + Inline script panel (#1023)
* feat(frontend): WIP

* feat(frontend): Refator

* feat(frontend): Fix inline script creation

* feat(frontend): Fix chart display bug

* feat(frontend): Fix build
2022-12-15 15:45:04 +01:00
Ádám Kovács
08071bb66b feat(frontend): Add select component to app builder (#1021)
* feat(frontend): Add select input to app builder
2022-12-15 08:51:43 +00:00
Ruben Fiszel
d1edf4e5c7 replace svelvet with fork to remove zoom and node moving 2022-12-15 01:53:35 +01:00
Ruben Fiszel
847dfe1c15 ux nits 2022-12-14 21:29:00 +01:00
Ruben Fiszel
af8e0ccd6a increase banned_domains list 2022-12-14 10:24:18 +01:00
Ruben Fiszel
8f0ad7c612 team plan 2022-12-14 01:17:17 +01:00
Ruben Fiszel
5f30970703 team plan 2022-12-13 23:09:33 +01:00
Ruben Fiszel
974dc2394f UX improvements 2022-12-13 21:24:35 +01:00
Ruben Fiszel
3485b07b25 feat: add operator mode 2022-12-13 20:16:57 +01:00
Ruben Fiszel
d813c007c0 add more metrics 2022-12-13 00:25:27 +01:00
Ruben Fiszel
48ca1e939c add more metrics 2022-12-12 23:10:53 +01:00
Ruben Fiszel
d3d2a8bc7d fix fixtures 2022-12-12 22:33:24 +01:00
Ruben Fiszel
2bae50f391 feat: auto-invite from same domain 2022-12-12 20:20:17 +01:00
dependabot[bot]
c8bd68b56b chore(deps-dev): bump @typescript-eslint/parser in /frontend (#1019)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.43.0 to 5.46.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.46.1/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-12 18:27:39 +00:00
dependabot[bot]
5d7cf4ca09 chore(deps): bump highlight.js from 11.6.0 to 11.7.0 in /frontend (#1020)
Bumps [highlight.js](https://github.com/highlightjs/highlight.js) from 11.6.0 to 11.7.0.
- [Release notes](https://github.com/highlightjs/highlight.js/releases)
- [Changelog](https://github.com/highlightjs/highlight.js/blob/main/CHANGES.md)
- [Commits](https://github.com/highlightjs/highlight.js/compare/11.6.0...11.7.0)

---
updated-dependencies:
- dependency-name: highlight.js
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-12 18:24:24 +00:00
dependabot[bot]
ab83f899b5 chore(deps-dev): bump @typescript-eslint/eslint-plugin in /frontend (#1016)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.45.0 to 5.46.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.46.1/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-12 18:15:04 +00:00
dependabot[bot]
14e85cd5b0 chore(deps-dev): bump prettier-plugin-svelte in /frontend (#1017)
Bumps [prettier-plugin-svelte](https://github.com/sveltejs/prettier-plugin-svelte) from 2.8.1 to 2.9.0.
- [Release notes](https://github.com/sveltejs/prettier-plugin-svelte/releases)
- [Changelog](https://github.com/sveltejs/prettier-plugin-svelte/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/prettier-plugin-svelte/commits)

---
updated-dependencies:
- dependency-name: prettier-plugin-svelte
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-12 18:14:37 +00:00
dependabot[bot]
b36a9c2cc8 chore(deps): bump python from 3.11.0-slim-buster to 3.11.1-slim-buster (#1014)
Bumps python from 3.11.0-slim-buster to 3.11.1-slim-buster.

---
updated-dependencies:
- dependency-name: python
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-12 18:10:40 +00:00
Faton Ramadani
f7627b5f17 fix(frontend): Fix AppTable frontend search (#1013)
* fix(frontend): Fix AppTable frontend search

* fix(frontend): Fix AppTable frontend search
2022-12-12 18:42:15 +01:00
Faton Ramadani
424c31c54a feat(frontend): Add form component + fix connection bug (#1012) 2022-12-12 17:05:54 +01:00
Ádám Kovács
2fe927f7fd feat(frontend): Add app number input (#1010)
Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
2022-12-12 14:40:32 +01:00
Faton Ramadani
0ceb4ab1a8 feat(frontend): Fix UI (#1009)
* feat(frontend): Fix UI

* feat(frontend): Set correct default value when adding a new element to a typed array

* feat(frontend): add refresh all

* feat(frontend): add inline delete button

* feat(frontend): fix alignment

* feat(frontend): clean up

* feat(frontend): rework editor

* feat(frontend): Fix component dimensions

* feat(frontend): Fix default min dimensions

* feat(frontend): add missing alert

* feat(frontend): Fix default data

* feat(frontend): Support frontend/backend search

* feat(frontend): finish picker
2022-12-12 14:35:17 +01:00
Ruben Fiszel
94cecd56d2 add the all group to admin 2022-12-12 12:47:07 +01:00
Ruben Fiszel
b920345de2 improve perceived performances 2022-12-12 02:26:31 +01:00
Ruben Fiszel
bb531845b4 improve perceived performances 2022-12-12 02:05:32 +01:00
Ruben Fiszel
4daf5c18fa usage tracker v3 2022-12-12 01:11:44 +01:00
Ruben Fiszel
ac6f9e90f0 usage tracker v2 2022-12-11 22:48:09 +01:00
Ruben Fiszel
46f3388632 UX nits 2022-12-11 21:32:07 +01:00
Ruben Fiszel
9a5fb6b9e5 UX nits 2022-12-11 21:24:48 +01:00
Ruben Fiszel
c77e4a06c0 grant all on usage 2022-12-11 20:27:04 +01:00
Ruben Fiszel
fd871093f0 feat: implement usage tracker + quotas 2022-12-11 18:47:39 +01:00
Ruben Fiszel
e4d61eeb6e remove ButtonAndDropdown 2022-12-10 20:43:32 +01:00
Ruben Fiszel
6fe10b210f split states from resources on resources page 2022-12-10 18:47:43 +01:00
Ruben Fiszel
5a325c9e51 remove virtual list 2022-12-10 16:35:03 +01:00
Ruben Fiszel
2e36f65986 increase variable length to 10000 2022-12-10 13:57:41 +01:00
Ruben Fiszel
f4f359e846 add more default linkedin scopes 2022-12-10 13:40:15 +01:00
Ruben Fiszel
cabadc3849 add more icons 2022-12-10 13:39:25 +01:00
Ruben Fiszel
df6049bdc5 add all current resource types icons 2022-12-10 13:05:48 +01:00
Ruben Fiszel
46975aceab add microsoft login 2022-12-10 12:18:40 +01:00
Kai Jellinghaus
4004de0618 feat: Superadmins workspace (#1003)
* Add auto-invite superadmins workspace

* Revert "Add auto-invite superadmins workspace"

This reverts commit 768ae31857.

* Add Admin Workspace

* Show unable to delete info in UI

* Add Hub sync

* Update Migration

* Add Starter workspace

* Add workspace_key for admins workspace

* Remove schedule

* Update CI
2022-12-10 01:46:26 +01:00
Ruben Fiszel
dc23af4209 chore(main): release 1.55.0 (#1007)
* chore(main): release 1.55.0

* Apply automatic changes

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2022-12-09 20:48:58 +01:00
Ruben Fiszel
0d421f1506 display account without refresh tokens as non refreshed 2022-12-09 20:17:39 +01:00
Ruben Fiszel
ffc4c3a9a0 add linkedin as oauth integration 2022-12-09 19:38:50 +01:00
Ruben Fiszel
d2dfe29f03 add linkedin as oauth integration 2022-12-09 19:28:00 +01:00
Ruben Fiszel
18c7a33b6d add linkedin as oauth integration 2022-12-09 19:25:12 +01:00
Ruben Fiszel
dbfafda44d add linkedin as oauth integration 2022-12-09 18:21:59 +01:00
Ruben Fiszel
64cda02a17 add linkedin as oauth integration 2022-12-09 17:47:26 +01:00
Ádám Kovács
6198383138 feat(frontend): Add text input to app builder (#1008)
* fix(frontend): Use official Lucide package
* feat(frontend): Add text input to app builder
2022-12-09 16:41:55 +01:00
Ruben Fiszel
8616570bce get schedule from the database directly instead of the client 2022-12-09 16:36:36 +01:00
Ruben Fiszel
87c94d4015 fix list jobs 2022-12-09 14:52:49 +01:00
Ruben Fiszel
f92e581bd1 editor styling 2022-12-09 14:32:50 +01:00
Ruben Fiszel
0bfc7391b4 pin monaco to 0.33 + remove cursor style 2022-12-09 11:01:43 +01:00
Ruben Fiszel
7270baf17f chore(main): release 1.54.0 (#1000)
* chore(main): release 1.54.0

* Apply automatic changes

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2022-12-08 23:57:28 +01:00
Kai Jellinghaus
abc0ca3a5b feat(backend): Input transform errors (#1006)
* Add pre run error & propagate input transforms there

* Handle pre-run errors

* Update SQLX data

* Update SQLX data
2022-12-08 23:56:49 +01:00
Ruben Fiszel
e1f5fcb162 add public-helm to the README 2022-12-08 18:47:44 +01:00
Ruben Fiszel
2e83afbcd8 flow UX improvements 2022-12-08 17:28:24 +01:00
Ruben Fiszel
08bfc1020d UX flow improvements 2022-12-08 17:18:24 +01:00
Ruben Fiszel
1f05a2070d display number of retry attempts 2022-12-08 15:46:24 +01:00
Faton Ramadani
a562dee3ce feat(frontend): Add support for object editor + fix wording (#1004) 2022-12-08 13:11:25 +00:00
Ruben Fiszel
c6e04414e5 sqlx update 2022-12-08 13:32:33 +01:00
Ruben Fiszel
18e1e92c23 add iteration info 2022-12-08 13:30:48 +01:00
Faton Ramadani
3942741c80 App editor v2 (#1001)
* fix(frontend): Fix app InputValue sync

* feat(frontend): WIP

* feat(frontend): WIP

* feat(frontend): Type aligned

* feat(frontend): working

* feat(frontend): working

* feat(frontend): Fix text binding

* feat(frontend): Connect + script working

* feat(frontend): Fix reactity issues
2022-12-08 13:26:01 +01:00
Ruben Fiszel
afe680b6fb display flow errors step much better in the flow status viewer 2022-12-08 12:59:18 +01:00
Ruben Fiszel
8bcf4952a2 display errors much better in the error handler 2022-12-08 11:07:44 +01:00
Kai Jellinghaus
9715baa6b5 Correctly parse schema (#1002) 2022-12-07 14:22:07 +01:00
Ruben Fiszel
37a775d65a handle undefined modules 2022-12-07 13:54:39 +01:00
Ruben Fiszel
4225f2a414 nit fix 2022-12-07 13:40:46 +01:00
Ruben Fiszel
a76ec2f3ab improve branches module editor 2022-12-07 13:20:41 +01:00
Ruben Fiszel
82731e7248 improve branches module editor 2022-12-07 13:10:21 +01:00
Ruben Fiszel
1dc3f915a6 fix flow input picker 2022-12-07 11:04:31 +01:00
Ruben Fiszel
8c1c508358 feat: implement flow as a flow step 2022-12-06 23:38:06 +01:00
Ruben Fiszel
9a029f46af fix flowmodule_serde test 2022-12-06 17:48:56 +01:00
Ruben Fiszel
3b46eb643c early stop now bubble up to the top 2022-12-06 17:39:07 +01:00
Ruben Fiszel
799fa925b3 feat: add lockable version to scripts inside flows (#972)
* add it to openflow

* push

* push

* final
2022-12-06 13:11:11 +01:00
Ruben Fiszel
d5c79d12d8 document include_header in the openapi 2022-12-06 03:22:45 +01:00
Ruben Fiszel
5dbb249dc7 document include_header in the openapi 2022-12-06 03:22:35 +01:00
Ruben Fiszel
3561275be0 chore(main): release 1.53.0 (#976)
* chore(main): release 1.53.0

* Apply automatic changes

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2022-12-06 00:29:41 +01:00
dependabot[bot]
c5cd273728 chore(deps): bump chartjs-plugin-zoom from 1.2.1 to 2.0.0 in /frontend (#997)
Bumps [chartjs-plugin-zoom](https://github.com/chartjs/chartjs-plugin-zoom) from 1.2.1 to 2.0.0.
- [Release notes](https://github.com/chartjs/chartjs-plugin-zoom/releases)
- [Commits](https://github.com/chartjs/chartjs-plugin-zoom/compare/v1.2.1...v2.0.0)

---
updated-dependencies:
- dependency-name: chartjs-plugin-zoom
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-06 00:21:57 +01:00
Ruben Fiszel
c318cb255a handle more waiting case as base case for special primitives in flows 2022-12-06 00:05:02 +01:00
Ruben Fiszel
31c317b358 feat: add include_header to pass request headers to script 2022-12-05 23:36:31 +01:00
Ruben Fiszel
cfe80115a4 improve flow status viewer 2022-12-05 22:32:24 +01:00
Ruben Fiszel
c7c58ad8bc improve flow viewer 2022-12-05 21:57:04 +01:00
Ruben Fiszel
73587a8400 display jobs scheduled for later in flow status viewer 2022-12-05 20:33:28 +01:00
dependabot[bot]
764ca1d7bd chore(deps): bump vscode-ws-jsonrpc from 2.0.0 to 2.0.1 in /frontend (#998)
Bumps [vscode-ws-jsonrpc](https://github.com/TypeFox/monaco-languageclient/tree/HEAD/packages/vscode-ws-jsonrpc) from 2.0.0 to 2.0.1.
- [Release notes](https://github.com/TypeFox/monaco-languageclient/releases)
- [Changelog](https://github.com/TypeFox/monaco-languageclient/blob/main/packages/vscode-ws-jsonrpc/CHANGELOG.md)
- [Commits](https://github.com/TypeFox/monaco-languageclient/commits/v2.0.1/packages/vscode-ws-jsonrpc)

---
updated-dependencies:
- dependency-name: vscode-ws-jsonrpc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-05 18:19:57 +00:00
dependabot[bot]
b32f282b17 chore(deps-dev): bump svelte2tsx from 0.5.20 to 0.5.22 in /frontend (#996)
Bumps [svelte2tsx](https://github.com/sveltejs/language-tools) from 0.5.20 to 0.5.22.
- [Release notes](https://github.com/sveltejs/language-tools/releases)
- [Commits](https://github.com/sveltejs/language-tools/compare/svelte2tsx-0.5.20...svelte2tsx-0.5.22)

---
updated-dependencies:
- dependency-name: svelte2tsx
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-05 18:18:37 +00:00
Ruben Fiszel
ae77a71ed6 fix svelte-markdown in description 2022-12-05 17:35:22 +01:00
Faton Ramadani
e217fbf071 fix(frontend): Fix app InputValue sync (#994)
* fix(frontend): Fix app InputValue sync

* fix(frontend): Fix drawer
2022-12-05 15:19:12 +01:00
Faton Ramadani
c9ad638958 feat(frontend): Add app preview (#993) 2022-12-05 14:48:07 +01:00
Faton Ramadani
9f59a160c3 fix(frontend): Align hub flow list + fix drawer content everywhere (#991) 2022-12-05 13:28:06 +01:00
Ruben Fiszel
35b57e20ed fix frontend errors 2022-12-05 13:26:26 +01:00
Faton Ramadani
62d2a3343d fix(frontend): Fix home margins (#992) 2022-12-05 13:23:52 +01:00
Ruben Fiszel
d64030961b improve flow viewer 2022-12-05 13:21:42 +01:00
Ruben Fiszel
ede6cbea99 improve flow viewer 2022-12-05 13:09:50 +01:00
Ruben Fiszel
177fc2b421 improve flow status viewer 2022-12-05 11:19:26 +01:00
Ruben Fiszel
d73eb26019 allow passing payload to approval method 2022-12-05 01:35:43 +01:00
Ruben Fiszel
8d27175235 fix mysql template 2022-12-05 00:09:35 +01:00
Ruben Fiszel
332bd87e9f searchable resource and variables 2022-12-04 23:57:00 +01:00
Jakub Kołodziejczak
f880b88869 chore(frontend): update types (#990) 2022-12-04 22:22:35 +01:00
Jakub Kołodziejczak
e089109b50 feat(frontend): introduce mysql as a script language (#982)
* fix(deno-client): export mysql from mod.ts + improve robustness

* feat(frontend): introduce mysql as a script language
2022-12-04 19:56:51 +01:00
Faton Ramadani
11ef60fe9c Hub lists (#989)
* fix(frontend): Adapt hub list design

* fix(frontend): revert changes
2022-12-04 18:06:57 +01:00
Kai Jellinghaus
b64d5a621b Add CLI arg (#987) 2022-12-04 13:24:46 +01:00
Kai Jellinghaus
1bac23785c fix(cli): Fix cli pull push (#985)
* Fix workspace tar

* Ignore dotfolders
2022-12-04 13:24:31 +01:00
Faton Ramadani
9264f4b233 fix(frontend): Make context clickable (#984)
* fix(frontend): Make context clickable

* fix(frontend): Fix buttons
2022-12-04 13:24:16 +01:00
Ruben Fiszel
c55fae54dd feat: refactor favorite menu 2022-12-04 13:23:29 +01:00
Ruben Fiszel
60984a3ed8 improve search windows and remove fuse.js 2022-12-04 11:55:26 +01:00
Ruben Fiszel
372f2da5b9 remove flow_input from sleep expression 2022-12-04 01:31:59 +01:00
Ruben Fiszel
db05df6d7e superadmin can see all 2022-12-04 01:19:38 +01:00
Ruben Fiszel
e7e79bbaf7 UX improvements 2022-12-04 01:03:38 +01:00
Ruben Fiszel
e4839e21ff feat(frontend): clarified UX for connect step 2022-12-03 23:54:07 +01:00
Kai Jellinghaus
86eae2c824 feat(frontend); Improve Pie schema (#983)
* Make the pie schema good

* Remove private info

* Fix build error
2022-12-03 20:48:04 +01:00
Ruben Fiszel
5163c2fe8b simplify resource picker 2022-12-03 20:25:02 +01:00
Ruben Fiszel
e51a2161e7 automatically go to resource page add + hub tab size on index 2022-12-03 18:44:39 +01:00
Faton Ramadani
fa64e83f7e fix(frontend): fix home (#981) 2022-12-03 14:24:20 +01:00
Ruben Fiszel
90ce431418 fix(frontend): variables and resources uses tab navigation 2022-12-03 14:23:55 +01:00
Kai Jellinghaus
eb855942f9 Fix clear button for flows (#980) 2022-12-03 13:26:05 +01:00
Faton Ramadani
129a0ad56b fix(frontend): fix app preview (#979) 2022-12-03 12:49:38 +01:00
Ruben Fiszel
7c1b1c068b further resource types clarifications 2022-12-03 12:19:06 +01:00
Ruben Fiszel
d6c039935a resources improvements 2022-12-03 12:14:24 +01:00
Ruben Fiszel
ee83b0620d disable inputs non readable resource 2022-12-03 12:14:24 +01:00
Kai Jellinghaus
84dcde8e64 Expose root command (#978) 2022-12-03 11:41:54 +01:00
Ruben Fiszel
74bda463e1 improve appConnect skeletons 2022-12-03 11:26:49 +01:00
Ruben Fiszel
0ec465ca7e skeletons for home search 2022-12-03 11:03:47 +01:00
Ruben Fiszel
2071a06668 search improvement v1 2022-12-03 10:29:41 +01:00
Faton Ramadani
e9fa0ad0b7 fix(frontend): fix home header (#977)
* fix(frontend): fix home header

* fix(frontend): fix virtual list height
2022-12-03 10:11:05 +01:00
Ruben Fiszel
cafa6d4a0c add unique path and rls to apps 2022-12-03 09:21:26 +01:00
Ruben Fiszel
788688e92f improvements on app connect 2022-12-03 02:18:15 +01:00
Ruben Fiszel
026d755979 add search to appconnect 2022-12-03 02:09:59 +01:00
Ruben Fiszel
16f00ce6db fix app inline scripts 2022-12-03 01:48:39 +01:00
Ruben Fiszel
4e0816337c add resource select it directly 2022-12-03 01:28:20 +01:00
Ruben Fiszel
b20c67d91a fix select picker initialization 2022-12-03 01:13:51 +01:00
Ruben Fiszel
022459dcbd adding a resource is done in inlined-drawer 2022-12-03 00:28:51 +01:00
Ruben Fiszel
4245255fda fix workspace settings script picker 2022-12-03 00:18:06 +01:00
Ruben Fiszel
a4e4e7188f fix creating resources manually 2022-12-03 00:08:52 +01:00
Ruben Fiszel
04d2e3f316 add path env to python req 2022-12-02 23:48:42 +01:00
Ruben Fiszel
519247740a fix PIP_INDEX_URL when nsjail is disabled 2022-12-02 23:45:51 +01:00
Ruben Fiszel
dee9a3bb4a ci: remove community/ and pull-hub ci 2022-12-02 22:55:07 +01:00
Kai Jellinghaus
22653727a4 feat(cli): hub sync (#975)
* Begin add hub sync

* Add Hub sync
2022-12-02 22:51:44 +01:00
597 changed files with 43858 additions and 15056 deletions

View File

@@ -9,6 +9,6 @@ for item in ${RT[@]}; do
jq -r '.resource_type.schema' <<< "$body" > ./tmp
description=$(jq -r '.resource_type.description' <<< "$body")
description=$(echo -E $description)
echo "{\"workspace_id\": \"starter\", \"name\": \"$name\", \"schema\": $(cat ./tmp), \"description\": \"$description\"} " | jq . > community/resource_types/${name}.json
echo "{\"workspace_id\": \"admins\", \"name\": \"$name\", \"schema\": $(cat ./tmp), \"description\": \"$description\"} " | jq . > community/resource_types/${name}.json
rm ./tmp
done

5
.github/uffizzi/caddy/Caddyfile vendored Normal file
View File

@@ -0,0 +1,5 @@
localhost {
bind 0.0.0.0
reverse_proxy /ws/* http://0.0.0.0:3001
reverse_proxy /* http://0.0.0.0:8000
}

View File

@@ -0,0 +1,49 @@
version: '3.7'
x-uffizzi:
ingress:
service: windmill
port: 8000
services:
db:
image: postgres:14
environment:
POSTGRES_PASSWORD: changeme
POSTGRES_DB: windmill
windmill:
image: '${WINDMILL_IMAGE}'
privileged: false
restart: unless-stopped
ports:
- 8000:8000
environment:
- DATABASE_URL=postgres://postgres:changeme@localhost/windmill?sslmode=disable
- BASE_URL=http://localhost
- BASE_INTERNAL_URL=http://localhost:8000
- RUST_LOG=info
- NUM_WORKERS=3
- KEEP_JOB_DIR=false
- DENO_PATH=/usr/bin/deno
- PYTHON_PATH=/usr/local/bin/python3
- METRICS_ADDR=false
volumes:
- worker_dependency_cache:/tmp/windmill/cache
lsp:
image: '${LSP_IMAGE}'
restart: unless-stopped
ports:
- 3001:3001
# caddy:
# image: caddy:2.5.2-alpine
# restart: unless-stopped
# volumes:
# - ./.github/uffizzi/caddy:/etc/caddy
# environment:
# - BASE_URL=localhost
volumes:
worker_dependency_cache:

View File

@@ -40,4 +40,4 @@ jobs:
backend -> target
- name: cargo test
timeout-minutes: 10
run: mkdir frontend/build && cd backend && DATABASE_URL=postgres://postgres:changeme@postgres:5432/windmill cargo test --all -- --nocapture
run: mkdir frontend/build && cd backend && touch windmill-api/openapi-deref.yaml && DATABASE_URL=postgres://postgres:changeme@postgres:5432/windmill cargo test --all -- --nocapture

View File

@@ -12,4 +12,9 @@ jobs:
- uses: actions/checkout@v3
- name: Change versions
run: ./.github/change-versions.sh "$(cat version.txt)"
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: update lockfile
run: cd backend && cargo generate-lockfile
- uses: stefanzweifel/git-auto-commit-action@v4

View File

@@ -1,62 +1,60 @@
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
LOCAL_REGISTRY: registry.wimill.xyz
# env:
# REGISTRY: ghcr.io
# IMAGE_NAME: ${{ github.repository }}
name: Build and push arm docker image
on:
push:
branches: [main]
tags: ["*"]
# name: Build and push arm docker image
# on:
# push:
# branches: [main]
# tags: ["*"]
concurrency:
group: ${{ github.ref }}-arm
cancel-in-progress: true
# concurrency:
# group: ${{ github.ref }}-arm
# cancel-in-progress: true
jobs:
publish_arm:
runs-on: [self-hosted, new]
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
# permissions:
# contents: read
# id-token: write
# packages: write
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
# jobs:
# publish_arm:
# runs-on: ubuntu-22.04
# steps:
# - uses: actions/checkout@v3
# with:
# fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# - uses: depot/setup-action@v1
- name: Docker meta
id: meta-slim-public
uses: docker/metadata-action@v4
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
# - name: Docker meta
# id: meta-slim-public
# uses: docker/metadata-action@v4
# with:
# images: |
# ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# tags: |
# type=ref,event=branch
# type=ref,event=pr
# type=semver,pattern={{version}}
# type=semver,pattern={{major}}.{{minor}}
- name: Login to registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# - name: Login to registry
# uses: docker/login-action@v2
# with:
# registry: ${{ env.REGISTRY }}
# username: ${{ github.actor }}
# password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push publicly
uses: docker/build-push-action@v3
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
${{ steps.meta-slim-public.outputs.tags }}
labels: |
${{ steps.meta-slim-public.outputs.labels }}
org.opencontainers.image.licenses=AGPLv3
cache-from: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-arm:buildcache
cache-to: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-arm:buildcache,mode=max
# - name: Build and push publicly
# uses: depot/build-push-action@v1
# with:
# context: .
# push: true
# platforms: linux/amd64,linux/arm64
# tags: |
# ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
# ${{ steps.meta-slim-public.outputs.tags }}
# labels: |
# ${{ steps.meta-slim-public.outputs.labels }}
# org.opencontainers.image.licenses=AGPLv3

View File

@@ -9,22 +9,27 @@ on:
push:
branches: [main]
tags: ["*"]
pull_request:
types: [opened, synchronize, reopened]
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
id-token: write
packages: write
jobs:
build:
runs-on: [self-hosted, new]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
- uses: depot/setup-action@v1
- name: Login to registry
uses: docker/login-action@v2
@@ -33,36 +38,8 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta
id: meta-slim
if: github.event_name == 'pull_request'
uses: docker/metadata-action@v4
with:
images: |
${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-slim
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Build and push publicly
uses: docker/build-push-action@v3
if: github.event_name == 'pull_request'
with:
context: .
push: true
tags: |
${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-slim:latest
${{ steps.meta-slim.outputs.tags }}
labels: |
${{ steps.meta-slim.outputs.labels }}
cache-from: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-slim:buildcache
cache-to: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-slim:buildcache,mode=max
- name: Docker meta
id: meta-public
if: github.event_name != 'pull_request'
uses: docker/metadata-action@v4
with:
images: |
@@ -75,47 +52,31 @@ jobs:
- name: Build and push publicly
if: github.event_name != 'pull_request'
uses: docker/build-push-action@v3
uses: depot/build-push-action@v1
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-slim:latest
${{ steps.meta-public.outputs.tags }}
labels: |
${{ steps.meta-public.outputs.labels }}
org.opencontainers.image.licenses=AGPLv3
cache-from: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-slim:buildcache
cache-to: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-slim:buildcache,mode=max
build_ee:
runs-on: [self-hosted, new]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Docker meta
id: meta-slim
if: github.event_name == 'pull_request'
uses: docker/metadata-action@v4
with:
images: |
${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-slim
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
- uses: depot/setup-action@v1
- name: Docker meta
id: meta-ee-public
if: github.event_name != 'pull_request'
uses: docker/metadata-action@v4
with:
images: |
@@ -128,15 +89,13 @@ jobs:
- name: Login to registry
uses: docker/login-action@v2
if: github.event_name != 'pull_request'
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push publicly ee
if: github.event_name != 'pull_request'
uses: docker/build-push-action@v3
uses: depot/build-push-action@v1
with:
context: .
push: true
@@ -149,8 +108,7 @@ jobs:
labels: |
${{ steps.meta-ee-public.outputs.labels }}
org.opencontainers.image.licenses=Windmill-Enterprise-License
cache-from: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-ee:buildcache
cache-to: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-ee:buildcache,mode=max
playwright:
runs-on: [self-hosted, new]
@@ -172,14 +130,14 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: "Docker"
run: echo "::set-output name=id::$(docker run --network=host --rm -d -p 8000:8000 --privileged -it -e DATABASE_URL=postgres://admin:changeme@localhost:5432/windmill -e BASE_INTERNAL_URL=http://localhost:8000 ${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-slim:latest)"
run: echo "::set-output name=id::$(docker run --network=host --rm -d -p 8000:8000 --privileged -it -e DATABASE_URL=postgres://admin:changeme@localhost:5432/windmill -e BASE_INTERNAL_URL=http://localhost:8000 ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest)"
id: docker-container
- uses: actions/setup-node@v3
with:
node-version: 16
- name: "Playwright run"
timeout-minutes: 2
run: cd frontend && npm ci @playwright/test && npx playwright install && npm run test
run: cd frontend && npm ci @playwright/test && npx playwright install && export BASE_URL=http://localhost:8000 && npm run test
- name: "Clean up"
run: docker kill ${{ steps.docker-container.outputs.id }}
if: always()
@@ -188,7 +146,6 @@ jobs:
publish_privately_heavy:
needs: [build_ee]
runs-on: [self-hosted, new]
if: github.event_name != 'pull_request'
steps:
- uses: actions/checkout@v3
with:
@@ -202,7 +159,6 @@ jobs:
with:
images: |
${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}
${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr

17
.github/workflows/frontend-check.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: check frontend build
on:
pull_request:
types: [opened,synchronize,reopened,closed]
paths:
- "frontend/**"
jobs:
npm_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: "npm check"
timeout-minutes: 2
run: cd frontend && npm ci && npm run generate-backend-client && npm run check

View File

@@ -1,20 +0,0 @@
name: Pull Hub Items
on:
schedule:
# * is a special character in YAML so you have to quote this string
- cron: "0 0 */1 * *"
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
change_version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Pull hub
run: ./.github/pull_hub_items.sh
- name: Create Pull Request
uses: peter-evans/create-pull-request@v4
with:
title: sync hub items with community
commit-message: sync hub items with community

View File

@@ -74,6 +74,7 @@ jobs:
${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
${{ steps.metalocal.outputs.tags }}
${{ steps.meta.outputs.tags }}
registry.uffizzi.com/windmill-lsp:60d
labels: ${{ steps.metalocal.outputs.labels }}
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max

91
.github/workflows/uffizzi-build.yml vendored Normal file
View File

@@ -0,0 +1,91 @@
name: Build PR Image
on:
pull_request:
types: [opened,synchronize,reopened,closed]
paths:
- "backend/**"
workflow_dispatch:
jobs:
build-windmill:
name: Build and Push `windmill`
runs-on: ubuntu-latest
if: ${{ (github.event_name != 'pull_request' || github.event.action != 'closed')}}
outputs:
tags: ${{ steps.meta.outputs.tags }}
steps:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Generate UUID image name
id: uuid
run: echo "UUID_TAG_APP=$(uuidgen)" >> $GITHUB_ENV
- name: Docker metadata
id: meta
uses: docker/metadata-action@v3
with:
images: registry.uffizzi.com/${{ env.UUID_TAG_APP }}
tags: type=raw,value=60d
- name: Build and Push Image to registry.uffizzi.com ephemeral registry
uses: docker/build-push-action@v2
with:
push: true
context: ./
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
render-compose-file:
name: Render Docker Compose File
# Pass output of this workflow to another triggered by `workflow_run` event.
runs-on: ubuntu-latest
needs:
- build-windmill
outputs:
compose-file-cache-key: ${{ steps.hash.outputs.hash }}
steps:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Render Compose File
run: |
WINDMILL_IMAGE=${{ needs.build-windmill.outputs.tags }}
export WINDMILL_IMAGE
LSP_IMAGE=registry.uffizzi.com/windmill-lsp:60d
export LSP_IMAGE
envsubst '${WINDMILL_IMAGE} ${LSP_IMAGE}' < ./.github/uffizzi/docker-compose.uffizzi.yml > docker-compose.rendered.yml
cat docker-compose.rendered.yml
- name: Upload Rendered Compose File as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: docker-compose.rendered.yml
retention-days: 2
- name: Serialize PR Event to File
run: |
cat << EOF > event.json
${{ toJSON(github.event) }}
EOF
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: event.json
retention-days: 2
delete-preview:
name: Call for Preview Deletion
runs-on: ubuntu-latest
if: ${{ github.event.action == 'closed' }}
steps:
# If this PR is closing, we will not render a compose file nor pass it to the next workflow.
- name: Serialize PR Event to File
run: echo '${{ toJSON(github.event) }}' > event.json
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: event.json
retention-days: 2

102
.github/workflows/uffizzi-preview.yml vendored Normal file
View File

@@ -0,0 +1,102 @@
name: Deploy Uffizzi Preview
on:
workflow_run:
workflows:
- "Build PR Image"
types:
- completed
jobs:
cache-compose-file:
name: Cache Compose File
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
outputs:
compose-file-cache-key: ${{ env.COMPOSE_FILE_HASH }}
pr-number: ${{ env.PR_NUMBER }}
steps:
- name: 'Download artifacts'
# Fetch output (zip archive) from the workflow run that triggered this workflow.
uses: actions/github-script@v6
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "preview-spec"
})[0];
if (matchArtifact === undefined) {
throw TypeError('Build Artifact not found!');
}
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/preview-spec.zip`, Buffer.from(download.data));
- name: 'Unzip artifact'
run: unzip preview-spec.zip
- name: Read Event into ENV
run: |
echo 'EVENT_JSON<<EOF' >> $GITHUB_ENV
cat event.json >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Hash Rendered Compose File
id: hash
# If the previous workflow was triggered by a PR close event, we will not have a compose file artifact.
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
run: echo "COMPOSE_FILE_HASH=$(md5sum docker-compose.rendered.yml | awk '{ print $1 }')" >> $GITHUB_ENV
- name: Cache Rendered Compose File
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
uses: actions/cache@v3
with:
path: docker-compose.rendered.yml
key: ${{ env.COMPOSE_FILE_HASH }}
- name: Read PR Number From Event Object
id: pr
run: echo "PR_NUMBER=${{ fromJSON(env.EVENT_JSON).number }}" >> $GITHUB_ENV
- name: DEBUG - Print Job Outputs
if: ${{ runner.debug }}
run: |
echo "PR number: ${{ env.PR_NUMBER }}"
echo "Compose file hash: ${{ env.COMPOSE_FILE_HASH }}"
cat event.json
deploy-uffizzi-preview:
name: Use Remote Workflow to Preview on Uffizzi
needs:
- cache-compose-file
if: ${{ github.event.workflow_run.conclusion == 'success' }}
uses: UffizziCloud/preview-action/.github/workflows/reusable.yaml@v2
with:
# If this workflow was triggered by a PR close event, cache-key will be an empty string
# and this reusable workflow will delete the preview deployment.
compose-file-cache-key: ${{ needs.cache-compose-file.outputs.compose-file-cache-key }}
compose-file-cache-path: docker-compose.rendered.yml
server: https://app.uffizzi.com
pr-number: ${{ needs.cache-compose-file.outputs.pr-number }}
permissions:
contents: read
pull-requests: write
id-token: write
playwright:
runs-on: ubuntu-latest
needs:
- deploy-uffizzi-preview
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- name: "Playwright run"
timeout-minutes: 2
run: cd frontend && npm ci @playwright/test && npx playwright install && export BASE_URL=${{ needs.deploy-uffizzi-preview.outputs.url }} && npm run test

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ nohup.out
local/
frontend/src/routes/test.svelte
CaddyfileRemoteMalo
*.swp

View File

@@ -1,6 +1,162 @@
# Changelog
## [1.60.0](https://github.com/windmill-labs/windmill/compare/v1.59.0...v1.60.0) (2023-01-11)
### Features
* add 'add user to workspace' ([a14623f](https://github.com/windmill-labs/windmill/commit/a14623feaab4a36c01d558b775a42e587a74cdc9))
* **frontend:** Add frost to color palette ([#1084](https://github.com/windmill-labs/windmill/issues/1084)) ([8e72007](https://github.com/windmill-labs/windmill/commit/8e7200736827e8f6e593f900124b1bd1bc0bd5f2))
### Bug Fixes
* **frontend:** Keep pane resizer under open drawer ([#1089](https://github.com/windmill-labs/windmill/issues/1089)) ([cb25f88](https://github.com/windmill-labs/windmill/commit/cb25f883005b99b4ce98e8ae7b8253a8a2fedb5b))
## [1.59.0](https://github.com/windmill-labs/windmill/compare/v1.58.0...v1.59.0) (2023-01-09)
### Features
* add relative imports for python scripts ([#1075](https://github.com/windmill-labs/windmill/issues/1075)) ([5347cd4](https://github.com/windmill-labs/windmill/commit/5347cd46a996b4cf48a96fbb873e4d029ca4f75f))
### Bug Fixes
* **frontend:** Iconed resource height issue ([#1073](https://github.com/windmill-labs/windmill/issues/1073)) ([a84eb9b](https://github.com/windmill-labs/windmill/commit/a84eb9b1f7e1b10c960ee1594ef476e7ba146f5e))
## [1.58.0](https://github.com/windmill-labs/windmill/compare/v1.57.1...v1.58.0) (2023-01-07)
### Features
* add archive/unarchive/delete workspace ([6edf9b9](https://github.com/windmill-labs/windmill/commit/6edf9b9946d613b599cb91688c4986044caaba8d))
* add hub support for apps ([50453ca](https://github.com/windmill-labs/windmill/commit/50453ca690dfd936474ebbf000e36ae1006b188b))
* add min/max constraint to number + slider component ([0bcdcae](https://github.com/windmill-labs/windmill/commit/0bcdcaedcfdf7b7f76f703df3bf50d97dd389995))
* add support for yaml format as a string format ([5204e4a](https://github.com/windmill-labs/windmill/commit/5204e4a75d74e6bb4087dee7087390f7c0388e51))
* **frontend:** Add integration icons ([#1063](https://github.com/windmill-labs/windmill/issues/1063)) ([45acb89](https://github.com/windmill-labs/windmill/commit/45acb89f87ad78c48a1ba6abf1bd1424088b41c4))
* **frontend:** Toggle to hide optional inputs ([#1060](https://github.com/windmill-labs/windmill/issues/1060)) ([4d6a568](https://github.com/windmill-labs/windmill/commit/4d6a568820ceb6c064dc2871085b80412e18c379))
* **frontend:** Update app auto-refresh button ([#1062](https://github.com/windmill-labs/windmill/issues/1062)) ([34e3331](https://github.com/windmill-labs/windmill/commit/34e33319192f6d747d84fc6559853410f5d72ec8))
### Bug Fixes
* **frontend:** Remove popover hover styles ([#1064](https://github.com/windmill-labs/windmill/issues/1064)) ([76a860f](https://github.com/windmill-labs/windmill/commit/76a860fe538dadfc6691074384f92db1a331063d))
## [1.57.1](https://github.com/windmill-labs/windmill/compare/v1.57.0...v1.57.1) (2023-01-02)
### Bug Fixes
* preserver order changes for flows' schema ([2c8e98a](https://github.com/windmill-labs/windmill/commit/2c8e98a9c7fe3fdd48c851c0575fdb1d87c953a9))
* support setting undefined states ([ab0aeb0](https://github.com/windmill-labs/windmill/commit/ab0aeb0df825fb5afefbefae6739179dbbbc5f30))
## [1.57.0](https://github.com/windmill-labs/windmill/compare/v1.56.1...v1.57.0) (2023-01-01)
### Features
* add a All Static Inputs module to the flow editor ([3296deb](https://github.com/windmill-labs/windmill/commit/3296debfe7940fe833d489af0a4b6609c2d53411))
* apps can be published publicly ([be14aab](https://github.com/windmill-labs/windmill/commit/be14aab9b102ef81eccf689e00cd3cd8eae8f503))
* **app:** Update sidebar menu ([#1050](https://github.com/windmill-labs/windmill/issues/1050)) ([faa046a](https://github.com/windmill-labs/windmill/commit/faa046a3fdc326084df93f8e57dd5c573164b91d))
* **app:** Use consistent styles on settings pages ([#1048](https://github.com/windmill-labs/windmill/issues/1048)) ([681e2e8](https://github.com/windmill-labs/windmill/commit/681e2e824a39d9748f1aaa37f20001b5200f82bc))
* **backend:** resume from owner directly in flow status viewer ([#1042](https://github.com/windmill-labs/windmill/issues/1042)) ([40195d4](https://github.com/windmill-labs/windmill/commit/40195d42f661d401cd9ce11ca9739f87c1a27afd))
* **frontend:** Add customization props to radio ([#1056](https://github.com/windmill-labs/windmill/issues/1056)) ([0812f6e](https://github.com/windmill-labs/windmill/commit/0812f6efd8484e86a4f09631b28c71d17cd69627))
* **frontend:** Fix initial component dimensions + Select select + add spinner when buttons are clicked ([#1044](https://github.com/windmill-labs/windmill/issues/1044)) ([70e7a5d](https://github.com/windmill-labs/windmill/commit/70e7a5d07542e1ac936152e434146e056a80bad4))
* **frontend:** Properly support resource ([#1039](https://github.com/windmill-labs/windmill/issues/1039)) ([65f4e86](https://github.com/windmill-labs/windmill/commit/65f4e86a22838bd34373ce808c77a1770eeaf295))
* **frontend:** Update tooltip and home list dropdown ([#1053](https://github.com/windmill-labs/windmill/issues/1053)) ([9d30e5f](https://github.com/windmill-labs/windmill/commit/9d30e5fa57363c4cf715f845f5268856c4aa0fb3))
### Bug Fixes
* **app:** Fix inconsistencies in list items and sidebar menus ([#1051](https://github.com/windmill-labs/windmill/issues/1051)) ([0f1b19c](https://github.com/windmill-labs/windmill/commit/0f1b19c7d3eea4f8106fed3188460678e5035812))
* **frontend:** List item overflowing corners ([#1055](https://github.com/windmill-labs/windmill/issues/1055)) ([2fd730f](https://github.com/windmill-labs/windmill/commit/2fd730f8d2303b57f2da42354cd207dad2a410ce))
* **frontend:** Minor fixes in editor ([#1054](https://github.com/windmill-labs/windmill/issues/1054)) ([adc84f0](https://github.com/windmill-labs/windmill/commit/adc84f06d97275b17bf77cb6c8d264ad28b0f6ce))
* **frontend:** Static inputs overflow ([#1057](https://github.com/windmill-labs/windmill/issues/1057)) ([72aeba1](https://github.com/windmill-labs/windmill/commit/72aeba121cb694e8e96ad189b4acbfc2340bf520))
## [1.56.1](https://github.com/windmill-labs/windmill/compare/v1.56.0...v1.56.1) (2022-12-23)
### Bug Fixes
* **cli:** typo in cli deps ([0614ec4](https://github.com/windmill-labs/windmill/commit/0614ec42baf3e8f1675d62ca0f143b831c2700a1))
## [1.56.0](https://github.com/windmill-labs/windmill/compare/v1.55.0...v1.56.0) (2022-12-23)
### Features
* add move to drawer for script and flows ([f73dbd8](https://github.com/windmill-labs/windmill/commit/f73dbd8039b3c987ca94e5b56f0ecdea93cbd1b8))
* add operator mode ([3485b07](https://github.com/windmill-labs/windmill/commit/3485b07b2548b7ea8fbd2b6b31b91e2d36d072ef))
* auto-invite from same domain ([2bae50f](https://github.com/windmill-labs/windmill/commit/2bae50f3910a99a87efa402a9eef566320fe1f68))
* **backend:** add SUPERADMIN_SECRET as an env set superadmin ([c283112](https://github.com/windmill-labs/windmill/commit/c28311242d58af12a039b81a5e5c90688022ce8c))
* **frontend:** Add an input field to edit inline script name ([#1033](https://github.com/windmill-labs/windmill/issues/1033)) ([95a0b9c](https://github.com/windmill-labs/windmill/commit/95a0b9ceae73e291a0def340e935658b6c2ac3a5))
* **frontend:** Add app number input ([#1010](https://github.com/windmill-labs/windmill/issues/1010)) ([2fe927f](https://github.com/windmill-labs/windmill/commit/2fe927f7fdc1309c7bad8b90fb7e0cc41d364b3f))
* **frontend:** Add form component + fix connection bug ([#1012](https://github.com/windmill-labs/windmill/issues/1012)) ([424c31c](https://github.com/windmill-labs/windmill/commit/424c31c54a2652b89f9b06499a5aaf1cc0f00ad9))
* **frontend:** Add select component to app builder ([#1021](https://github.com/windmill-labs/windmill/issues/1021)) ([08071bb](https://github.com/windmill-labs/windmill/commit/08071bb66b4fc40e3b984ffb459e5d52d5816298))
* **frontend:** Add the ability to lock components so they don't move around ([#1035](https://github.com/windmill-labs/windmill/issues/1035)) ([26a6de2](https://github.com/windmill-labs/windmill/commit/26a6de247c3566bfa524b8fa4f8fc212ca557874))
* **frontend:** Align output panel UI ([#1025](https://github.com/windmill-labs/windmill/issues/1025)) ([0e871ca](https://github.com/windmill-labs/windmill/commit/0e871ca8432d4f0bc68543b4a3f3bf8f8af99669))
* **frontend:** App builder password and date input ([#1022](https://github.com/windmill-labs/windmill/issues/1022)) ([4651c9d](https://github.com/windmill-labs/windmill/commit/4651c9d8cd644e59bfd4f57be0bcecc01962a536))
* **frontend:** AppTable v2 + Inline script panel ([#1023](https://github.com/windmill-labs/windmill/issues/1023)) ([f6df3ae](https://github.com/windmill-labs/windmill/commit/f6df3ae36748a1271625c3f4b50ca66f604d79f7))
* **frontend:** Fix component synchro ([#1038](https://github.com/windmill-labs/windmill/issues/1038)) ([cebbc5f](https://github.com/windmill-labs/windmill/commit/cebbc5fbd1b8b855c9b1bcab535cff5b9de8d778))
* **frontend:** Fix inline script status ([#1034](https://github.com/windmill-labs/windmill/issues/1034)) ([be74311](https://github.com/windmill-labs/windmill/commit/be743117d155afb2a2f0fe33ff610e0f621409f7))
* **frontend:** Fix UI ([#1009](https://github.com/windmill-labs/windmill/issues/1009)) ([0ceb4ab](https://github.com/windmill-labs/windmill/commit/0ceb4ab1a893fecf9e64497612e6040d0e7bc8cd))
* **frontend:** Fork + Fix table ([#1037](https://github.com/windmill-labs/windmill/issues/1037)) ([ab13e8c](https://github.com/windmill-labs/windmill/commit/ab13e8cce44ded7e05a8dda3d4d4d1ac696bf739))
* **frontend:** Small UI fixes ([#1026](https://github.com/windmill-labs/windmill/issues/1026)) ([ebca9f3](https://github.com/windmill-labs/windmill/commit/ebca9f39eab27dda65d0ee5de175a90363bfebae))
* **frontend:** templatable editor with autocompletion ([e228c64](https://github.com/windmill-labs/windmill/commit/e228c6448ead4a7aef433f4abdfe3c466a0f50f4))
* implement usage tracker + quotas ([fd87109](https://github.com/windmill-labs/windmill/commit/fd871093f0ea4b2def351857d7d8d7e4e79f9539))
* introduce folders, deprecate items owned by groups ([4329d25](https://github.com/windmill-labs/windmill/commit/4329d259887da71eb2b2a67f73947b0fbe9f3941))
* introduce folders, deprecate items owned by groups ([c1b0b64](https://github.com/windmill-labs/windmill/commit/c1b0b64e1728007b364d2a0acc58fc459e49e461))
* Superadmins workspace ([#1003](https://github.com/windmill-labs/windmill/issues/1003)) ([4004de0](https://github.com/windmill-labs/windmill/commit/4004de06180868af4570668a2040bd711a461e0d))
### Bug Fixes
* **frontend:** copy-to-clipnoard url with protocol ([#1027](https://github.com/windmill-labs/windmill/issues/1027)) ([f77fe7b](https://github.com/windmill-labs/windmill/commit/f77fe7b6b321c3d00a51a42a4118fd37f7c9d782))
* **frontend:** Fix AppTable frontend search ([#1013](https://github.com/windmill-labs/windmill/issues/1013)) ([f7627b5](https://github.com/windmill-labs/windmill/commit/f7627b5f17a9f5a4528715eebb4d207f33609da2))
## [1.55.0](https://github.com/windmill-labs/windmill/compare/v1.54.0...v1.55.0) (2022-12-09)
### Features
* **frontend:** Add text input to app builder ([#1008](https://github.com/windmill-labs/windmill/issues/1008)) ([6198383](https://github.com/windmill-labs/windmill/commit/6198383138929237c1eb898954a1fd91bdded08a))
## [1.54.0](https://github.com/windmill-labs/windmill/compare/v1.53.0...v1.54.0) (2022-12-08)
### Features
* add lockable version to scripts inside flows ([#972](https://github.com/windmill-labs/windmill/issues/972)) ([799fa92](https://github.com/windmill-labs/windmill/commit/799fa925b39316f6f8232d01959c35c4d6fa9533))
* **frontend:** Add support for object editor + fix wording ([#1004](https://github.com/windmill-labs/windmill/issues/1004)) ([a562dee](https://github.com/windmill-labs/windmill/commit/a562dee3cebfc07f72f0e952cb102c4c86022937))
* implement flow as a flow step ([8c1c508](https://github.com/windmill-labs/windmill/commit/8c1c5083585f4882aac3f05f71ad1a6414772082))
## [1.53.0](https://github.com/windmill-labs/windmill/compare/v1.52.0...v1.53.0) (2022-12-05)
### Features
* add include_header to pass request headers to script ([31c317b](https://github.com/windmill-labs/windmill/commit/31c317b3581e24aa24fa41a708f080c1d1de7e0c))
* **cli:** hub sync ([#975](https://github.com/windmill-labs/windmill/issues/975)) ([2265372](https://github.com/windmill-labs/windmill/commit/22653727a4106fa604796b3958efab94762041c2))
* **frontend:** Add app preview ([#993](https://github.com/windmill-labs/windmill/issues/993)) ([c9ad638](https://github.com/windmill-labs/windmill/commit/c9ad63895891ab3bbaeab43a008573f5bd3681b5))
* **frontend:** clarified UX for connect step ([e4839e2](https://github.com/windmill-labs/windmill/commit/e4839e21ff5d60bec4499245742f2400168c70ad))
* **frontend:** introduce mysql as a script language ([#982](https://github.com/windmill-labs/windmill/issues/982)) ([e089109](https://github.com/windmill-labs/windmill/commit/e089109b50bd014c7a4f0fd7f60c53e8be63fb95))
* refactor favorite menu ([c55fae5](https://github.com/windmill-labs/windmill/commit/c55fae54dd043eb1c01a15c8005e29166a4e992b))
### Bug Fixes
* **cli:** Fix cli pull push ([#985](https://github.com/windmill-labs/windmill/issues/985)) ([1bac237](https://github.com/windmill-labs/windmill/commit/1bac23785cb6af255732b1a2551bf9ffa00e24e7))
* **frontend:** Align hub flow list + fix drawer content everywhere ([#991](https://github.com/windmill-labs/windmill/issues/991)) ([9f59a16](https://github.com/windmill-labs/windmill/commit/9f59a160c39048447ffeefc5070c52e8692c8316))
* **frontend:** Fix app InputValue sync ([#994](https://github.com/windmill-labs/windmill/issues/994)) ([e217fbf](https://github.com/windmill-labs/windmill/commit/e217fbf071fa834c4b4288f602125164bf1d93bf))
* **frontend:** fix app preview ([#979](https://github.com/windmill-labs/windmill/issues/979)) ([129a0ad](https://github.com/windmill-labs/windmill/commit/129a0ad56b58840620fdc77e619928e04c67cd1f))
* **frontend:** fix home ([#981](https://github.com/windmill-labs/windmill/issues/981)) ([fa64e83](https://github.com/windmill-labs/windmill/commit/fa64e83f7ea6bc7786a15db647319d2f2a322b5b))
* **frontend:** fix home header ([#977](https://github.com/windmill-labs/windmill/issues/977)) ([e9fa0ad](https://github.com/windmill-labs/windmill/commit/e9fa0ad0b75d0678167e7a48f8406639e85986a9))
* **frontend:** Fix home margins ([#992](https://github.com/windmill-labs/windmill/issues/992)) ([62d2a33](https://github.com/windmill-labs/windmill/commit/62d2a3343dc27317f33446918404373b7d8285f5))
* **frontend:** Make context clickable ([#984](https://github.com/windmill-labs/windmill/issues/984)) ([9264f4b](https://github.com/windmill-labs/windmill/commit/9264f4b233858537bb344355c5be43be3ec9d8d9))
* **frontend:** variables and resources uses tab navigation ([90ce431](https://github.com/windmill-labs/windmill/commit/90ce4314181d8e5031c08d5fbb75b920c33b7f75))
## [1.52.0](https://github.com/windmill-labs/windmill/compare/v1.51.0...v1.52.0) (2022-12-02)

View File

@@ -51,12 +51,16 @@ COPY frontend .
RUN mkdir /backend
COPY /backend/windmill-api/openapi.yaml /backend/windmill-api/openapi.yaml
COPY /openflow.openapi.yaml /openflow.openapi.yaml
COPY /backend/windmill-api/build_openapi.sh /backend/windmill-api/build_openapi.sh
RUN cd /backend/windmill-api && . ./build_openapi.sh
RUN npm run generate-backend-client
ENV NODE_OPTIONS "--max-old-space-size=8192"
RUN npm run build
RUN npm run check
FROM rust_base AS planner
COPY ./openflow.openapi.yaml /openflow.openapi.yaml
@@ -75,12 +79,13 @@ COPY ./openflow.openapi.yaml /openflow.openapi.yaml
COPY ./backend ./
COPY --from=frontend /frontend /frontend
COPY --from=frontend /backend/windmill-api/openapi-deref.yaml ./windmill-api/openapi-deref.yaml
COPY .git/ .git/
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release --features "$features"
FROM python:3.11.0-slim-buster
FROM python:3.11.1-slim-buster
ARG APP=/usr/src/app

View File

@@ -66,6 +66,7 @@ Windmill is <b>fully open-sourced (AGPLv3)</b>:
- [Technical Architecture](#technical-architecture)
- [How to self-host](#how-to-self-host)
- [Docker compose](#docker-compose)
- [Kubernetes (k8s) and Helm charts](#kubernetes-k8s-and-helm-charts)
- [Commercial license](#commercial-license)
- [OAuth for self-hosting (very optional)](#oauth-for-self-hosting-very-optional)
- [Resource types](#resource-types)
@@ -90,7 +91,7 @@ Windmill is <b>fully open-sourced (AGPLv3)</b>:
shared on [WindmillHub](https://hub.windmill.dev).
![Step 4](./imgs/windmill-flow.png)
4. (Coming soon) Build complex UI on top of your scripts and flows.
4. Build complex UI on top of your scripts and flows.
![Step 5](./imgs/windmill-builder.png)
Scripts and flows can also be triggered by a cron schedule '*/5 * * * *' or
@@ -192,6 +193,10 @@ The default super-admin user is: admin@windmill.dev / changeme
From there, you can create other users (do not forget to change the password!)
### Kubernetes (k8s) and Helm charts
We publish helm charts at: <https://github.com/windmill-labs/windmill-helm-charts>
### Commercial license
To self-host Windmill, you must respect the terms of the AGPLv3 license which
@@ -226,6 +231,8 @@ the following format:
and mount it at `/usr/src/app/oauth.json`.
The redirect url for the oauth clients is: `<instance_url>/user/login_callback/<client>`
[The list of all possible "connect an app" oauth clients](https://github.com/windmill-labs/windmill/blob/main/backend/oauth_connect.json)
To add more "connect an app" OAuth clients to the Windmill project, read the
@@ -278,6 +285,7 @@ upcoming CLI tool.
| BASE_INTERNAL_URL | http://localhost:8000 | The base url that is reachable by your workers to talk to the Servers. This help avoiding going through the external load balancer for VPC-internal requests. | Worker |
| TIMEOUT | 300 | The timeout in seconds for the execution of a script | Worker |
| SLEEP_QUEUE | 50 | The number of ms to sleep in between the last check for new jobs in the DB. It is multiplied by NUM_WORKERS such that in average, for one worker instance, there is one pull every SLEEP_QUEUE ms. | Worker |
| MAX_LOG_SIZE | 500000 | The maximum number of characters a job can emit (log + result) | Worker |
| DISABLE_NUSER | false | If Nsjail is enabled, disable the nsjail's `clone_newuser` setting | Worker |
| KEEP_JOB_DIR | false | Keep the job directory after the job is done. Useful for debugging. | Worker |
| LICENSE_KEY (EE only) | None | License key checked at startup for the Enterprise Edition of Windmill | Worker |
@@ -294,6 +302,16 @@ upcoming CLI tool.
| PIP_TRUSTED_HOST | None | The trusted host to pass to pip. | Worker |
| PATH | None | The path environment variable, usually inherited | Worker |
| HOME | None | The home directory to use for Go and Bash , usually inherited | Worker |
| DATABASE_CONNECTIONS | 50 (Server)/3 (Worker) | The max number of connections in the database connection pool | All |
| SUPERADMIN_SECRET | None | A token that would let the caller act as a virtual superadmin superadmin@windmill.dev | Server |
| TIMEOUT_WAIT_RESULT | 20 | The number of seconds to wait before timeout on the 'run_wait_result' endpoint | Worker |
| QUEUE_LIMIT_WAIT_RESULT | None | The number of max jobs in the queue before rejecting immediately the request in 'run_wait_result' endpoint. Takes precedence on the query arg. If none is specified, there are no limit. | Worker |
| DENO_AUTH_TOKENS | None | Custom DENO_AUTH_TOKENS to pass to worker to allow the use of private modules | Worker |
| DENO_FLAGS | None | Override the flags passed to deno (default --allow-all) to tighten permissions. Minimum permissions needed are "--allow-read=args.json --allow-write=result.json" | Worker |
| |
## Run a local dev setup

1
backend/.gitignore vendored
View File

@@ -1,3 +1,4 @@
target/
.env
oauth.json
windmill-api/openapi-deref.yaml

3
backend/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"python.analysis.typeCheckingMode": "basic"
}

1130
backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.52.0"
version = "1.60.0"
authors.workspace = true
edition.workspace = true
@@ -19,7 +19,7 @@ members = [
]
[workspace.package]
version = "1.52.0"
version = "1.60.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -28,7 +28,7 @@ name = "windmill"
path = "./src/main.rs"
[features]
enterprise = ["windmill-worker/enterprise"]
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise"]
[dependencies]
anyhow.workspace = true
@@ -44,6 +44,10 @@ sqlx.workspace = true
tokio-metrics.workspace = true
rand.workspace = true
chrono.workspace = true
git-version.workspace = true
rsa.workspace = true
base64.workspace = true
sha2.workspace = true
[dev-dependencies]
serde_json.workspace = true
@@ -138,3 +142,6 @@ tokio-metrics = "0.1.0"
lazy_static = "1.4.0"
serde_derive = "1.0.147"
const_format = { version = "0.2", features = ["rust_1_64", "rust_1_51"] }
dyn-iter = "0.2.0"
rsa = "0.7.2"
async-stripe = { version = "0.14", features = ["runtime-tokio-hyper", "checkout"] }

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,3 @@
-- Add up migration script here
ALTER TABLE app ADD CONSTRAINT unique_path_workspace_id UNIQUE (workspace_id, path);
ALTER TABLE app ENABLE ROW LEVEL SECURITY;

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,9 @@
INSERT INTO workspace(id, name, owner) VALUES
('admins', 'Admins', 'admin@windmill.dev');
INSERT INTO workspace_settings (workspace_id) VALUES
('admins');
INSERT INTO workspace_key
(workspace_id, kind, key)
VALUES ('admins', 'cloud', md5(random()::text) || md5(random()::text))

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,20 @@
INSERT INTO script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'admins',
'system',
'import wmill from "https://deno.land/x/wmill@v1.55.0/main.ts";
export async function main() {
await run(
"workspace", "add", "__automation", "starter", Deno.env.get("WM_BASE_URL") + "/", "--token", Deno.env.get("WM_TOKEN"));
await run("hub", "pull");
}
async function run(...cmd: string[]) {
console.log("Running \"" + cmd.join('' '') + "\"");
await wmill.parse(cmd);
}',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'Syncronize Hub Resource types with starter workspace',
'Basic administrative script to sync latest resource types from hub. Recommended to run at least once. On a schedule by default.',
'u/admin/hub_sync', -28028598712388162, 'deno', '');

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TABLE queue ADD COLUMN pre_run_error text;

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TABLE variable ALTER COLUMN value TYPE VARCHAR(15000);

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,8 @@
-- Add up migration script here
CREATE TABLE usage (
id VARCHAR(50) NOT NULL,
is_workspace BOOLEAN NOT NULL,
month_ INTEGER NOT NULL,
usage INTEGER NOT NULL,
PRIMARY KEY (id, is_workspace, month_)
);

View File

@@ -0,0 +1,5 @@
-- Add down migration script here
ALTER TABLE queue DROP COLUMN email;
ALTER TABLE workspace_settings DROP COLUMN slack_email;
ALTER TABLE schedule DROP COLUMN email;
ALTER TABLE schedule DROP COLUMN error;

View File

@@ -0,0 +1,6 @@
-- Add up migration script here
ALTER TABLE queue ADD COLUMN email VARCHAR(50) NOT NULL DEFAULT 'missing@email.xyz';
ALTER TABLE workspace_settings ADD COLUMN slack_email VARCHAR(50) NOT NULL DEFAULT 'missing@email.xyz';;
ALTER TABLE schedule ADD COLUMN email VARCHAR(50) NOT NULL DEFAULT 'missing@email.xyz';
ALTER TABLE schedule ADD COLUMN error TEXT;

View File

@@ -0,0 +1,2 @@
-- Add down migration script here
ALTER TABLE workspace_settings DROP COLUMN auto_invite_domain;

View File

@@ -0,0 +1,4 @@
-- Add up migration script here
GRANT ALL PRIVILEGES ON TABLE usage TO windmill_admin;
GRANT ALL PRIVILEGES ON TABLE usage TO windmill_user;

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,2 @@
-- Add up migration script here
DELETE FROM usr WHERE workspace_id = 'starter' AND email = 'admin@windmill.dev';

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,2 @@
-- Add up migration script here
INSERT INTO group_ SELECT id, 'all', 'The group that always contains all users of this workspace' FROM workspace ON CONFLICT DO NOTHING;

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,3 @@
-- Add up migration script here
ALTER TABLE workspace_settings ADD COLUMN auto_invite_domain VARCHAR(50);
ALTER TABLE workspace DROP COLUMN domain;

View File

@@ -0,0 +1,3 @@
-- Add down migration script here
ALTER TABLE workspace_invite DROP COLUMN operator;
ALTER TABLE workspace_settings DROP COLUMN auto_invite_operator;

View File

@@ -0,0 +1,4 @@
-- Add up migration script here
ALTER TABLE workspace_invite ADD COLUMN operator BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE workspace_settings ADD COLUMN auto_invite_operator BOOLEAN DEFAULT false;
ALTER TABLE completed_job ADD COLUMN email VARCHAR(50) NOT NULL DEFAULT 'missing@email.xyz';

View File

@@ -0,0 +1,5 @@
-- Add down migration script here
ALTER TABLE queue DROP COLUMN visible_to_owner;
ALTER TABLE completed_job DROP COLUMN visible_to_owner;
DROP POLICY see_own_path ON queue;

View File

@@ -0,0 +1,15 @@
-- Add up migration script here
ALTER TABLE queue ADD COLUMN visible_to_owner BOOLEAN DEFAULT true;
ALTER TABLE completed_job ADD COLUMN visible_to_owner BOOLEAN DEFAULT true;
CREATE POLICY see_own_path ON queue FOR ALL
USING (queue.visible_to_owner IS true AND SPLIT_PART(queue.script_path, '/', 1) = 'u' AND SPLIT_PART(queue.script_path, '/', 2) = current_setting('session.user'));
CREATE POLICY see_member_path ON queue FOR ALL
USING (queue.visible_to_owner IS true AND SPLIT_PART(queue.script_path, '/', 1) = 'g' AND SPLIT_PART(queue.script_path, '/', 2) = any(regexp_split_to_array(current_setting('session.groups'), ',')::text[]));
CREATE POLICY see_own_path ON completed_job FOR ALL
USING (completed_job.visible_to_owner IS true AND SPLIT_PART(completed_job.script_path, '/', 1) = 'u' AND SPLIT_PART(completed_job.script_path, '/', 2) = current_setting('session.user'));
CREATE POLICY see_member_path ON completed_job FOR ALL
USING (completed_job.visible_to_owner IS true AND SPLIT_PART(completed_job.script_path, '/', 1) = 'g' AND SPLIT_PART(completed_job.script_path, '/', 2) = any(regexp_split_to_array(current_setting('session.groups'), ',')::text[]));

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,19 @@
-- Add up migration script here
ALTER TABLE app_version
RENAME COLUMN flow_id TO app_id;
CREATE TABLE folder (
name VARCHAR(255),
workspace_id VARCHAR(50) NOT NULL,
display_name VARCHAR(100) NOT NULL,
owners VARCHAR(255)[] NOT NULL,
extra_perms JSONB NOT NULL DEFAULT '{}',
FOREIGN KEY (workspace_id) REFERENCES workspace(id) ON DELETE CASCADE,
PRIMARY KEY (workspace_id, name)
);
CREATE INDEX folder_extra_perms ON folder USING GIN (extra_perms);
CREATE INDEX folder_owners ON folder USING GIN (owners);
GRANT ALL ON folder TO windmill_user;
GRANT ALL ON folder TO windmill_admin;

View File

@@ -0,0 +1,21 @@
-- Add down migration script here
DO
$do$
DECLARE
i text;
arr text[] := array['resource', 'script', 'variable', 'schedule', 'flow', 'app'];
BEGIN
FOREACH i IN ARRAY arr
LOOP
EXECUTE FORMAT(
$$
DROP POLICY see_folder_extra_perms_user ON %1$I;
$$,
i
);
END LOOP;
END
$do$;
DROP POLICY see_extra_perms_user ON folder;
DROP POLICY see_extra_perms_groups ON folder;

View File

@@ -0,0 +1,52 @@
-- Add up migration script here
DO
$do$
DECLARE
i text;
arr text[] := array['resource', 'script', 'variable', 'schedule', 'flow', 'app'];
BEGIN
FOREACH i IN ARRAY arr
LOOP
EXECUTE FORMAT(
$$
CREATE POLICY see_folder_extra_perms_user ON %1$I FOR ALL
USING (SPLIT_PART(%1$I.path, '/', 1) = 'f' AND SPLIT_PART(%1$I.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_read'), ',')::text[]))
WITH CHECK (SPLIT_PART(%1$I.path, '/', 1) = 'f' AND SPLIT_PART(%1$I.path, '/', 2) = any(regexp_split_to_array(current_setting('session.folders_write'), ',')::text[]));
$$,
i
);
END LOOP;
END
$do$;
ALTER TABLE folder ENABLE ROW LEVEL SECURITY;
CREATE POLICY see_extra_perms_user ON folder FOR ALL
USING (extra_perms ? CONCAT('u/', current_setting('session.user')) or (CONCAT('u/', current_setting('session.user')) = ANY(owners)))
WITH CHECK ((CONCAT('u/', current_setting('session.user')) = ANY(owners)));
CREATE POLICY see_extra_perms_groups ON folder FOR ALL
USING (extra_perms ?| regexp_split_to_array(current_setting('session.pgroups'), ',')::text[] or (exists(
SELECT o FROM unnest(owners) as o
WHERE o = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[]))))
WITH CHECK (exists(
SELECT o FROM unnest(owners) as o
WHERE o = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])));
DO
$do$
DECLARE
i text;
arr text[] := array['script', 'flow', 'variable', 'resource', 'schedule'];
BEGIN
FOREACH i IN ARRAY arr
LOOP
EXECUTE FORMAT(
$$
ALTER TABLE %1$I DROP CONSTRAINT proper_id;
ALTER TABLE %1$I ADD CONSTRAINT proper_id CHECK (path ~ '^[ufg](\/[\w-]+){2,}$');
$$,
i
);
END LOOP;
END
$do$;

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,15 @@
-- Add up migration script here
UPDATE script SET content = 'import wmill from "https://deno.land/x/wmill@v1.57.0/main.ts";
export async function main() {
await run(
"workspace", "add", "__automation", "admins", Deno.env.get("WM_BASE_URL") + "/", "--token", Deno.env.get("WM_TOKEN"));
await run("hub", "pull");
}
async function run(...cmd: string[]) {
console.log("Running \"" + cmd.join('' '') + "\"");
await wmill.parse(cmd);
}', summary = 'Synchronize Hub Resource types with admins workspace',
description = 'Basic administrative script to sync latest resource types from hub to share to every workspace. Recommended to run at least once. On a schedule by default.'
WHERE hash = -28028598712388162 AND workspace_id = 'admins';

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,16 @@
-- Add up migration script here
-- Add up migration script here
UPDATE script SET content = 'import wmill from "https://deno.land/x/wmill@v1.57.0/main.ts";
export async function main() {
await run(
"workspace", "add", "__automation", "admins", Deno.env.get("BASE_INTERNAL_URL") + "/", "--token", Deno.env.get("WM_TOKEN"));
await run("hub", "pull");
}
async function run(...cmd: string[]) {
console.log("Running \"" + cmd.join('' '') + "\"");
await wmill.parse(cmd);
}', summary = 'Synchronize Hub Resource types with admins workspace',
description = 'Basic administrative script to sync latest resource types from hub to share to every workspace. Recommended to run at least once. On a schedule by default.'
WHERE hash = -28028598712388162 AND workspace_id = 'admins';

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,3 @@
-- Add up migration script here
ALTER TABLE workspace_settings ADD COLUMN customer_id VARCHAR(100);
ALTER TABLE workspace_settings ADD COLUMN plan VARCHAR(40);

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,5 @@
-- Add up migration script here
ALTER TABLE
workspace_settings
ADD
COLUMN webhook text;

View File

@@ -71,5 +71,11 @@
"extra_params": {
"type": "web_server"
}
},
"linkedin": {
"auth_url": "https://www.linkedin.com/oauth/v2/authorization",
"token_url": "https://www.linkedin.com/oauth/v2/accessToken",
"scopes": ["w_member_social", "r_liteprofile", "r_emailaddress"],
"req_body_auth": true
}
}

View File

@@ -16,5 +16,11 @@
"token_url": "https://oauth2.googleapis.com/token",
"userinfo_url": "https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
"scopes": ["https://www.googleapis.com/auth/userinfo.email"]
},
"microsoft": {
"auth_url": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
"token_url": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
"userinfo_url": "https://graph.microsoft.com/oidc/userinfo",
"scopes": ["openid", "profile", "email"]
}
}

View File

@@ -104,6 +104,7 @@ pub fn parse_python_signature(code: &str) -> error::Result<MainArgSignature> {
"bytes" => Typ::Bytes,
"datetime" => Typ::Datetime,
"datetime.datetime" => Typ::Datetime,
"Sql" | "sql" => Typ::Sql,
_ => Typ::Resource(id),
},
_ => Typ::Unknown,
@@ -174,8 +175,13 @@ fn constant_to_value(c: &Constant) -> serde_json::Value {
static PYTHON_IMPORTS_REPLACEMENT: phf::Map<&'static str, &'static str> = phf_map! {
"psycopg2" => "psycopg2-binary",
"psycopg" => "psycopg[binary, pool]",
"yaml" => "pyyaml",
"git" => "GitPython"
"git" => "GitPython",
"u" => "requests",
"f" => "requests",
"shopify" => "ShopifyAPI",
"seleniumwire" => "selenium-wire",
};
fn replace_import(x: String) -> String {

File diff suppressed because it is too large Load Diff

24
backend/src/ee.rs Normal file
View File

@@ -0,0 +1,24 @@
#[cfg(feature = "enterprise")]
use rsa::{pkcs8::DecodePublicKey, signature::Verifier};
#[cfg(feature = "enterprise")]
use sha2::Sha256;
#[cfg(feature = "enterprise")]
pub fn verify_license_key(license_key: Option<String>) -> anyhow::Result<()> {
if let Some(license_key) = license_key {
let splitted_lk = license_key
.split_once(".")
.expect("license_key can be splitted with a .");
let pub_key = rsa::RsaPublicKey::from_public_key_der(
&base64::decode("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgVShzcLSPiOi+8ET8fggob1kmi47/cE12JaidPkwfGnScZItghkqtiLsct0U4kJhlp5gO89DYTBmIKadvxwY7kMsLlZzmi2emVH7c27cByGASY8QmWDNdG4Ggy/NDflGGBdAtN6gHawZAg4zHv3qpbPQGHH1/6sXIohcXhOnouwIDAQAB")?)?;
let msg = base64::decode(splitted_lk.0)?;
let signature = base64::decode(splitted_lk.1)?;
rsa::pss::VerifyingKey::<Sha256>::new(pub_key)
.verify(&msg, &rsa::pss::Signature::from(signature))
.map_err(|_| anyhow::anyhow!("Invalid license key".to_string()))?;
} else {
panic!("License key is required for the enterprise edition");
}
Ok(())
}

View File

@@ -8,18 +8,21 @@
use std::net::SocketAddr;
use git_version::git_version;
use sqlx::{Pool, Postgres};
use windmill_common::utils::rd_string;
use windmill_worker::WorkerConfig;
const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
mod ee;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
dotenv::dotenv().ok();
windmill_common::tracing_init::initialize_tracing();
let db = windmill_common::connect_db().await?;
let num_workers = std::env::var("NUM_WORKERS")
.ok()
.and_then(|x| x.parse::<i32>().ok())
@@ -40,6 +43,8 @@ async fn main() -> anyhow::Result<()> {
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(false);
let db = windmill_common::connect_db(server_mode).await?;
if server_mode {
windmill_api::migrate_db(&db).await?;
}
@@ -47,11 +52,10 @@ async fn main() -> anyhow::Result<()> {
let (tx, rx) = tokio::sync::broadcast::channel::<()>(3);
let shutdown_signal = windmill_common::shutdown_signal(tx);
let base_internal_url =
std::env::var("BASE_INTERNAL_URL").unwrap_or_else(|_| "http://localhost:8000".to_string());
let base_url = std::env::var("BASE_URL").unwrap_or_else(|_| "http://localhost".to_string());
let base_internal_url =
std::env::var("BASE_INTERNAL_URL").unwrap_or_else(|_| "http://localhost:8000".to_string());
let timeout = std::env::var("TIMEOUT")
.ok()
.and_then(|x| x.parse::<i32>().ok())
@@ -93,11 +97,28 @@ async fn main() -> anyhow::Result<()> {
.map(|e| Some(e))
.unwrap_or(None);
#[cfg(feature = "enterprise")]
tracing::info!(
"
##############################
Windmill Enterprise Edition {GIT_VERSION} LICENSE_KEY: {license_key:?}, S3_CACHE_BUCKET: {sync_bucket:?}
##############################"
);
#[cfg(not(feature = "enterprise"))]
tracing::info!(
"
##############################
Windmill Community Edition {GIT_VERSION}
##############################"
);
tracing::info!(
"DISABLE_NSJAIL: {disable_nsjail}, DISABLE_NUSER: {disable_nuser}, BASE_URL: \
{base_url}, SLEEP_QUEUE: {sleep_queue}, NUM_WORKERS: {num_workers}, TIMEOUT: \
{timeout}, KEEP_JOB_DIR: {keep_job_dir}"
);
run_workers(
db.clone(),
addr,
@@ -171,19 +192,12 @@ pub async fn run_workers(
license_key: Option<String>,
) -> anyhow::Result<()> {
#[cfg(feature = "enterprise")]
if let Some(license_key) = license_key {
if license_key != "REQUIRED_DEC1" {
panic!("Invalid license key");
}
} else {
panic!("License key is required for the enterprise edition");
}
ee::verify_license_key(license_key)?;
#[cfg(not(feature = "enterprise"))]
if license_key.is_some() {
panic!("License key is required ONLY for the enterprise edition");
}
#[cfg(not(feature = "enterprise"))]
if !worker_config.disable_nsjail {
tracing::warn!(

View File

@@ -2,8 +2,8 @@
-- https://docs.rs/sqlx/latest/sqlx/attr.test.html
INSERT INTO workspace
(id, name, owner, domain)
VALUES ('test-workspace', 'test-workspace', 'test-user', null);
(id, name, owner)
VALUES ('test-workspace', 'test-workspace', 'test-user');
INSERT INTO usr(workspace_id, email, username, is_admin, role) VALUES
('test-workspace', 'test@windmill.dev', 'test-user', true, 'Admin');

View File

@@ -186,7 +186,7 @@ mod suspend_resume {
const secret = await r.text();\
console.log('Secret: ' + secret + ' ' + job + ' ' + token);\
const r2 = await fetch(
`http://localhost:${port}/api/w/test-workspace/jobs/${op}/${job}/0/${secret}?approver=ruben`,\
`http://localhost:${port}/api/w/test-workspace/jobs_u/${op}/${job}/0/${secret}?approver=ruben`,\
{\
method: 'POST',\
body: JSON.stringify('from job'),\
@@ -276,7 +276,7 @@ mod suspend_resume {
/* ImZyb20gdGVzdCIK = base64 "from test" */
reqwest::get(format!(
"http://localhost:{port}/api/w/test-workspace/jobs/resume/{second}/0/{secret}?payload=ImZyb20gdGVzdCIK&approver=ruben"
"http://localhost:{port}/api/w/test-workspace/jobs_u/resume/{second}/0/{secret}?payload=ImZyb20gdGVzdCIK&approver=ruben"
))
.await
.unwrap()
@@ -333,7 +333,7 @@ mod suspend_resume {
server.close().await.unwrap();
assert_eq!(
json!({"error": "Job canceled: approval request disapproved by ruben" }),
json!({"error": {"name": "Canceled", "reason": "approval request disapproved", "message": "Job canceled: approval request disapproved by ruben", "canceler": "ruben"}}),
result
);
}
@@ -379,7 +379,7 @@ mod suspend_resume {
/* ImZyb20gdGVzdCIK = base64 "from test" */
reqwest::get(format!(
"http://localhost:{port}/api/w/test-workspace/jobs/cancel/{second}/0/{secret}?payload=ImZyb20gdGVzdCIK"
"http://localhost:{port}/api/w/test-workspace/jobs_u/cancel/{second}/0/{secret}?payload=ImZyb20gdGVzdCIK"
))
.await
.unwrap()
@@ -395,7 +395,7 @@ mod suspend_resume {
let result = completed_job(flow, &db).await.result.unwrap();
assert_eq!(
json!({"error": "Job canceled: approval request disapproved by unknown" }),
json!( {"error": {"name": "Canceled", "reason": "approval request disapproved", "message": "Job canceled: approval request disapproved by unknown", "canceler": "unknown"}}),
result
);
}
@@ -462,7 +462,7 @@ export async function main(index, port) {
const buf = new Uint8Array([0]);
const sock = await Deno.connect({ port });
await sock.write(new Uint8Array([index]));
if (await sock.read(buf) != 1) throw "read";
if (await sock.read(buf) != 1) throw Error("read");
return buf[0];
}
"#
@@ -581,10 +581,17 @@ def main(last, port):
.unwrap();
assert_eq!(server.close().await, attempts);
assert!(result[1]["error"]
.as_str()
.unwrap()
.contains(r#"Uncaught (in promise) "read""#));
assert!(
result[1]["error"]
.as_object()
.unwrap()
.get("message")
.unwrap()
.as_str()
.unwrap()
== "read"
);
}
#[sqlx::test(fixtures("base"))]
@@ -615,6 +622,10 @@ def main(last, port):
let result = job.result.unwrap();
assert_eq!(server.close().await, attempts);
assert!(result["error"]
.as_object()
.unwrap()
.get("message")
.unwrap()
.as_str()
.unwrap()
.contains("index out of range"));
@@ -684,9 +695,7 @@ def main(error, port):
result,
json!({
"recv": 42,
"from failure module": {
"error": "Error during execution of the script:\n\nTraceback (most recent call last):\n File \"/tmp/main.py\", line 14, in <module>\n res = inner_script.main(**kwargs)\n File \"/tmp/inner.py\", line 5, in main\n return sock.recv(1)[0]\nIndexError: index out of range",
}
"from failure module": {"error": {"name": "IndexError", "stack": " File \"/tmp/inner.py\", line 5, in main\n return sock.recv(1)[0]\n", "message": "index out of range"}},
})
);
}
@@ -739,9 +748,13 @@ async fn test_iteration(db: Pool<Postgres>) {
.unwrap();
assert!(matches!(result, serde_json::Value::Array(_)));
assert!(result[2]["error"]
.as_object()
.unwrap()
.get("message")
.unwrap()
.as_str()
.unwrap()
.contains("StopIteration: 2"));
.contains("2"));
}
#[sqlx::test(fixtures("base"))]
@@ -792,9 +805,13 @@ async fn test_iteration_parallel(db: Pool<Postgres>) {
let result = job.result.unwrap();
assert!(matches!(result, serde_json::Value::Array(_)));
assert!(result[2]["error"]
.as_object()
.unwrap()
.get("message")
.unwrap()
.as_str()
.unwrap()
.contains("StopIteration: 2"));
.contains("2"));
}
struct RunJob {
@@ -821,14 +838,17 @@ impl RunJob {
tx,
"test-workspace",
payload,
Some(args),
args,
/* user */ "test-user",
/* email */ "test@windmill.dev",
/* permissioned_as */ "u/admin".to_string(),
/* scheduled_for_o */ None,
/* schedule_path */ None,
/* parent_job */ None,
/* is_flow_step */ false,
/* running */ false,
None,
true,
)
.await
.expect("push has to succeed");
@@ -1764,7 +1784,7 @@ async fn test_invalid_first_step(db: Pool<Postgres>) {
assert_eq!(
job.result.unwrap(),
serde_json::json!({"error":"Expected an array value, found: {}"})
serde_json::json!( {"error": {"name": "InternalErr", "message": "Expected an array value, found: {}"}})
);
}
@@ -2080,7 +2100,7 @@ async fn test_branchall_skip_failure(db: Pool<Postgres>) {
assert_eq!(
result,
serde_json::json!([{"error": "Error during execution of the script:\n\nerror: Uncaught (in promise) Error: failure\nexport function main(){ throw Error('failure') }\n ^\n at main (file:///tmp/inner.ts:1:31)\n at run (file:///tmp/main.ts:9:26)\n at file:///tmp/main.ts:14:1"}, [1,3]])
serde_json::json!([{"error": {"name": "Error", "stack": "Error: failure\n at main (file:///tmp/inner.ts:1:31)\n at run (file:///tmp/main.ts:9:26)\n at file:///tmp/main.ts:14:1", "message": "failure"}}, [1,3]])
);
let flow: FlowValue = serde_json::from_value(json!({
@@ -2114,7 +2134,7 @@ async fn test_branchall_skip_failure(db: Pool<Postgres>) {
assert_eq!(
result,
serde_json::json!([{"error": "Error during execution of the script:\n\nerror: Uncaught (in promise) Error: failure\nexport function main(){ throw Error('failure') }\n ^\n at main (file:///tmp/inner.ts:1:31)\n at run (file:///tmp/main.ts:9:26)\n at file:///tmp/main.ts:14:1"}, [1, 2]])
serde_json::json!([ {"error": {"name": "Error", "stack": "Error: failure\n at main (file:///tmp/inner.ts:1:31)\n at run (file:///tmp/main.ts:9:26)\n at file:///tmp/main.ts:14:1", "message": "failure"}}, [1, 2]])
);
}
@@ -2253,7 +2273,7 @@ async fn test_failure_module(db: Pool<Postgres>) {
},
"type": "rawscript",
"language": "deno",
"content": "export function main(n, l) { if (n == 0) throw l; return { l: [...l, 0] } }",
"content": "export function main(n, l) { if (n == 0) throw Error(JSON.stringify(l)); return { l: [...l, 0] } }",
},
}, {
"id": "b",
@@ -2264,7 +2284,7 @@ async fn test_failure_module(db: Pool<Postgres>) {
},
"type": "rawscript",
"language": "deno",
"content": "export function main(n, l) { if (n == 1) throw l; return { l: [...l, 1] } }",
"content": "export function main(n, l) { if (n == 1) throw Error(JSON.stringify(l)); return { l: [...l, 1] } }",
},
}, {
"value": {
@@ -2274,7 +2294,7 @@ async fn test_failure_module(db: Pool<Postgres>) {
},
"type": "rawscript",
"language": "deno",
"content": "export function main(n, l) { if (n == 2) throw l; return { l: [...l, 2] } }",
"content": "export function main(n, l) { if (n == 2) throw Error(JSON.stringify(l)); return { l: [...l, 2] } }",
},
}],
"failure_module": {
@@ -2294,10 +2314,15 @@ async fn test_failure_module(db: Pool<Postgres>) {
.await
.result
.unwrap();
assert!(result["from failure module"]["error"]
.as_object()
.unwrap()
.get("message")
.unwrap()
.as_str()
.unwrap()
.contains("Uncaught (in promise) []"));
.contains("[]"));
let result = RunJob::from(JobPayload::RawFlow { value: flow.clone(), path: None })
.arg("n", json!(1))
@@ -2305,10 +2330,15 @@ async fn test_failure_module(db: Pool<Postgres>) {
.await
.result
.unwrap();
assert!(result["from failure module"]["error"]
.as_object()
.unwrap()
.get("message")
.unwrap()
.as_str()
.unwrap()
.contains("Uncaught (in promise) [ 0 ]"));
.contains("[0]"));
let result = RunJob::from(JobPayload::RawFlow { value: flow.clone(), path: None })
.arg("n", json!(2))
@@ -2316,10 +2346,15 @@ async fn test_failure_module(db: Pool<Postgres>) {
.await
.result
.unwrap();
assert!(result["from failure module"]["error"]
.as_object()
.unwrap()
.get("message")
.unwrap()
.as_str()
.unwrap()
.contains("Uncaught (in promise) [ 0, 1 ]"));
.contains("[0,1]"));
let result = RunJob::from(JobPayload::RawFlow { value: flow.clone(), path: None })
.arg("n", json!(3))

View File

@@ -12,6 +12,9 @@ path = "src/lib.rs"
name = "windmill_api"
path = "src/main.rs"
[features]
enterprise = ["windmill-queue/enterprise"]
[dependencies]
windmill-queue.workspace = true
windmill-common = { workspace = true, features = [
@@ -66,3 +69,6 @@ hmac.workspace = true
cookie.workspace = true
sha2.workspace = true
urlencoding.workspace = true
async-stripe.workspace = true
lazy_static.workspace = true
prometheus.workspace = true

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
#!/bin/bash
set -e
npx @redocly/openapi-cli@latest bundle openapi.yaml > openapi-bundled.yaml
npx @redocly/openapi-cli@latest bundle openapi-bundled.yaml --ext yaml -d > openapi-deref.yaml
rm openapi-bundled.yaml

View File

File diff suppressed because it is too large Load Diff

View File

@@ -10,24 +10,32 @@ use std::collections::HashMap;
use crate::{
db::{UserDB, DB},
jobs::script_path_to_payload,
users::{Authed, OptAuthed},
users::{require_owner_of_path, Authed, OptAuthed},
variables::build_crypt,
webhook_util::{WebhookMessage, WebhookUtil},
};
use axum::{
extract::{Extension, Path, Query},
extract::{Extension, Json, Path, Query},
routing::{delete, get, post},
Json, Router,
Router,
};
use hyper::StatusCode;
use magic_crypt::MagicCryptTrait;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value};
use sha2::{Digest, Sha256};
use sql_builder::{bind::Bind, SqlBuilder};
use sqlx::{types::Uuid, FromRow};
use std::str;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
apps::ListAppQuery,
error::{to_anyhow, Error, JsonResult, Result},
users::owner_to_token_owner,
utils::{not_found_if_none, paginate, Pagination, StripPath},
users::username_to_permissioned_as,
utils::{
http_get_from_hub, list_elems_from_hub, not_found_if_none, paginate, Pagination, StripPath,
},
};
use windmill_queue::{push, JobPayload, RawCode};
@@ -35,14 +43,24 @@ pub fn workspaced_service() -> Router {
Router::new()
.route("/list", get(list_apps))
.route("/get/p/*path", get(get_app))
.route("/secret_of/*path", get(get_secret_id))
.route("/get/v/*id", get(get_app_by_id))
.route("/exists/*path", get(exists_app))
.route("/update/*path", post(update_app))
.route("/delete/*path", delete(delete_app))
.route("/create", post(create_app))
}
pub fn unauthed_service() -> Router {
Router::new().route("/execute_component/*path", post(execute_component))
Router::new()
.route("/execute_component/*path", post(execute_component))
.route("/public_app/:secret", get(get_public_app_by_secret))
}
pub fn global_service() -> Router {
Router::new()
.route("/hub/list", get(list_hub_apps))
.route("/hub/get/:id", get(get_hub_app_by_id))
}
#[derive(FromRow, Deserialize, Serialize)]
@@ -55,12 +73,13 @@ pub struct ListableApp {
pub extra_perms: serde_json::Value,
pub execution_mode: String,
pub starred: bool,
pub edited_at: chrono::DateTime<chrono::Utc>,
}
#[derive(FromRow, Serialize, Deserialize)]
pub struct AppVersion {
pub id: i64,
pub flow_id: Uuid,
pub app_id: Uuid,
pub value: serde_json::Value,
pub created_by: String,
pub created_at: chrono::DateTime<chrono::Utc>,
@@ -92,6 +111,7 @@ pub enum ExecutionMode {
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Policy {
pub on_behalf_of: Option<String>,
pub on_behalf_of_email: Option<String>,
//paths:
// - script/<path>
// - flow/<path>
@@ -118,13 +138,14 @@ pub struct EditApp {
async fn list_apps(
authed: Authed,
Query(pagination): Query<Pagination>,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(pagination): Query<Pagination>,
Query(lq): Query<ListAppQuery>,
) -> JsonResult<Vec<ListableApp>> {
let (per_page, offset) = paginate(pagination);
let sqlb = SqlBuilder::select_from("app")
let mut sqlb = SqlBuilder::select_from("app")
.fields(&[
"app.id",
"app.workspace_id",
@@ -153,6 +174,11 @@ async fn list_apps(
.offset(offset)
.limit(per_page)
.clone();
if lq.starred_only.unwrap_or(false) {
sqlb.and_where_is_not_null("favorite.path");
}
let sql = sqlb.sql().map_err(|e| Error::InternalErr(e.to_string()))?;
let mut tx = user_db.begin(&authed).await?;
let rows = sqlx::query_as::<_, ListableApp>(&sql)
@@ -201,7 +227,7 @@ async fn get_app_by_id(
"SELECT app.id, app.path, app.summary, app.versions, app.policy,
app.extra_perms, app_version.value,
app_version.created_at, app_version.created_by from app, app_version
WHERE app_version.id = $1 AND app.id = app_version.flow_id AND app.workspace_id = $2",
WHERE app_version.id = $1 AND app.id = app_version.app_id AND app.workspace_id = $2",
id,
&w_id
)
@@ -213,9 +239,79 @@ async fn get_app_by_id(
Ok(Json(app))
}
async fn get_public_app_by_secret(
Extension(db): Extension<DB>,
Path((w_id, secret)): Path<(String, String)>,
) -> JsonResult<AppWithLastVersion> {
let mut tx = db.begin().await?;
let mc = build_crypt(&mut tx, &w_id).await?;
let decrypted = mc
.decrypt_bytes_to_bytes(&(hex::decode(secret)?))
.map_err(|e| Error::InternalErr(e.to_string()))?;
let bytes = str::from_utf8(&decrypted).map_err(to_anyhow)?;
let id: i64 = bytes.parse().map_err(to_anyhow)?;
let app_o = sqlx::query_as!(
AppWithLastVersion,
"SELECT app.id, app.path, app.summary, app.versions, app.policy,
app.extra_perms, app_version.value,
app_version.created_at, app_version.created_by from app, app_version
WHERE app.id = $1 AND app.workspace_id = $2 AND app_version.id = app.versions[array_upper(app.versions, 1)]",
id,
&w_id
)
.fetch_optional(&mut tx)
.await?;
tx.commit().await?;
let app = not_found_if_none(app_o, "App", id.to_string())?;
let policy = serde_json::from_value::<Policy>(app.policy.clone()).map_err(to_anyhow)?;
if !matches!(policy.execution_mode, ExecutionMode::Anonymous) {
return Err(Error::NotAuthorized(
"App visibility does not allow public access".to_string(),
));
}
Ok(Json(app))
}
async fn get_secret_id(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
let id_o = sqlx::query_scalar!(
"SELECT app.id FROM app
WHERE app.path = $1 AND app.workspace_id = $2",
path,
&w_id
)
.fetch_optional(&mut tx)
.await?;
let id = not_found_if_none(id_o, "App", path.to_string())?;
let mc = build_crypt(&mut tx, &w_id).await?;
let hx = hex::encode(mc.encrypt_str_to_bytes(id.to_string()));
tx.commit().await?;
Ok(hx)
}
async fn create_app(
authed: Authed,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
Path(w_id): Path<String>,
Json(app): Json<CreateApp>,
) -> Result<(StatusCode, String)> {
@@ -235,7 +331,7 @@ async fn create_app(
let v_id = sqlx::query_scalar!(
"INSERT INTO app_version
(flow_id, value, created_by)
(app_id, value, created_by)
VALUES ($1, $2, $3) RETURNING id",
id,
app.value,
@@ -262,14 +358,51 @@ async fn create_app(
None,
)
.await?;
tx.commit().await?;
webhook.send_message(WebhookMessage::CreateApp {
workspace: w_id.clone(),
path: app.path.clone(),
});
Ok((StatusCode::CREATED, app.path))
}
async fn list_hub_apps(
Authed { email, .. }: Authed,
Extension(http_client): Extension<Client>,
) -> JsonResult<serde_json::Value> {
let flows = list_elems_from_hub(
http_client,
"https://hub.windmill.dev/searchUiData?approved=true",
&email,
)
.await?;
Ok(Json(flows))
}
pub async fn get_hub_app_by_id(
Authed { email, .. }: Authed,
Path(id): Path<i32>,
Extension(http_client): Extension<Client>,
) -> JsonResult<serde_json::Value> {
let value = http_get_from_hub(
http_client,
&format!("https://hub.windmill.dev/apps/{id}/json"),
&email,
false,
)
.await?
.json()
.await
.map_err(to_anyhow)?;
Ok(Json(value))
}
async fn delete_app(
authed: Authed,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
@@ -293,6 +426,8 @@ async fn delete_app(
)
.await?;
tx.commit().await?;
webhook
.send_message(WebhookMessage::DeleteApp { workspace: w_id.clone(), path: path.to_owned() });
Ok(format!("app {} deleted", path))
}
@@ -300,6 +435,8 @@ async fn delete_app(
async fn update_app(
authed: Authed,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(ns): Json<EditApp>,
) -> Result<String> {
@@ -315,6 +452,12 @@ async fn update_app(
sqlb.and_where_eq("workspace_id", "?".bind(&w_id));
if let Some(npath) = &ns.path {
if npath != path {
if !authed.is_admin {
require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db)
.await?;
}
}
sqlb.set_str("path", npath);
}
@@ -343,7 +486,7 @@ async fn update_app(
"".to_string()
};
if let Some(nvalue) = &ns.value {
let flow_id = sqlx::query_scalar!(
let app_id = sqlx::query_scalar!(
"SELECT id FROM app WHERE path = $1 AND workspace_id = $2",
path,
w_id
@@ -353,9 +496,9 @@ async fn update_app(
let v_id = sqlx::query_scalar!(
"INSERT INTO app_version
(flow_id, value, created_by)
(app_id, value, created_by)
VALUES ($1, $2, $3) RETURNING id",
flow_id,
app_id,
nvalue,
authed.username,
)
@@ -382,6 +525,11 @@ async fn update_app(
)
.await?;
tx.commit().await?;
webhook.send_message(WebhookMessage::UpdateApp {
workspace: w_id.clone(),
old_path: path.to_owned(),
new_path: npath.clone(),
});
Ok(format!("app {} updated (npath: {:?})", path, npath))
}
@@ -411,12 +559,12 @@ async fn execute_component(
Json(payload): Json<ExecuteApp>,
) -> Result<String> {
match (payload.path.is_some(), payload.raw_code.is_some()) {
(true, true) => {
(false, false) => {
return Err(Error::BadRequest(
"path or raw_code is required".to_string(),
))
}
(false, false) => {
(true, true) => {
return Err(Error::BadRequest(
"path and raw_code cannot be set at the same time".to_string(),
))
@@ -427,18 +575,6 @@ async fn execute_component(
let path = path.to_path();
let mut tx = db.begin().await?;
let policy_o = sqlx::query_scalar!(
"SELECT policy from app WHERE path = $1 AND workspace_id = $2",
path,
&w_id
)
.fetch_optional(&mut tx)
.await?;
let policy = not_found_if_none(policy_o, "App", path)?;
let policy = serde_json::from_value::<Policy>(policy).map_err(to_anyhow)?;
let policy = if let Some(static_fields) = payload.clone().force_viewer_static_fields {
let mut hm = HashMap::new();
if let Some(path) = payload.path.clone() {
@@ -449,49 +585,50 @@ async fn execute_component(
static_fields,
);
}
Policy { execution_mode: ExecutionMode::Viewer, triggerables: hm, on_behalf_of: None }
Policy {
execution_mode: ExecutionMode::Viewer,
triggerables: hm,
on_behalf_of: None,
on_behalf_of_email: None,
}
} else {
policy
let policy_o = sqlx::query_scalar!(
"SELECT policy from app WHERE path = $1 AND workspace_id = $2",
path,
&w_id
)
.fetch_optional(&mut tx)
.await?;
let policy = not_found_if_none(policy_o, "App", path)?;
serde_json::from_value::<Policy>(policy).map_err(to_anyhow)?
};
let (username, permissioned_as) = match policy.execution_mode {
let (username, permissioned_as, email) = match policy.execution_mode {
ExecutionMode::Anonymous => {
let username = opt_authed
.map(|a| a.username)
.unwrap_or_else(|| "anonymous".to_string());
let permissioned_as = policy
.on_behalf_of
.as_ref()
.ok_or_else(|| {
Error::BadRequest(
"on_behalf_of is missing in the app policy and is required for anonymous execution"
.to_string(),
)
})?
.to_string();
(username, permissioned_as)
let (permissioned_as, email) = get_on_behalf_of(&policy)?;
(username, permissioned_as, email)
}
ExecutionMode::Publisher => {
let username = opt_authed.map(|a| a.username).ok_or_else(|| {
Error::BadRequest("publisher execution mode requires authentication".to_string())
})?;
let permissioned_as = policy
.on_behalf_of
.as_ref()
.ok_or_else(|| {
Error::BadRequest(
"on_behalf_of is missing in the app policy and is required for publisher execution"
.to_string(),
)
})?
.to_string();
(username, permissioned_as)
let (permissioned_as, email) = get_on_behalf_of(&policy)?;
(username, permissioned_as, email)
}
ExecutionMode::Viewer => {
let username = opt_authed
.map(|a| a.username)
.ok_or_else(|| Error::BadRequest("".to_string()))?;
(username.clone(), owner_to_token_owner(&username, false))
let (username, email) = opt_authed.map(|a| (a.username, a.email)).ok_or_else(|| {
Error::BadRequest("Required to be authed in viewer mode".to_string())
})?;
(
username.clone(),
username_to_permissioned_as(&username),
email,
)
}
};
@@ -525,14 +662,17 @@ async fn execute_component(
tx,
&w_id,
job_payload,
Some(args),
args,
&username,
&email,
permissioned_as,
None,
None,
None,
false,
false,
None,
true,
)
.await?;
@@ -540,6 +680,47 @@ async fn execute_component(
Ok(uuid.to_string())
}
fn get_on_behalf_of(policy: &Policy) -> Result<(String, String)> {
let permissioned_as = policy
.on_behalf_of
.as_ref()
.ok_or_else(|| {
Error::BadRequest(
"on_behalf_of is missing in the app policy and is required for anonymous execution"
.to_string(),
)
})?
.to_string();
let email = policy
.on_behalf_of_email
.as_ref()
.ok_or_else(|| {
Error::BadRequest(
"on_behalf_of_email is missing in the app policy and is required for anonymous execution"
.to_string(),
)
})?
.to_string();
Ok((permissioned_as, email))
}
async fn exists_app(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<bool> {
let path = path.to_path();
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM app WHERE path = $1 AND workspace_id = $2)",
path,
w_id
)
.fetch_one(&db)
.await?
.unwrap_or(false);
Ok(Json(exists))
}
fn build_args(
policy: Policy,
path: String,

View File

@@ -72,6 +72,34 @@ impl UserDB {
)
.fetch_optional(&mut tx)
.await?;
let (folders_write, folders_read): &(Vec<_>, Vec<_>) =
&authed.folders.clone().into_iter().partition(|x| x.1);
let mut folders_read = folders_read.clone();
folders_read.extend(folders_write.clone());
sqlx::query!(
"SELECT set_config('session.folders_read', $1, true)",
folders_read
.iter()
.map(|x| x.0.clone())
.collect::<Vec<_>>()
.join(",")
)
.fetch_optional(&mut tx)
.await?;
sqlx::query!(
"SELECT set_config('session.folders_write', $1, true)",
folders_write
.iter()
.map(|x| x.0.clone())
.collect::<Vec<_>>()
.join(",")
)
.fetch_optional(&mut tx)
.await?;
Ok(tx)
}
}

View File

@@ -11,7 +11,7 @@ use reqwest::Client;
use sql_builder::prelude::*;
use axum::{
extract::{Extension, Host, Path, Query},
extract::{Extension, Path, Query},
routing::{get, post},
Json, Router,
};
@@ -21,15 +21,18 @@ use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{self, to_anyhow, Error, JsonResult, Result},
flows::{Flow, ListFlowQuery, ListableFlow, NewFlow},
schedule::Schedule,
utils::{
http_get_from_hub, list_elems_from_hub, not_found_if_none, paginate, Pagination, StripPath,
},
};
use windmill_queue::{push, JobPayload};
use windmill_queue::{push, schedule::push_scheduled_job, JobPayload};
use crate::{
db::{UserDB, DB},
users::Authed,
schedule::clear_schedule,
users::{require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookUtil},
};
pub fn workspaced_service() -> Router {
@@ -40,6 +43,7 @@ pub fn workspaced_service() -> Router {
.route("/archive/*path", post(archive_flow_by_path))
.route("/get/*path", get(get_flow_by_path))
.route("/exists/*path", get(exists_flow_by_path))
.route("/list_paths", get(list_paths))
}
pub fn global_service() -> Router {
@@ -108,33 +112,45 @@ async fn list_flows(
}
async fn list_hub_flows(
Authed { email, username, .. }: Authed,
Authed { email, .. }: Authed,
Extension(http_client): Extension<Client>,
Host(host): Host,
) -> JsonResult<serde_json::Value> {
let flows = list_elems_from_hub(
http_client,
"https://hub.windmill.dev/searchFlowData?approved=true",
email,
username,
host,
&email,
)
.await?;
Ok(Json(flows))
}
async fn list_paths(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
) -> JsonResult<Vec<String>> {
let mut tx = user_db.begin(&authed).await?;
let flows = sqlx::query_scalar!(
"SELECT distinct(path) FROM flow WHERE workspace_id = $1",
w_id
)
.fetch_all(&mut tx)
.await?;
tx.commit().await?;
Ok(Json(flows))
}
pub async fn get_hub_flow_by_id(
Authed { email, username, .. }: Authed,
Authed { email, .. }: Authed,
Path(id): Path<i32>,
Extension(http_client): Extension<Client>,
Host(host): Host,
) -> JsonResult<serde_json::Value> {
let value = http_get_from_hub(
http_client,
&format!("https://hub.windmill.dev/flows/{id}/json"),
email,
username,
host,
&email,
false,
)
.await?
@@ -144,15 +160,36 @@ pub async fn get_hub_flow_by_id(
Ok(Json(value))
}
async fn check_path_conflict<'c>(
tx: &mut Transaction<'c, Postgres>,
w_id: &str,
path: &str,
) -> Result<()> {
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM flow WHERE path = $1 AND workspace_id = $2)",
path,
w_id
)
.fetch_one(tx)
.await?
.unwrap_or(false);
if exists {
return Err(Error::BadRequest(format!("Flow {} already exists", path)));
}
return Ok(());
}
async fn create_flow(
authed: Authed,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
Path(w_id): Path<String>,
Json(nf): Json<NewFlow>,
) -> Result<(StatusCode, String)> {
// cron::Schedule::from_str(&ns.schedule).map_err(|e| error::Error::BadRequest(e.to_string()))?;
let mut tx = user_db.clone().begin(&authed).await?;
check_path_conflict(&mut tx, &w_id, &nf.path).await?;
check_schedule_conflict(&mut tx, &w_id, &nf.path).await?;
sqlx::query!(
@@ -186,20 +223,27 @@ async fn create_flow(
.await?;
tx.commit().await?;
webhook.send_message(WebhookMessage::CreateFlow {
workspace: w_id.clone(),
path: nf.path.clone(),
});
let tx = user_db.begin(&authed).await?;
let (dependency_job_uuid, mut tx) = push(
tx,
&w_id,
JobPayload::FlowDependencies { path: nf.path.clone() },
None,
serde_json::Map::new(),
&authed.username,
windmill_common::users::owner_to_token_owner(&authed.username, false),
&authed.email,
windmill_common::users::username_to_permissioned_as(&authed.username),
None,
None,
None,
false,
false,
None,
true,
)
.await?;
sqlx::query!(
@@ -241,6 +285,8 @@ async fn check_schedule_conflict<'c>(
async fn update_flow(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
webhook: WebhookUtil,
Path((w_id, flow_path)): Path<(String, StripPath)>,
Json(nf): Json<NewFlow>,
) -> Result<String> {
@@ -252,7 +298,7 @@ async fn update_flow(
let schema = nf.schema.map(|x| x.0);
let old_dep_job = sqlx::query_scalar!(
"SELECT dependency_job FROM flow WHERE path = $1 AND workspace_id = $2",
nf.path,
flow_path,
w_id
)
.fetch_optional(&mut tx)
@@ -260,19 +306,58 @@ async fn update_flow(
let old_dep_job = not_found_if_none(old_dep_job, "Flow", flow_path)?;
sqlx::query!(
"UPDATE flow SET path = $1, summary = $2, description = $3, value = $4, edited_by = $5, \
edited_at = now(), schema = $6, dependency_job = NULL WHERE path = $7 AND workspace_id = $8",
edited_at = now(), schema = $6::text::json, dependency_job = NULL WHERE path = $7 AND workspace_id = $8",
nf.path,
nf.summary,
nf.description,
nf.value,
&authed.username,
schema,
schema.and_then(|x| serde_json::to_string(&x).ok()),
flow_path,
w_id,
)
.execute(&mut tx)
.await?;
if nf.path != flow_path {
check_schedule_conflict(&mut tx, &w_id, &nf.path).await?;
if !authed.is_admin {
require_owner_of_path(&w_id, &authed.username, &authed.groups, &flow_path, &db).await?;
}
let mut schedulables = sqlx::query_as!(
Schedule,
"UPDATE schedule SET script_path = $1 WHERE script_path = $2 AND path != $2 AND workspace_id = $3 AND is_flow IS true RETURNING *",
nf.path,
flow_path,
w_id,
)
.fetch_all(&mut tx)
.await?;
let schedule = sqlx::query_as!(Schedule,
"UPDATE schedule SET path = $1, script_path = $1 WHERE path = $2 AND workspace_id = $3 AND is_flow IS true RETURNING *",
nf.path,
flow_path,
w_id,
)
.fetch_optional(&mut tx)
.await?;
if let Some(schedule) = schedule {
schedulables.push(schedule);
}
for schedule in schedulables {
clear_schedule(&mut tx, flow_path, true).await?;
if schedule.enabled {
tx = push_scheduled_job(tx, schedule).await?;
}
}
}
audit_log(
&mut tx,
&authed.username,
@@ -290,20 +375,28 @@ async fn update_flow(
.await?;
tx.commit().await?;
webhook.send_message(WebhookMessage::UpdateFlow {
workspace: w_id.clone(),
old_path: flow_path.to_owned(),
new_path: nf.path.clone(),
});
let tx = user_db.begin(&authed).await?;
let (dependency_job_uuid, mut tx) = push(
tx,
&w_id,
JobPayload::FlowDependencies { path: nf.path.clone() },
None,
serde_json::Map::new(),
&authed.username,
windmill_common::users::owner_to_token_owner(&authed.username, false),
&authed.email,
windmill_common::users::username_to_permissioned_as(&authed.username),
None,
None,
None,
false,
false,
None,
true,
)
.await?;
sqlx::query!(
@@ -369,6 +462,7 @@ async fn exists_flow_by_path(
async fn archive_flow_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
@@ -393,6 +487,10 @@ async fn archive_flow_by_path(
)
.await?;
tx.commit().await?;
webhook.send_message(WebhookMessage::ArchiveFlow {
workspace: w_id.clone(),
path: path.to_owned(),
});
Ok(format!("Flow {path} archived"))
}
@@ -426,6 +524,7 @@ mod tests {
InputTransform::Static { value: serde_json::json!("test2") },
)]
.into(),
hash: None,
},
stop_after_if: None,
summary: None,
@@ -477,6 +576,7 @@ mod tests {
value: FlowModuleValue::Script {
path: "test".to_string(),
input_transforms: HashMap::new(),
hash: None,
},
stop_after_if: Some(StopAfterIf {
expr: "previous.isEmpty()".to_string(),

View File

@@ -0,0 +1,570 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2022
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
use crate::{
db::{UserDB, DB},
users::Authed,
webhook_util::{WebhookMessage, WebhookUtil},
};
use axum::{
extract::{Extension, Path, Query},
routing::{delete, get, post},
Json, Router,
};
use itertools::Itertools;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{self, Error, JsonResult, Result},
users::username_to_permissioned_as,
utils::{not_found_if_none, paginate, Pagination},
};
use serde::{Deserialize, Serialize};
use sqlx::{query_scalar, FromRow, Postgres, Transaction};
pub fn workspaced_service() -> Router {
Router::new()
.route("/list", get(list_folders))
.route("/listnames", get(list_foldernames))
.route("/create", post(create_folder))
.route("/get/:name", get(get_folder))
.route("/update/:name", post(update_folder))
.route("/getusage/:name", get(get_folder_usage))
.route("/delete/:name", delete(delete_folder))
.route("/addowner/:name", post(add_owner))
.route("/removeowner/:name", post(remove_owner))
.route("/is_owner/*path", get(is_owner))
}
#[derive(FromRow, Serialize, Deserialize, Clone)]
pub struct Folder {
pub workspace_id: String,
pub name: String,
pub display_name: String,
pub owners: Vec<String>,
pub extra_perms: serde_json::Value,
}
#[derive(Deserialize)]
pub struct NewFolder {
pub name: String,
pub display_name: Option<String>,
pub owners: Option<Vec<String>>,
pub extra_perms: Option<serde_json::Value>,
}
#[derive(Deserialize)]
pub struct UpdateFolder {
pub display_name: Option<String>,
pub owners: Option<Vec<String>>,
pub extra_perms: Option<serde_json::Value>,
}
#[derive(Deserialize)]
pub struct Owner {
pub owner: String,
}
async fn list_folders(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(pagination): Query<Pagination>,
) -> JsonResult<Vec<Folder>> {
let (per_page, offset) = paginate(pagination);
let mut tx = user_db.begin(&authed).await?;
let rows = sqlx::query_as!(
Folder,
"SELECT workspace_id, name, display_name, owners, extra_perms FROM folder WHERE workspace_id = $1 ORDER BY name desc LIMIT $2 OFFSET $3",
w_id,
per_page as i64,
offset as i64
)
.fetch_all(&mut tx)
.await?;
tx.commit().await?;
Ok(Json(rows))
}
async fn list_foldernames(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(pagination): Query<Pagination>,
) -> JsonResult<Vec<String>> {
let (per_page, offset) = paginate(pagination);
let mut tx = user_db.begin(&authed).await?;
let rows = sqlx::query_scalar!(
"SELECT name FROM folder WHERE workspace_id = $1 ORDER BY name desc LIMIT $2 OFFSET $3",
w_id,
per_page as i64,
offset as i64
)
.fetch_all(&mut tx)
.await?;
tx.commit().await?;
Ok(Json(rows))
}
async fn check_name_conflict<'c>(
tx: &mut Transaction<'c, Postgres>,
w_id: &str,
name: &str,
) -> Result<()> {
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM folder WHERE name = $1 AND workspace_id = $2)",
name,
w_id
)
.fetch_one(tx)
.await?
.unwrap_or(false);
if exists {
return Err(windmill_common::error::Error::BadRequest(format!(
"Folder {} already exists",
name
)));
}
return Ok(());
}
async fn create_folder(
authed: Authed,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
Path(w_id): Path<String>,
Json(ng): Json<NewFolder>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
check_name_conflict(&mut tx, &w_id, &ng.name).await?;
let owner = username_to_permissioned_as(&authed.username);
let owners = &ng.owners.unwrap_or(vec![owner.clone()]);
if let Some(extra_perms) = ng.extra_perms.clone() {
for o in owners {
if !extra_perms
.get(&o)
.and_then(|x| x.as_bool())
.unwrap_or(false)
{
return Err(windmill_common::error::Error::BadRequest(format!(
"Owner {} would not have permission to write to folder and that is an inconsistent state",
o
)));
}
}
}
let extra_perms = ng.extra_perms.unwrap_or_else(|| {
let mut map = serde_json::Map::new();
for o in owners {
map.insert(o.clone(), serde_json::json!(true));
}
serde_json::Value::Object(map)
});
sqlx::query_as!(
Folder,
"INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms) VALUES ($1, $2, $3, $4, $5)",
w_id,
ng.name,
ng.display_name.unwrap_or(ng.name.clone()),
owners,
extra_perms,
)
.execute(&mut tx)
.await?;
audit_log(
&mut tx,
&authed.username,
"folder.create",
ActionKind::Create,
&w_id,
Some(&ng.name.to_string()),
None,
)
.await?;
tx.commit().await?;
webhook.send_message(WebhookMessage::CreateFolder {
workspace: w_id.clone(),
name: ng.name.clone(),
});
Ok(format!("Created folder {}", ng.name))
}
pub async fn is_owner(
Authed { username, is_admin, groups, .. }: Authed,
Extension(db): Extension<DB>,
Path((w_id, name)): Path<(String, String)>,
) -> JsonResult<bool> {
if is_admin {
Ok(Json(true))
} else {
Ok(Json(
require_is_owner(&name, &username, &groups, &w_id, &db)
.await
.is_ok(),
))
}
}
pub async fn require_is_owner(
folder_name: &str,
username: &str,
groups: &Vec<String>,
w_id: &str,
db: &DB,
) -> Result<()> {
let is_owner = query_scalar!(
"SELECT EXISTS(SELECT 1 FROM folder WHERE CONCAT('u/', $1::text) = ANY(owners) AND name = $2 AND workspace_id = $4) OR exists(
SELECT 1 FROM folder, unnest(folder.owners) as o
WHERE o = ANY($3::text[]) AND folder.name = $2 AND folder.workspace_id = $4)",
username,
folder_name,
groups,
w_id,
).fetch_one(db)
.await?
.unwrap_or(false);
if !is_owner {
Err(Error::BadRequest(format!(
"{} is not an owner of {} and hence is not authorized to perform this operation",
username, folder_name
)))
} else {
Ok(())
}
}
async fn update_folder(
authed: Authed,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
Path((w_id, name)): Path<(String, String)>,
Json(ng): Json<UpdateFolder>,
) -> Result<String> {
use sql_builder::prelude::*;
let mut sqlb = SqlBuilder::update_table("folder");
sqlb.and_where_eq("name", "?".bind(&name));
sqlb.and_where_eq("workspace_id", "?".bind(&w_id));
if let Some(display_name) = ng.display_name {
sqlb.set("display_name", display_name);
}
if let Some(owners) = ng.owners {
sqlb.set_str("owners", format!("{{{}}}", owners.into_iter().join(",")));
}
if let Some(extra_perms) = ng.extra_perms {
sqlb.set_str("extra_perms", extra_perms.to_string());
}
sqlb.returning("*");
let mut tx = user_db.begin(&authed).await?;
let sql = sqlb
.sql()
.map_err(|e| error::Error::InternalErr(e.to_string()))?;
let nfolder = sqlx::query_as::<_, Folder>(&sql).fetch_one(&mut tx).await?;
if let Some(extra_perms) = nfolder.extra_perms.as_object() {
for o in nfolder.owners {
if !extra_perms
.get(&o)
.and_then(|x| x.as_bool())
.unwrap_or(false)
{
return Err(windmill_common::error::Error::BadRequest(format!(
"Owner {} would not have permission to write to folder and that is an invalid state",
o
)));
}
}
}
audit_log(
&mut tx,
&authed.username,
"folder.update",
ActionKind::Update,
&w_id,
Some(&name.to_string()),
None,
)
.await?;
tx.commit().await?;
webhook.send_message(WebhookMessage::UpdateFolder {
workspace: w_id.clone(),
name: name.to_owned(),
});
Ok(format!("Updated folder {}", name))
}
pub async fn get_folderopt<'c>(
db: &mut Transaction<'c, Postgres>,
w_id: &str,
name: &str,
) -> Result<Option<Folder>> {
let folderopt = sqlx::query_as!(
Folder,
"SELECT workspace_id, name, display_name, owners, extra_perms FROM folder WHERE name = $1 AND workspace_id = $2",
name,
w_id
)
.fetch_optional(db)
.await?;
Ok(folderopt)
}
async fn get_folder(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path((w_id, name)): Path<(String, String)>,
) -> JsonResult<Folder> {
let mut tx = user_db.begin(&authed).await?;
let folder = not_found_if_none(get_folderopt(&mut tx, &w_id, &name).await?, "Folder", &name)?;
tx.commit().await?;
Ok(Json(folder))
}
#[derive(Serialize)]
struct FolderUsage {
pub scripts: i64,
pub schedules: i64,
pub flows: i64,
pub apps: i64,
pub resources: i64,
pub variables: i64,
}
async fn get_folder_usage(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path((w_id, name)): Path<(String, String)>,
) -> JsonResult<FolderUsage> {
let mut tx = user_db.begin(&authed).await?;
let scripts = sqlx::query_scalar!(
"SELECT count(path) FROM script WHERE path LIKE 'f/' || $1 || '%' AND archived IS false AND workspace_id = $2",
name,
w_id
)
.fetch_one(&mut tx)
.await?
.unwrap_or(0);
let schedules = sqlx::query_scalar!(
"SELECT count(path) FROM schedule WHERE path LIKE 'f/' || $1 || '%' AND workspace_id = $2",
name,
w_id
)
.fetch_one(&mut tx)
.await?
.unwrap_or(0);
let flows = sqlx::query_scalar!(
"SELECT count(path) FROM flow WHERE path LIKE 'f/' || $1 || '%' AND archived IS false AND workspace_id = $2",
name,
w_id
)
.fetch_one(&mut tx)
.await?
.unwrap_or(0);
let apps = sqlx::query_scalar!(
"SELECT count(path) FROM app WHERE path LIKE 'f/' || $1 || '%' AND workspace_id = $2",
name,
w_id
)
.fetch_one(&mut tx)
.await?
.unwrap_or(0);
let resources = sqlx::query_scalar!(
"SELECT count(path) FROM resource WHERE path LIKE 'f/' || $1 || '%' AND workspace_id = $2",
name,
w_id
)
.fetch_one(&mut tx)
.await?
.unwrap_or(0);
let variables = sqlx::query_scalar!(
"SELECT count(path) FROM variable WHERE path LIKE 'f/' || $1 || '%' AND workspace_id = $2",
name,
w_id
)
.fetch_one(&mut tx)
.await?
.unwrap_or(0);
tx.commit().await?;
Ok(Json(FolderUsage {
scripts,
flows,
schedules,
apps,
resources,
variables,
}))
}
async fn delete_folder(
authed: Authed,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
Path((w_id, name)): Path<(String, String)>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
not_found_if_none(get_folderopt(&mut tx, &w_id, &name).await?, "Folder", &name)?;
sqlx::query!(
"DELETE FROM folder WHERE name = $1 AND workspace_id = $2",
name,
w_id
)
.execute(&mut tx)
.await?;
audit_log(
&mut tx,
&authed.username,
"folder.delete",
ActionKind::Delete,
&w_id,
Some(&name.to_string()),
None,
)
.await?;
tx.commit().await?;
webhook
.send_message(WebhookMessage::DeleteFolder { workspace: w_id.clone(), name: name.clone() });
Ok(format!("delete folder at name {}", name))
}
async fn add_owner(
authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
Path((w_id, name)): Path<(String, String)>,
Json(Owner { owner }): Json<Owner>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
not_found_if_none(get_folderopt(&mut tx, &w_id, &name).await?, "Folder", &name)?;
if !authed.is_admin {
require_is_owner(&name, &authed.username, &authed.groups, &w_id, &db).await?;
}
sqlx::query!(
"UPDATE folder SET owners = array_append(owners, $1) WHERE name = $2 AND workspace_id = $3 AND NOT $1 = ANY(owners) RETURNING name",
owner,
name,
&w_id,
)
.fetch_optional(&mut tx)
.await?;
audit_log(
&mut tx,
&authed.username,
"folder.add_owner",
ActionKind::Update,
&w_id,
Some(&name.to_string()),
Some([("owner", owner.as_str())].into()),
)
.await?;
tx.commit().await?;
webhook
.send_message(WebhookMessage::UpdateFolder { workspace: w_id.clone(), name: name.clone() });
Ok(format!("Added {} to folder {}", owner, name))
}
pub async fn get_folders_for_user(
w_id: &str,
username: &str,
groups: &[String],
db: &DB,
) -> Result<Vec<(String, bool)>> {
let mut perms = groups
.into_iter()
.map(|x| format!("g/{}", x))
.collect::<Vec<_>>();
perms.insert(0, format!("u/{}", username));
let folders = sqlx::query!(
"SELECT name, (EXISTS (SELECT 1 FROM (SELECT key, value FROM jsonb_each_text(extra_perms) WHERE key = ANY($1)) t WHERE value::boolean IS true)) as write FROM folder
WHERE extra_perms ?| $1 AND workspace_id = $2",
&perms[..],
w_id,
)
.fetch_all(db)
.await?
.into_iter()
.map(|x| (x.name, x.write.unwrap_or(false)))
.collect();
Ok(folders)
}
async fn remove_owner(
authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
Path((w_id, name)): Path<(String, String)>,
Json(Owner { owner }): Json<Owner>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
not_found_if_none(get_folderopt(&mut tx, &w_id, &name).await?, "Folder", &name)?;
if !authed.is_admin {
require_is_owner(&name, &authed.username, &authed.groups, &w_id, &db).await?;
}
sqlx::query!(
"UPDATE folder SET owners = array_remove(owners, $1) WHERE name = $2 AND workspace_id = $3 RETURNING name",
owner,
name,
&w_id,
)
.fetch_optional(&mut tx)
.await?;
audit_log(
&mut tx,
&authed.username,
"folder.remove_owner",
ActionKind::Update,
&w_id,
Some(&name.to_string()),
Some([("owner", owner.as_str())].into()),
)
.await?;
tx.commit().await?;
webhook
.send_message(WebhookMessage::UpdateFolder { workspace: w_id.clone(), name: name.clone() });
Ok(format!("Removed {} to folder {}", owner, name))
}

View File

@@ -6,7 +6,10 @@
* LICENSE-AGPL for a copy of the license.
*/
use crate::{db::UserDB, users::Authed};
use crate::{
db::{UserDB, DB},
users::{require_owner_of_path, Authed},
};
use axum::{
extract::{Extension, Path},
routing::{get, post},
@@ -34,6 +37,7 @@ pub struct GranularAcl {
async fn add_granular_acl(
authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(GranularAcl { owner, write }): Json<GranularAcl>,
@@ -44,7 +48,20 @@ async fn add_granular_acl(
.ok_or_else(|| Error::BadRequest("Invalid path or kind".to_string()))?;
let mut tx = user_db.begin(&authed).await?;
let identifier = if kind == "group_" { "name" } else { "path" };
if !authed.is_admin {
if kind == "folder" {
crate::folders::require_is_owner(&path, &authed.username, &authed.groups, &w_id, &db)
.await?;
} else if kind == "group_" {
} else {
require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db).await?;
}
}
let identifier = if kind == "group_" || kind == "folder" {
"name"
} else {
"path"
};
let obj_o = sqlx::query_scalar::<_, serde_json::Value>(&format!(
"UPDATE {kind} SET extra_perms = jsonb_set(extra_perms, '{{\"{owner}\"}}', to_jsonb($1), \
true) WHERE {identifier} = $2 AND workspace_id = $3 RETURNING extra_perms"
@@ -63,17 +80,25 @@ async fn add_granular_acl(
async fn remove_granular_acl(
authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(GranularAcl { owner, write: _ }): Json<GranularAcl>,
) -> Result<String> {
let path = path.to_path();
if !authed.is_admin {
require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db).await?;
}
let (kind, path) = path
.split_once('/')
.ok_or_else(|| Error::BadRequest("Invalid path or kind".to_string()))?;
let mut tx = user_db.begin(&authed).await?;
let identifier = if kind == "group_" { "name" } else { "path" };
let identifier = if kind == "group_" || kind == "folder" {
"name"
} else {
"path"
};
let obj_o = sqlx::query_scalar::<_, serde_json::Value>(&format!(
"UPDATE {kind} SET extra_perms = extra_perms - $1 WHERE {identifier} = $2 AND \
workspace_id = $3 RETURNING extra_perms"

View File

@@ -8,7 +8,7 @@
use crate::{
db::{UserDB, DB},
users::Authed,
users::{get_groups_for_user, Authed},
};
use axum::{
extract::{Extension, Path, Query},
@@ -16,14 +16,15 @@ use axum::{
Json, Router,
};
use windmill_audit::{audit_log, ActionKind};
use windmill_common::users::username_to_permissioned_as;
use windmill_common::{
error::{Error, JsonResult, Result},
users::owner_to_token_owner,
utils::{not_found_if_none, paginate, Pagination},
};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Postgres, Transaction};
use sqlx::{query_scalar, FromRow, Postgres, Transaction};
use windmill_queue::CLOUD_HOSTED;
pub fn workspaced_service() -> Router {
Router::new()
@@ -35,6 +36,7 @@ pub fn workspaced_service() -> Router {
.route("/delete/:name", delete(delete_group))
.route("/adduser/:name", post(add_user))
.route("/removeuser/:name", post(remove_user))
.route("/is_owner", get(is_owner))
}
#[derive(FromRow, Serialize, Deserialize)]
@@ -90,20 +92,97 @@ async fn list_groups(
Ok(Json(rows))
}
#[derive(Deserialize)]
struct QueryListGroup {
pub only_member_of: Option<bool>,
}
async fn list_group_names(
Authed { username, .. }: Authed,
Extension(db): Extension<DB>,
Query(QueryListGroup { only_member_of }): Query<QueryListGroup>,
Path(w_id): Path<String>,
) -> JsonResult<Vec<String>> {
let rows = sqlx::query_scalar!(
"SELECT name FROM group_ WHERE workspace_id = $1 ORDER BY name desc",
w_id
)
.fetch_all(&db)
.await?;
let rows = if !only_member_of.unwrap_or(false) {
sqlx::query_scalar!(
"SELECT name FROM group_ WHERE workspace_id = $1 ORDER BY name desc",
w_id
)
.fetch_all(&db)
.await?
} else {
get_groups_for_user(&w_id, &username, &db).await?
};
Ok(Json(rows))
}
async fn check_name_conflict<'c>(
tx: &mut Transaction<'c, Postgres>,
w_id: &str,
name: &str,
) -> Result<()> {
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM group_ WHERE name = $1 AND workspace_id = $2)",
name,
w_id
)
.fetch_one(tx)
.await?
.unwrap_or(false);
if exists {
return Err(windmill_common::error::Error::BadRequest(format!(
"Group {} already exists",
name
)));
}
return Ok(());
}
pub async fn is_owner(
Authed { username, is_admin, groups, .. }: Authed,
Extension(db): Extension<DB>,
Path((w_id, name)): Path<(String, String)>,
) -> JsonResult<bool> {
if is_admin {
Ok(Json(true))
} else {
Ok(Json(
require_is_owner(&name, &username, &groups, &w_id, &db)
.await
.is_ok(),
))
}
}
pub async fn require_is_owner(
group_name: &str,
username: &str,
groups: &Vec<String>,
w_id: &str,
db: &DB,
) -> Result<()> {
let is_owner = query_scalar!(
"SELECT EXISTS(SELECT 1 FROM group_ WHERE (group_.extra_perms ->> CONCAT('u/', $1::text))::boolean AND name = $2 AND workspace_id = $4) OR exists(
SELECT 1 FROM group_ g, jsonb_each_text(g.extra_perms) f
WHERE $2 = g.name AND $4 = g.workspace_id AND SPLIT_PART(key, '/', 1) = 'g' AND key = ANY($3::text[])
AND value::boolean)",
username,
group_name,
groups,
w_id,
).fetch_one(db)
.await?
.unwrap_or(false);
if !is_owner {
Err(Error::BadRequest(format!(
"{} is not an owner of {} and hence is not authorized to perform this operation",
username, group_name
)))
} else {
Ok(())
}
}
async fn create_group(
authed: Authed,
Extension(user_db): Extension<UserDB>,
@@ -112,13 +191,25 @@ async fn create_group(
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
check_name_conflict(&mut tx, &w_id, &ng.name).await?;
sqlx::query_as!(
Group,
"INSERT INTO group_ (workspace_id, name, summary, extra_perms) VALUES ($1, $2, $3, $4)",
w_id,
ng.name,
ng.summary,
serde_json::json!({owner_to_token_owner(&authed.username, false): true})
serde_json::json!({username_to_permissioned_as(&authed.username): true})
)
.execute(&mut tx)
.await?;
sqlx::query_as!(
Group,
"INSERT INTO usr_to_group (workspace_id, usr, group_) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
&w_id,
&authed.username,
ng.name,
)
.execute(&mut tx)
.await?;
@@ -159,6 +250,16 @@ async fn get_group(
Extension(user_db): Extension<UserDB>,
Path((w_id, name)): Path<(String, String)>,
) -> JsonResult<GroupInfo> {
if *CLOUD_HOSTED && w_id == "demo" && name == "all" && !authed.is_admin {
return Ok(Json(GroupInfo {
workspace_id: w_id,
name: name,
summary: Some("The group that contains all users".to_string()),
members: vec!["redacted_in_demo_workspace".to_string()],
extra_perms: serde_json::json!({}),
}));
}
let mut tx = user_db.begin(&authed).await?;
let group = not_found_if_none(get_group_opt(&mut tx, &w_id, &name).await?, "Group", &name)?;
@@ -185,11 +286,15 @@ async fn get_group(
async fn delete_group(
authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path((w_id, name)): Path<(String, String)>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
if !authed.is_admin {
require_is_owner(&name, &authed.username, &authed.groups, &w_id, &db).await?;
}
not_found_if_none(get_group_opt(&mut tx, &w_id, &name).await?, "Group", &name)?;
sqlx::query!(
@@ -222,12 +327,15 @@ async fn delete_group(
async fn update_group(
authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path((w_id, name)): Path<(String, String)>,
Json(eg): Json<EditGroup>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
if !authed.is_admin {
require_is_owner(&name, &authed.username, &authed.groups, &w_id, &db).await?;
}
not_found_if_none(get_group_opt(&mut tx, &w_id, &name).await?, "Group", &name)?;
sqlx::query_as!(
@@ -256,17 +364,21 @@ async fn update_group(
async fn add_user(
authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path((w_id, name)): Path<(String, String)>,
Json(Username { username: user_username }): Json<Username>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
if !authed.is_admin {
require_is_owner(&name, &authed.username, &authed.groups, &w_id, &db).await?;
}
not_found_if_none(get_group_opt(&mut tx, &w_id, &name).await?, "Group", &name)?;
sqlx::query_as!(
Group,
"INSERT INTO usr_to_group (workspace_id, usr, group_) VALUES ($1, $2, $3)",
"INSERT INTO usr_to_group (workspace_id, usr, group_) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
&w_id,
user_username,
name,
@@ -290,11 +402,15 @@ async fn add_user(
async fn remove_user(
authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path((w_id, name)): Path<(String, String)>,
Json(Username { username: user_username }): Json<Username>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
if !authed.is_admin {
require_is_owner(&name, &authed.username, &authed.groups, &w_id, &db).await?;
}
not_found_if_none(get_group_opt(&mut tx, &w_id, &name).await?, "Group", &name)?;
if &name == "all" {

View File

@@ -10,16 +10,17 @@ use std::sync::Arc;
use anyhow::Context;
use axum::{
extract::{FromRequest, Path, Query},
extract::{FromRequest, Json, Path, Query},
response::{IntoResponse, Response},
routing::{get, post},
Extension, Json, Router,
Extension, Router,
};
use base64::Engine;
use hmac::Mac;
use hyper::StatusCode;
use hyper::{HeaderMap, Request, StatusCode};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use sql_builder::{prelude::*, quote, SqlBuilder};
use sqlx::{query_scalar, types::Uuid, Postgres, Transaction};
use sqlx::{query_scalar, types::Uuid, FromRow, Postgres, Transaction};
use urlencoding::encode;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
@@ -28,16 +29,16 @@ use windmill_common::{
flows::FlowValue,
oauth2::HmacSha256,
scripts::{ScriptHash, ScriptLang},
users::owner_to_token_owner,
users::username_to_permissioned_as,
utils::{not_found_if_none, now_from_db, paginate, require_admin, Pagination, StripPath},
};
use windmill_queue::{get_queued_job, push, JobKind, JobPayload, QueuedJob, RawCode};
use crate::{
db::{UserDB, DB},
users::Authed,
users::{require_owner_of_path, Authed},
variables::get_workspace_key,
BaseUrl,
BaseUrl, QueueLimitWaitResult, TimeoutWaitResult,
};
pub fn workspaced_service() -> Router {
@@ -62,8 +63,7 @@ pub fn workspaced_service() -> Router {
.route("/completed/get/:id", get(get_completed_job))
.route("/completed/get_result/:id", get(get_completed_job_result))
.route("/completed/delete/:id", post(delete_completed_job))
.route("/get/:id", get(get_job))
.route("/getupdate/:id", get(get_job_update))
.route("/flow/resume/:id", post(resume_suspended_flow_as_owner))
.route(
"/job_signature/:job_id/:resume_id",
get(create_job_signature),
@@ -94,6 +94,8 @@ pub fn global_service() -> Router {
"/get_flow/:job_id/:resume_id/:secret",
get(get_suspended_job_flow),
)
.route("/get/:id", get(get_job))
.route("/getupdate/:id", get(get_job_update))
}
async fn get_result_by_id(
@@ -101,7 +103,9 @@ async fn get_result_by_id(
Query(ResultByIdQuery { skip_direct }): Query<ResultByIdQuery>,
Path((w_id, flow_id, node_id)): Path<(String, String, String)>,
) -> windmill_common::error::JsonResult<serde_json::Value> {
tracing::error!("get_result_by_id_bef: {:?} {:?}", flow_id, node_id);
let res = windmill_queue::get_result_by_id(db, skip_direct, w_id, flow_id, node_id).await?;
tracing::error!("get_result_by_id: {:?}", res);
Ok(Json(res))
}
@@ -214,42 +218,59 @@ pub async fn get_job_by_id<'c>(
pub struct CompletedJob {
pub workspace_id: String,
pub id: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_job: Option<Uuid>,
pub created_by: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub started_at: chrono::DateTime<chrono::Utc>,
pub duration_ms: i32,
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub script_hash: Option<ScriptHash>,
#[serde(skip_serializing_if = "Option::is_none")]
pub script_path: Option<String>,
pub args: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logs: Option<String>,
pub deleted: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_code: Option<String>,
pub canceled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub canceled_by: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub canceled_reason: Option<String>,
pub job_kind: JobKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub schedule_path: Option<String>,
pub permissioned_as: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub flow_status: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_flow: Option<serde_json::Value>,
pub is_flow_step: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<ScriptLang>,
pub is_skipped: bool,
pub email: String,
pub visible_to_owner: bool,
}
#[derive(Deserialize, Clone, Copy)]
#[derive(Deserialize, Clone)]
pub struct RunJobQuery {
scheduled_for: Option<chrono::DateTime<chrono::Utc>>,
scheduled_in_secs: Option<i64>,
parent_job: Option<Uuid>,
include_header: Option<String>,
invisible_to_owner: Option<bool>,
queue_limit: Option<i64>,
}
impl RunJobQuery {
async fn get_scheduled_for<'c>(
self,
&self,
db: &mut Transaction<'c, Postgres>,
) -> error::Result<Option<chrono::DateTime<chrono::Utc>>> {
if let Some(scheduled_for) = self.scheduled_for {
@@ -261,6 +282,27 @@ impl RunJobQuery {
Ok(None)
}
}
fn add_include_headers(
&self,
headers: HeaderMap,
mut args: serde_json::Map<String, serde_json::Value>,
) -> serde_json::Map<String, serde_json::Value> {
self.include_header
.as_ref()
.map(|s| s.split(",").map(|s| s.to_string()).collect::<Vec<_>>())
.unwrap_or_default()
.iter()
.for_each(|h| {
if let Some(v) = headers.get(h) {
args.insert(
h.to_string().to_lowercase().replace('-', "_"),
serde_json::Value::String(v.to_str().unwrap().to_string()),
);
}
});
args
}
}
#[derive(Deserialize)]
@@ -275,6 +317,7 @@ pub struct ListQueueQuery {
pub parent_job: Option<String>,
pub order_desc: Option<bool>,
pub job_kinds: Option<String>,
pub suspended: Option<bool>,
}
fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> SqlBuilder {
@@ -309,6 +352,14 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
if let Some(dt) = &lq.created_after {
sqlb.and_where_gt("created_at", format!("to_timestamp({})", dt.timestamp()));
}
if let Some(s) = &lq.suspended {
if *s {
sqlb.and_where_gt("suspend", 0);
} else {
sqlb.and_where_eq("suspend", 0);
}
}
if let Some(jk) = &lq.job_kinds {
sqlb.and_where_in(
"job_kind",
@@ -319,13 +370,55 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
sqlb
}
#[derive(Serialize, FromRow)]
struct ListableQueuedJob {
pub id: Uuid,
pub created_by: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub started_at: Option<chrono::DateTime<chrono::Utc>>,
pub scheduled_for: chrono::DateTime<chrono::Utc>,
pub script_hash: Option<ScriptHash>,
pub script_path: Option<String>,
pub args: Option<serde_json::Value>,
pub job_kind: JobKind,
pub schedule_path: Option<String>,
pub is_flow_step: bool,
pub language: Option<ScriptLang>,
pub email: String,
pub suspend: Option<i32>,
}
async fn list_queue_jobs(
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Query(lq): Query<ListQueueQuery>,
) -> error::JsonResult<Vec<QueuedJob>> {
let sql = list_queue_jobs_query(&w_id, &lq, &["*"]).sql()?;
let jobs = sqlx::query_as::<_, QueuedJob>(&sql).fetch_all(&db).await?;
) -> error::JsonResult<Vec<ListableQueuedJob>> {
let sql = list_queue_jobs_query(
&w_id,
&lq,
&[
"id",
"created_by",
"created_at",
"started_at",
"scheduled_for",
"script_hash",
"script_path",
"args",
"job_kind",
"schedule_path",
"permissioned_as",
"is_flow_step",
"language",
"same_worker",
"email",
"suspend",
],
)
.sql()?;
let jobs = sqlx::query_as::<_, ListableQueuedJob>(&sql)
.fetch_all(&db)
.await?;
Ok(Json(jobs))
}
@@ -352,6 +445,7 @@ async fn list_jobs(
parent_job: lq.parent_job,
order_desc: Some(true),
job_kinds: lq.job_kinds,
suspended: lq.suspended,
},
&[
"'QueuedJob' as typ",
@@ -374,10 +468,12 @@ async fn list_jobs(
"job_kind",
"schedule_path",
"permissioned_as",
"flow_status",
"is_flow_step",
"language",
"false as is_skipped",
"email",
"visible_to_owner",
"suspend",
],
);
let sqlc = list_completed_jobs_query(
@@ -406,10 +502,12 @@ async fn list_jobs(
"job_kind",
"schedule_path",
"permissioned_as",
"flow_status",
"is_flow_step",
"language",
"is_skipped",
"email",
"visible_to_owner",
"null as suspend",
],
);
let sql = format!(
@@ -425,12 +523,41 @@ async fn list_jobs(
Ok(Json(jobs.into_iter().map(From::from).collect()))
}
pub async fn resume_suspended_flow_as_owner(
authed: Authed,
Extension(db): Extension<DB>,
Path((w_id, flow_id)): Path<(String, Uuid)>,
QueryOrBody(value): QueryOrBody<serde_json::Value>,
) -> error::Result<StatusCode> {
let value = value.unwrap_or(serde_json::Value::Null);
let mut tx = db.begin().await?;
let (flow, job_id) = get_suspended_flow_info(flow_id, &mut tx).await?;
if !authed.is_admin {
require_owner_of_path(
&w_id,
&authed.username,
&authed.groups,
&flow.script_path.clone().unwrap_or_else(|| String::new()),
&db,
)
.await?;
}
insert_resume_job(0, job_id, &flow, value, Some(authed.username), &mut tx).await?;
resume_immediately_if_relevant(flow, job_id, &mut tx).await?;
tx.commit().await?;
Ok(StatusCode::CREATED)
}
pub async fn resume_suspended_job(
/* unauthed */
Extension(db): Extension<DB>,
Path((w_id, job_id, resume_id, secret)): Path<(String, Uuid, u32, String)>,
QueryOrBody(value): QueryOrBody<serde_json::Value>,
Query(approver): Query<QueryApprover>,
QueryOrBody(value): QueryOrBody<serde_json::Value>,
) -> error::Result<StatusCode> {
let value = value.unwrap_or(serde_json::Value::Null);
let mut tx = db.begin().await?;
@@ -443,18 +570,7 @@ pub async fn resume_suspended_job(
}
mac.verify_slice(hex::decode(secret)?.as_ref())
.map_err(|_| anyhow::anyhow!("Invalid signature"))?;
let flow = sqlx::query!(
r#"
SELECT id, flow_status, suspend
FROM queue
WHERE id = ( SELECT parent_job FROM queue WHERE id = $1 UNION ALL SELECT parent_job FROM completed_job WHERE id = $1)
FOR UPDATE
"#,
job_id,
)
.fetch_optional(&mut tx)
.await?
.ok_or_else(|| anyhow::anyhow!("parent flow job not found"))?;
let flow = get_suspended_prent_flow_info(job_id, &mut tx).await?;
let exists = sqlx::query_scalar!(
r#"
@@ -470,6 +586,55 @@ pub async fn resume_suspended_job(
return Err(anyhow::anyhow!("resume request already sent").into());
}
insert_resume_job(resume_id, job_id, &flow, value, approver.approver, &mut tx).await?;
resume_immediately_if_relevant(flow, job_id, &mut tx).await?;
tx.commit().await?;
Ok(StatusCode::CREATED)
}
/* If the flow is currently waiting to be resumed (`FlowStatusModule::WaitingForEvents`)
* the suspend column must be set to the number of resume messages waited on.
*
* The flow's queue row is locked in this transaction because to avoid race conditions around
* the suspend column.
* That is, a job needs one event but it hasn't arrived, a worker counts zero events before
* entering WaitingForEvents. Then this message arrives but the job isn't in WaitingForEvents
* yet so the suspend counter isn't updated. Then the job enters WaitingForEvents expecting
* one event to arrive based on the count that is no longer correct. */
async fn resume_immediately_if_relevant<'c>(
flow: FlowInfo,
job_id: Uuid,
tx: &mut Transaction<'c, Postgres>,
) -> error::Result<()> {
Ok(
if let Some(suspend) = (0 < flow.suspend).then(|| flow.suspend - 1) {
let status =
serde_json::from_value::<FlowStatus>(flow.flow_status.context("no flow status")?)
.context("deserialize flow status")?;
if matches!(status.current_step(), Some(FlowStatusModule::WaitingForEvents { job, .. }) if job == &job_id)
{
sqlx::query!(
"UPDATE queue SET suspend = $1 WHERE id = $2",
suspend,
flow.id,
)
.execute(tx)
.await?;
}
},
)
}
async fn insert_resume_job<'c>(
resume_id: u32,
job_id: Uuid,
flow: &FlowInfo,
value: serde_json::Value,
approver: Option<String>,
tx: &mut Transaction<'c, Postgres>,
) -> error::Result<()> {
sqlx::query!(
r#"
INSERT INTO resume_job
@@ -481,38 +646,71 @@ pub async fn resume_suspended_job(
job_id,
flow.id,
value,
approver.approver
approver
)
.execute(&mut tx)
.execute(tx)
.await?;
Ok(())
}
/* If the flow is currently waiting to be resumed (`FlowStatusModule::WaitingForEvents`)
* the suspend column must be set to the number of resume messages waited on.
*
* The flow's queue row is locked in this transaction because to avoid race conditions around
* the suspend column.
* That is, a job needs one event but it hasn't arrived, a worker counts zero events before
* entering WaitingForEvents. Then this message arrives but the job isn't in WaitingForEvents
* yet so the suspend counter isn't updated. Then the job enters WaitingForEvents expecting
* one event to arrive based on the count that is no longer correct. */
if let Some(suspend) = (0 < flow.suspend).then(|| flow.suspend - 1) {
let status =
serde_json::from_value::<FlowStatus>(flow.flow_status.context("no flow status")?)
.context("deserialize flow status")?;
if matches!(status.current_step(), Some(FlowStatusModule::WaitingForEvents { job, .. }) if job == &job_id)
{
sqlx::query!(
"UPDATE queue SET suspend = $1 WHERE id = $2",
suspend,
flow.id,
)
.execute(&mut tx)
.await?;
}
#[derive(sqlx::FromRow)]
struct FlowInfo {
id: Uuid,
flow_status: Option<serde_json::Value>,
suspend: i32,
script_path: Option<String>,
}
async fn get_suspended_prent_flow_info<'c>(
job_id: Uuid,
tx: &mut Transaction<'c, Postgres>,
) -> error::Result<FlowInfo> {
let flow = sqlx::query_as!(
FlowInfo,
r#"
SELECT id, flow_status, suspend, script_path
FROM queue
WHERE id = ( SELECT parent_job FROM queue WHERE id = $1 UNION ALL SELECT parent_job FROM completed_job WHERE id = $1)
FOR UPDATE
"#,
job_id,
)
.fetch_optional(tx)
.await?
.ok_or_else(|| anyhow::anyhow!("parent flow job not found"))?;
Ok(flow)
}
async fn get_suspended_flow_info<'c>(
job_id: Uuid,
tx: &mut Transaction<'c, Postgres>,
) -> error::Result<(FlowInfo, Uuid)> {
let flow = sqlx::query_as!(
FlowInfo,
r#"
SELECT id, flow_status, suspend, script_path
FROM queue
WHERE id = $1
"#,
job_id,
)
.fetch_optional(tx)
.await?
.ok_or_else(|| anyhow::anyhow!("parent flow job not found"))?;
let job_id = flow
.flow_status
.as_ref()
.and_then(|v| serde_json::from_value::<FlowStatus>(v.clone()).ok())
.and_then(|s| match s.modules.get(s.step as usize) {
Some(FlowStatusModule::WaitingForEvents { job, .. }) => Some(job.to_owned()),
_ => None,
});
if let Some(job_id) = job_id {
Ok((flow, job_id))
} else {
Err(anyhow::anyhow!("the flow is not in a suspended state anymore").into())
}
tx.commit().await?;
Ok(StatusCode::CREATED)
}
pub async fn cancel_suspended_job(
@@ -702,7 +900,7 @@ fn build_resume_url(
approver: &str,
base_url: &str,
) -> String {
format!("{base_url}/api/w/{w_id}/jobs/{op}/{job_id}/{resume_id}/{signature}{approver}")
format!("{base_url}/api/w/{w_id}/jobs_u/{op}/{job_id}/{resume_id}/{signature}{approver}")
}
pub async fn get_resume_urls(
@@ -781,10 +979,12 @@ struct UnifiedJob {
job_kind: JobKind,
schedule_path: Option<String>,
permissioned_as: String,
flow_status: Option<serde_json::Value>,
is_flow_step: bool,
language: Option<ScriptLang>,
is_skipped: bool,
email: String,
visible_to_owner: bool,
suspend: Option<i32>,
}
impl From<UnifiedJob> for Job {
@@ -804,6 +1004,7 @@ impl From<UnifiedJob> for Job {
args: uj.args,
result: None,
logs: None,
flow_status: None,
deleted: uj.deleted,
canceled: uj.canceled,
canceled_by: uj.canceled_by,
@@ -812,11 +1013,12 @@ impl From<UnifiedJob> for Job {
job_kind: uj.job_kind,
schedule_path: uj.schedule_path,
permissioned_as: uj.permissioned_as,
flow_status: uj.flow_status,
raw_flow: None,
is_flow_step: uj.is_flow_step,
language: uj.language,
is_skipped: uj.is_skipped,
email: uj.email,
visible_to_owner: uj.visible_to_owner,
}),
"QueuedJob" => Job::QueuedJob(QueuedJob {
workspace_id: uj.workspace_id,
@@ -831,6 +1033,7 @@ impl From<UnifiedJob> for Job {
running: uj.running.unwrap(),
scheduled_for: uj.scheduled_for.unwrap(),
logs: None,
flow_status: None,
raw_code: None,
raw_lock: None,
canceled: uj.canceled,
@@ -840,11 +1043,14 @@ impl From<UnifiedJob> for Job {
job_kind: uj.job_kind,
schedule_path: uj.schedule_path,
permissioned_as: uj.permissioned_as,
flow_status: uj.flow_status,
raw_flow: None,
is_flow_step: uj.is_flow_step,
language: uj.language,
same_worker: false,
pre_run_error: None,
email: uj.email,
visible_to_owner: uj.visible_to_owner,
suspend: uj.suspend,
}),
t => panic!("job type {} not valid", t),
}
@@ -873,20 +1079,19 @@ struct PreviewFlow {
pub struct QueryOrBody<D>(pub Option<D>);
#[axum::async_trait]
impl<D, B> FromRequest<B> for QueryOrBody<D>
impl<S, D> FromRequest<S, axum::body::Body> for QueryOrBody<D>
where
D: DeserializeOwned,
B: Send + axum::body::HttpBody,
<B as axum::body::HttpBody>::Data: Send,
<B as axum::body::HttpBody>::Error: Into<axum::BoxError>,
S: Send + Sync,
{
type Rejection = Response;
async fn from_request(
req: &mut axum::extract::RequestParts<B>,
req: Request<axum::body::Body>,
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
return if req.method() == axum::http::Method::GET {
let Query(InPayload { payload }) = Query::from_request(req)
let Query(InPayload { payload }) = Query::from_request(req, state)
.await
.map_err(IntoResponse::into_response)?;
payload
@@ -898,7 +1103,7 @@ where
})
.unwrap_or(Ok(QueryOrBody(None)))
} else {
Json::from_request(req)
Json::from_request(req, state)
.await
.map(|Json(v)| QueryOrBody(Some(v)))
.map_err(IntoResponse::into_response)
@@ -910,7 +1115,9 @@ where
}
fn decode_payload<D: DeserializeOwned, T: AsRef<[u8]>>(t: T) -> anyhow::Result<D> {
let vec = base64::decode_config(&t, base64::URL_SAFE).context("invalid base64")?;
let vec = base64::engine::general_purpose::URL_SAFE
.decode(t)
.context("invalid base64")?;
serde_json::from_slice(vec.as_slice()).context("invalid json")
}
}
@@ -919,24 +1126,30 @@ pub async fn run_flow_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path((w_id, flow_path)): Path<(String, StripPath)>,
axum::Json(args): axum::Json<Option<serde_json::Map<String, serde_json::Value>>>,
Query(run_query): Query<RunJobQuery>,
headers: HeaderMap,
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
) -> error::Result<(StatusCode, String)> {
let flow_path = flow_path.to_path();
let mut tx = user_db.begin(&authed).await?;
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let (uuid, tx) = push(
tx,
&w_id,
JobPayload::Flow(flow_path.to_string()),
args,
&authed.username,
owner_to_token_owner(&authed.username, false),
&authed.email,
username_to_permissioned_as(&authed.username),
scheduled_for,
None,
run_query.parent_job,
false,
false,
None,
!run_query.invisible_to_owner.unwrap_or(false),
)
.await?;
tx.commit().await?;
@@ -947,13 +1160,15 @@ pub async fn run_job_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path((w_id, script_path)): Path<(String, StripPath)>,
axum::Json(args): axum::Json<Option<serde_json::Map<String, serde_json::Value>>>,
Query(run_query): Query<RunJobQuery>,
headers: HeaderMap,
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
) -> error::Result<(StatusCode, String)> {
let script_path = script_path.to_path();
let mut tx = user_db.begin(&authed).await?;
let job_payload = script_path_to_payload(script_path, &mut tx, &w_id).await?;
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let (uuid, tx) = push(
tx,
@@ -961,28 +1176,81 @@ pub async fn run_job_by_path(
job_payload,
args,
&authed.username,
owner_to_token_owner(&authed.username, false),
&authed.email,
username_to_permissioned_as(&authed.username),
scheduled_for,
None,
run_query.parent_job,
false,
false,
None,
!run_query.invisible_to_owner.unwrap_or(false),
)
.await?;
tx.commit().await?;
Ok((StatusCode::CREATED, uuid.to_string()))
}
struct Guard {
done: bool,
id: Uuid,
w_id: String,
db: UserDB,
authed: Authed,
}
impl Drop for Guard {
fn drop(&mut self) {
if !&self.done {
let id = self.id;
let username = self.authed.username.clone();
let w_id = self.w_id.clone();
let db = self.db.clone();
let authed = self.authed.clone();
tracing::info!("http connection broke, marking job {id} as canceled");
tokio::spawn(async move {
let tx = db.begin(&authed).await.ok();
if let Some(mut tx) = tx {
let _ = sqlx::query!(
"UPDATE queue SET canceled = true, canceled_reason = 'http connection broke', canceled_by = $1 WHERE id = $2 AND workspace_id = $3",
username,
id,
w_id
)
.execute(&mut tx)
.await;
let _ = tx.commit().await;
}
});
}
}
}
async fn run_wait_result<T>(
authed: Authed,
Extension(user_db): Extension<UserDB>,
timeout: i32,
uuid: Uuid,
Path((w_id, _)): Path<(String, T)>,
) -> error::JsonResult<serde_json::Value> {
let mut result = None;
for i in 0..48 {
let iters = if timeout <= 0 {
20
} else if timeout <= 1 {
timeout * 10
} else {
10 + ((timeout - 1) * 2)
};
let mut g = Guard {
done: false,
id: uuid,
w_id: w_id.clone(),
db: user_db.clone(),
authed: authed.clone(),
};
for i in 0..iters {
let mut tx = user_db.clone().begin(&authed).await?;
result = sqlx::query_scalar!(
"SELECT result FROM completed_job WHERE id = $1 AND workspace_id = $2",
uuid,
@@ -991,6 +1259,7 @@ async fn run_wait_result<T>(
.fetch_optional(&mut tx)
.await?
.flatten();
drop(tx);
if result.is_some() {
break;
@@ -999,54 +1268,98 @@ async fn run_wait_result<T>(
tokio::time::sleep(core::time::Duration::from_millis(delay)).await;
}
if let Some(result) = result {
g.done = true;
Ok(Json(result))
} else {
Err(Error::ExecutionErr("timeout after 20s".to_string()))
Err(Error::ExecutionErr(format!("timeout after {}s", timeout)))
}
}
pub async fn check_queue_too_long(db: DB, queue_limit: Option<i64>) -> error::Result<()> {
if let Some(limit) = queue_limit {
let count = sqlx::query_scalar!(
"SELECT COUNT(*) FROM queue WHERE canceled = false AND (scheduled_for <= now()
OR (suspend_until IS NOT NULL
AND ( suspend <= 0
OR suspend_until <= now())))",
)
.fetch_one(&db)
.await?
.unwrap_or(0);
if count > queue_limit.unwrap() {
return Err(Error::InternalErr(format!(
"Number of queued job is too high: {count} > {limit}"
)));
}
}
Ok(())
}
pub async fn run_wait_result_job_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Extension(timeout): Extension<Arc<TimeoutWaitResult>>,
Extension(queue_limit): Extension<Arc<QueueLimitWaitResult>>,
Path((w_id, script_path)): Path<(String, StripPath)>,
axum::Json(args): axum::Json<Option<serde_json::Map<String, serde_json::Value>>>,
Query(run_query): Query<RunJobQuery>,
headers: HeaderMap,
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
) -> error::JsonResult<serde_json::Value> {
check_queue_too_long(db, queue_limit.0.or(run_query.queue_limit)).await?;
let script_path = script_path.to_path();
let mut tx = user_db.clone().begin(&authed).await?;
let job_payload = script_path_to_payload(script_path, &mut tx, &w_id).await?;
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let (uuid, tx) = push(
tx,
&w_id,
job_payload,
args,
&authed.username,
owner_to_token_owner(&authed.username, false),
&authed.email,
username_to_permissioned_as(&authed.username),
scheduled_for,
None,
run_query.parent_job,
false,
false,
None,
!run_query.invisible_to_owner.unwrap_or(false),
)
.await?;
tx.commit().await?;
run_wait_result(authed, Extension(user_db), uuid, Path((w_id, script_path))).await
run_wait_result(
authed,
Extension(user_db),
timeout.0,
uuid,
Path((w_id, script_path)),
)
.await
}
pub async fn run_wait_result_job_by_hash(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Extension(timeout): Extension<Arc<TimeoutWaitResult>>,
Path((w_id, script_hash)): Path<(String, ScriptHash)>,
axum::Json(args): axum::Json<Option<serde_json::Map<String, serde_json::Value>>>,
Query(run_query): Query<RunJobQuery>,
headers: HeaderMap,
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
) -> error::JsonResult<serde_json::Value> {
check_queue_too_long(db, run_query.queue_limit).await?;
let hash = script_hash.0;
let mut tx = user_db.clone().begin(&authed).await?;
let path = get_path_for_hash(&mut tx, &w_id, hash).await?;
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let (uuid, tx) = push(
tx,
@@ -1054,17 +1367,27 @@ pub async fn run_wait_result_job_by_hash(
JobPayload::ScriptHash { hash: ScriptHash(hash), path },
args,
&authed.username,
owner_to_token_owner(&authed.username, false),
&authed.email,
username_to_permissioned_as(&authed.username),
scheduled_for,
None,
run_query.parent_job,
false,
false,
None,
!run_query.invisible_to_owner.unwrap_or(false),
)
.await?;
tx.commit().await?;
run_wait_result(authed, Extension(user_db), uuid, Path((w_id, script_hash))).await
run_wait_result(
authed,
Extension(user_db),
timeout.0,
uuid,
Path((w_id, script_hash)),
)
.await
}
// a similar function exists on the worker
@@ -1086,11 +1409,13 @@ async fn run_preview_job(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(run_query): Query<RunJobQuery>,
headers: HeaderMap,
Json(preview): Json<Preview>,
Query(sch_query): Query<RunJobQuery>,
) -> error::Result<(StatusCode, String)> {
let mut tx = user_db.begin(&authed).await?;
let scheduled_for = sch_query.get_scheduled_for(&mut tx).await?;
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
let args = run_query.add_include_headers(headers, preview.args.unwrap_or_default());
let (uuid, tx) = push(
tx,
@@ -1101,14 +1426,17 @@ async fn run_preview_job(
language: preview.language,
lock: None,
}),
preview.args,
args,
&authed.username,
owner_to_token_owner(&authed.username, false),
&authed.email,
username_to_permissioned_as(&authed.username),
scheduled_for,
None,
None,
false,
false,
None,
true,
)
.await?;
tx.commit().await?;
@@ -1119,23 +1447,29 @@ async fn run_preview_flow_job(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(run_query): Query<RunJobQuery>,
headers: HeaderMap,
Json(raw_flow): Json<PreviewFlow>,
Query(sch_query): Query<RunJobQuery>,
) -> error::Result<(StatusCode, String)> {
let mut tx = user_db.begin(&authed).await?;
let scheduled_for = sch_query.get_scheduled_for(&mut tx).await?;
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
let args = run_query.add_include_headers(headers, raw_flow.args.unwrap_or_default());
let (uuid, tx) = push(
tx,
&w_id,
JobPayload::RawFlow { value: raw_flow.value, path: raw_flow.path },
raw_flow.args,
args,
&authed.username,
owner_to_token_owner(&authed.username, false),
&authed.email,
username_to_permissioned_as(&authed.username),
scheduled_for,
None,
None,
false,
false,
None,
true,
)
.await?;
tx.commit().await?;
@@ -1146,13 +1480,15 @@ pub async fn run_job_by_hash(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path((w_id, script_hash)): Path<(String, ScriptHash)>,
axum::Json(args): axum::Json<Option<serde_json::Map<String, serde_json::Value>>>,
Query(run_query): Query<RunJobQuery>,
headers: HeaderMap,
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
) -> error::Result<(StatusCode, String)> {
let hash = script_hash.0;
let mut tx = user_db.begin(&authed).await?;
let path = get_path_for_hash(&mut tx, &w_id, hash).await?;
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let (uuid, tx) = push(
tx,
@@ -1160,12 +1496,15 @@ pub async fn run_job_by_hash(
JobPayload::ScriptHash { hash: ScriptHash(hash), path },
args,
&authed.username,
owner_to_token_owner(&authed.username, false),
&authed.email,
username_to_permissioned_as(&authed.username),
scheduled_for,
None,
run_query.parent_job,
false,
false,
None,
!run_query.invisible_to_owner.unwrap_or(false),
)
.await?;
tx.commit().await?;
@@ -1296,7 +1635,9 @@ pub struct ListCompletedQuery {
pub job_kinds: Option<String>,
pub is_skipped: Option<bool>,
pub is_flow_step: Option<bool>,
pub suspended: Option<bool>,
}
async fn list_completed_jobs(
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
@@ -1337,6 +1678,8 @@ async fn list_completed_jobs(
"is_flow_step",
"language",
"is_skipped",
"email",
"visible_to_owner",
],
)
.sql()?;
@@ -1387,7 +1730,7 @@ async fn delete_completed_job(
require_admin(authed.is_admin, &authed.username)?;
let job_o = sqlx::query_as::<_, CompletedJob>(
"UPDATE completed_job SET logs = '', deleted = true WHERE id = $1 AND workspace_id = $2 \
"UPDATE completed_job SET logs = '', result = null, deleted = true WHERE id = $1 AND workspace_id = $2 \
RETURNING *",
)
.bind(id)

View File

@@ -7,7 +7,7 @@
*/
use argon2::Argon2;
use axum::{handler::Handler, middleware::from_extractor, routing::get, Extension, Router};
use axum::{middleware::from_extractor, routing::get, Extension, Router};
use db::DB;
use git_version::git_version;
use std::{net::SocketAddr, sync::Arc};
@@ -21,6 +21,7 @@ use crate::{
oauth2::{build_oauth_clients, SlackVerifier},
tracing_init::{MyMakeSpan, MyOnResponse},
users::{Authed, OptAuthed},
webhook_util::{WebhookShared, WebhookUtil},
};
mod apps;
@@ -29,6 +30,7 @@ mod capture;
mod db;
mod favorite;
mod flows;
mod folders;
mod granular_acls;
mod groups;
pub mod jobs;
@@ -41,16 +43,20 @@ mod tracing_init;
mod users;
mod utils;
mod variables;
mod webhook_util;
mod worker_ping;
mod workspaces;
const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
pub const GIT_VERSION: &str =
git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
pub struct BaseUrl(String);
pub struct IsSecure(bool);
pub struct CookieDomain(Option<String>);
pub struct CloudHosted(bool);
pub struct ContentSecurityPolicy(String);
pub struct TimeoutWaitResult(i32);
pub struct QueueLimitWaitResult(Option<i64>);
pub use users::delete_expired_items_perdiodically;
@@ -62,7 +68,10 @@ pub async fn run_server(
) -> anyhow::Result<()> {
let user_db = UserDB::new(db.clone());
let auth_cache = Arc::new(users::AuthCache::new(db.clone()));
let auth_cache = Arc::new(users::AuthCache::new(
db.clone(),
std::env::var("SUPERADMIN_SECRET").ok(),
));
let argon2 = Arc::new(Argon2::default());
let basic_clients = Arc::new(build_oauth_clients(&base_url).await?);
let slack_verifier = Arc::new(
@@ -86,20 +95,21 @@ pub async fn run_server(
.layer(Extension(auth_cache.clone()))
.layer(Extension(basic_clients))
.layer(Extension(Arc::new(BaseUrl(base_url.to_string()))))
.layer(Extension(Arc::new(ContentSecurityPolicy(
std::env::var("SERVE_CSP").unwrap_or("".to_owned()),
))))
.layer(Extension(Arc::new(CloudHosted(
std::env::var("CLOUD_HOSTED").is_ok(),
))))
.layer(Extension(Arc::new(IsSecure(
base_url.starts_with("https://"),
))))
.layer(Extension(Arc::new(ContentSecurityPolicy(
std::env::var("SERVE_CSP").unwrap_or("".to_owned()),
))))
.layer(Extension(Arc::new(CookieDomain(
std::env::var("COOKIE_DOMAIN").ok(),
))))
.layer(Extension(http_client))
.layer(CookieManagerLayer::new());
.layer(CookieManagerLayer::new())
.layer(Extension(WebhookShared::new(rx.resubscribe())));
// build our application with a route
let app = Router::new()
.nest(
@@ -109,7 +119,21 @@ pub async fn run_server(
"/w/:workspace_id",
Router::new()
.nest("/scripts", scripts::workspaced_service())
.nest("/jobs", jobs::workspaced_service())
.nest(
"/jobs",
jobs::workspaced_service()
.layer(Extension(Arc::new(TimeoutWaitResult(
std::env::var("TIMEOUT_WAIT_RESULT")
.ok()
.and_then(|x| x.parse().ok())
.unwrap_or(20),
))))
.layer(Extension(Arc::new(QueueLimitWaitResult(
std::env::var("QUEUE_LIMIT_WAIT_RESULT")
.ok()
.and_then(|x| x.parse().ok()),
)))),
)
.nest(
"/users",
users::workspaced_service().layer(Extension(argon2.clone())),
@@ -122,10 +146,12 @@ pub async fn run_server(
.nest("/audit", audit::workspaced_service())
.nest("/acls", granular_acls::workspaced_service())
.nest("/workspaces", workspaces::workspaced_service())
.nest("/apps", apps::workspaced_service())
.nest("/flows", flows::workspaced_service())
.nest("/capture", capture::workspaced_service())
.nest("/apps", apps::workspaced_service())
.nest("/favorites", favorite::workspaced_service()),
.nest("/favorites", favorite::workspaced_service())
.nest("/folders", folders::workspaced_service())
.route_layer(from_extractor::<WebhookUtil>()),
)
.nest("/workspaces", workspaces::global_service())
.nest(
@@ -135,15 +161,16 @@ pub async fn run_server(
.nest("/workers", worker_ping::global_service())
.nest("/scripts", scripts::global_service())
.nest("/flows", flows::global_service())
.nest("/apps", apps::global_service())
.nest("/schedules", schedule::global_service())
.route_layer(from_extractor::<Authed>())
.route_layer(from_extractor::<users::Tokened>())
.nest(
"/w/:workspace_id/apps",
"/w/:workspace_id/apps_u",
apps::unauthed_service().layer(from_extractor::<OptAuthed>()),
)
.nest("/w/:workspace_id/jobs", jobs::global_service())
.nest("/w/:workspace_id/capture", capture::global_service())
.nest("/w/:workspace_id/jobs_u", jobs::global_service())
.nest("/w/:workspace_id/capture_u", capture::global_service())
.nest(
"/auth",
users::make_unauthed_service().layer(Extension(argon2)),
@@ -155,7 +182,7 @@ pub async fn run_server(
.route("/version", get(git_v))
.route("/openapi.yaml", get(openapi)),
)
.fallback(static_assets::static_handler.into_service())
.fallback(static_assets::static_handler)
.layer(middleware_stack);
let instance_name = rd_string(5);
@@ -179,7 +206,7 @@ async fn git_v() -> &'static str {
}
async fn openapi() -> &'static str {
include_str!("../openapi.yaml")
include_str!("../openapi-deref.yaml")
}
pub async fn migrate_db(db: &DB) -> anyhow::Result<()> {
db::migrate(db).await?;

View File

@@ -14,7 +14,7 @@ use anyhow::Ok;
async fn main() -> anyhow::Result<()> {
windmill_common::tracing_init::initialize_tracing();
let db = windmill_common::connect_db().await?;
let db = windmill_common::connect_db(true).await?;
let num_workers = std::env::var("NUM_WORKERS")
.ok()

View File

@@ -11,10 +11,12 @@ use std::{collections::HashMap, fmt::Debug};
use std::sync::Arc;
use anyhow::Context;
use axum::extract::FromRequestParts;
use axum::http::request::Parts;
use axum::{
async_trait,
body::Bytes,
extract::{Extension, FromRequest, Path, Query, RequestParts},
extract::{Extension, Path, Query},
response::Redirect,
routing::{get, post},
Json, Router,
@@ -32,7 +34,8 @@ use tower_cookies::{Cookie, Cookies};
use windmill_audit::{audit_log, ActionKind};
use windmill_common::utils::{not_found_if_none, now_from_db};
use crate::users::Authed;
use crate::users::{truncate_token, Authed};
use crate::workspaces::invite_user_to_all_auto_invite_worspaces;
use crate::{
db::{UserDB, DB},
variables::{build_crypt, encrypt},
@@ -76,6 +79,7 @@ pub struct ClientWithScopes {
client: OClient,
scopes: Vec<String>,
extra_params: Option<HashMap<String, String>>,
extra_params_callback: Option<HashMap<String, String>>,
allowed_domains: Option<Vec<String>>,
userinfo_url: Option<String>,
}
@@ -89,6 +93,8 @@ pub struct OAuthConfig {
userinfo_url: Option<String>,
scopes: Option<Vec<String>>,
extra_params: Option<HashMap<String, String>>,
extra_params_callback: Option<HashMap<String, String>>,
req_body_auth: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -158,6 +164,7 @@ pub async fn build_oauth_clients(base_url: &str) -> anyhow::Result<AllClients> {
client: named_client.1,
scopes: config.scopes.unwrap_or(vec![]),
extra_params: config.extra_params,
extra_params_callback: config.extra_params_callback,
allowed_domains: client_params.allowed_domains.clone(),
userinfo_url: config.userinfo_url,
},
@@ -188,6 +195,7 @@ pub async fn build_oauth_clients(base_url: &str) -> anyhow::Result<AllClients> {
client: named_client.1,
scopes: config.scopes.unwrap_or(vec![]),
extra_params: config.extra_params,
extra_params_callback: config.extra_params_callback,
allowed_domains: None,
userinfo_url: None,
},
@@ -204,6 +212,8 @@ pub async fn build_oauth_clients(base_url: &str) -> anyhow::Result<AllClients> {
userinfo_url: None,
scopes: None,
extra_params: None,
extra_params_callback: None,
req_body_auth: None,
},
v.clone(),
false,
@@ -236,6 +246,9 @@ pub fn build_basic_client(
};
let mut client = OClient::new(client_params.id, auth_url, token_url);
if config.req_body_auth.unwrap_or(false) {
client.set_auth_type(AuthType::RequestBody);
}
client.set_client_secret(client_params.secret.clone());
client.set_redirect_url(Url::parse(&redirect_url).expect("Invalid redirect URL"));
// Set up the config for the Github OAuth2 process.
@@ -304,7 +317,7 @@ async fn connect(
struct CreateAccount {
client: String,
owner: String,
refresh_token: String,
refresh_token: Option<String>,
expires_in: i64,
}
async fn create_account(
@@ -474,10 +487,10 @@ struct VariablePath {
async fn refresh_token(
authed: Authed,
Path((w_id, id)): Path<(String, i32)>,
Json(VariablePath { path }): Json<VariablePath>,
Extension(user_db): Extension<UserDB>,
Extension(clients): Extension<Arc<AllClients>>,
Extension(http_client): Extension<Client>,
Json(VariablePath { path }): Json<VariablePath>,
) -> error::Result<String> {
let tx = user_db.begin(&authed).await?;
@@ -595,19 +608,20 @@ pub struct OAuthCallback {
async fn connect_callback(
cookies: Cookies,
Path(client_name): Path<String>,
Json(callback): Json<OAuthCallback>,
Extension(clients): Extension<Arc<AllClients>>,
Extension(http_client): Extension<Client>,
Json(callback): Json<OAuthCallback>,
) -> error::JsonResult<TokenResponse> {
let client = (&clients
let client_w_scopes = &clients
.connects
.get(&client_name)
.ok_or_else(|| error::Error::BadRequest("invalid client".to_string()))?
.client)
.to_owned();
.ok_or_else(|| error::Error::BadRequest("invalid client".to_string()))?;
let client = client_w_scopes.client.to_owned();
let extra_params = client_w_scopes.extra_params_callback.clone();
let token_response =
exchange_code::<TokenResponse>(callback, &cookies, client, &http_client).await?;
exchange_code::<TokenResponse>(callback, &cookies, client, &http_client, extra_params)
.await?;
Ok(Json(token_response))
}
@@ -616,10 +630,10 @@ async fn connect_slack_callback(
Path(w_id): Path<String>,
authed: Authed,
cookies: Cookies,
Json(callback): Json<OAuthCallback>,
Extension(user_db): Extension<UserDB>,
Extension(clients): Extension<Arc<AllClients>>,
Extension(http_client): Extension<Client>,
Json(callback): Json<OAuthCallback>,
) -> error::Result<String> {
let client = clients
.slack
@@ -627,35 +641,35 @@ async fn connect_slack_callback(
.ok_or_else(|| error::Error::BadRequest("slack client not setup".to_string()))?
.to_owned();
let token =
exchange_code::<SlackTokenResponse>(callback, &cookies, client, &http_client).await?;
exchange_code::<SlackTokenResponse>(callback, &cookies, client, &http_client, None).await?;
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
"INSERT INTO workspace_settings
(workspace_id, slack_team_id, slack_name)
VALUES ($1, $2, $3) ON CONFLICT (workspace_id) DO UPDATE SET slack_team_id = $2, \
slack_name = $3",
(workspace_id, slack_team_id, slack_name, slack_email)
VALUES ($1, $2, $3, $4) ON CONFLICT (workspace_id) DO UPDATE SET slack_team_id = $2, \
slack_name = $3, slack_email = $4",
&w_id,
token.team_id,
token.team_name
token.team_name,
authed.email
)
.execute(&mut tx)
.await?;
sqlx::query!(
"INSERT INTO group_
(workspace_id, name, summary)
VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
"INSERT INTO folder
(workspace_id, name, owners, extra_perms)
VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING",
&w_id,
"slack",
"The group that runs the script triggered by the slack /windmill command.
Share scripts to this group to make them executable from slack and add
members to this group to let them manage the slack related owner space."
"slack_bot",
&[],
serde_json::json!({})
)
.execute(&mut tx)
.await?;
let token_path = "g/slack/bot_token";
let token_path = "f/slack_bot/bot_token";
let mc = build_crypt(&mut tx, &w_id).await?;
let value = encrypt(&mc, &token.bot.bot_access_token);
sqlx::query!(
@@ -705,14 +719,17 @@ pub struct SlackSig {
}
#[async_trait]
impl<B> FromRequest<B> for SlackSig
impl<S> FromRequestParts<S> for SlackSig
where
B: Send,
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request(req: &mut RequestParts<B>) -> std::result::Result<Self, Self::Rejection> {
let hm = req.headers();
async fn from_request_parts(
parts: &mut Parts,
_state: &S,
) -> std::result::Result<Self, Self::Rejection> {
let hm = &parts.headers;
Ok(Self {
sig: hm
.get("X-Slack-Signature")
@@ -783,14 +800,17 @@ async fn slack_command(
tx,
&settings.workspace_id,
payload,
Some(map),
map,
&form.user_name,
&settings.slack_email,
"g/slack".to_string(),
None,
None,
None,
false,
false,
None,
true,
)
.await?;
tx.commit().await?;
@@ -808,7 +828,7 @@ async fn slack_command(
}
#[allow(non_snake_case)]
#[derive(Deserialize)]
#[derive(Deserialize, Debug)]
pub struct UserInfo {
email: Option<String>,
name: Option<String>,
@@ -818,20 +838,21 @@ pub struct UserInfo {
async fn login_callback(
Path(client_name): Path<String>,
Json(callback): Json<OAuthCallback>,
cookies: Cookies,
Extension(clients): Extension<Arc<AllClients>>,
Extension(db): Extension<DB>,
Extension(http_client): Extension<Client>,
Extension(is_secure): Extension<Arc<IsSecure>>,
Extension(cookie_domain): Extension<Arc<CookieDomain>>,
Json(callback): Json<OAuthCallback>,
) -> error::Result<String> {
let client_w_config = &clients
.logins
.get(&client_name)
.ok_or_else(|| error::Error::BadRequest("invalid client".to_string()))?;
let client = client_w_config.client.to_owned();
let token_res = exchange_code::<TokenResponse>(callback, &cookies, client, &http_client).await;
let token_res =
exchange_code::<TokenResponse>(callback, &cookies, client, &http_client, None).await;
if let Ok(token) = token_res {
let token = &token.access_token.to_string();
@@ -893,6 +914,16 @@ async fn login_callback(
login type {login_type}"
)));
}
audit_log(
&mut tx,
&email,
"oauth.login",
ActionKind::Create,
"global",
Some(&truncate_token(&token)),
None,
)
.await?;
} else {
let mut name = user.name;
if name.is_none() || name == Some(String::new()) {
@@ -908,6 +939,9 @@ async fn login_callback(
.bind(user.company)
.execute(&mut tx)
.await?;
tx.commit().await?;
invite_user_to_all_auto_invite_worspaces(&db, &email).await?;
tx = db.begin().await?;
crate::users::create_session_token(
&email,
false,
@@ -962,6 +996,7 @@ async fn exchange_code<T: DeserializeOwned>(
cookies: &Cookies,
client: OClient,
http_client: &Client,
extra_params: Option<HashMap<String, String>>,
) -> error::Result<T> {
let csrf_state = cookies
.get("csrf")
@@ -971,8 +1006,15 @@ async fn exchange_code<T: DeserializeOwned>(
return Err(error::Error::BadRequest("csrf did not match".to_string()));
}
client
.exchange_code(callback.code)
let mut token_url = client.exchange_code(callback.code);
if let Some(extra_params) = extra_params {
for (key, value) in extra_params {
token_url = token_url.param(key, value)
}
}
token_url
.with_client(http_client)
.execute::<T>()
.await
@@ -991,13 +1033,30 @@ async fn http_get_user_info<T: DeserializeOwned>(
url: &str,
token: &str,
) -> error::Result<T> {
Ok(http_client
let res = http_client
.get(url)
.bearer_auth(token)
.send()
.await
.map_err(to_anyhow)
.context("failed to fetch user info")?
.context("failed to fetch user info")?;
if !res.status().is_success() {
tracing::debug!(
"The bearer token of the failed oauth user info exchange is: {}",
token
);
return Err(error::Error::BadConfig(format!(
"The user info endpoint responded with non 200: {}\n{}\n{}",
res.status(),
res.headers()
.iter()
.map(|x| format!("{}: {}", x.0.as_str(), x.1.to_str().unwrap_or_default()))
.collect::<Vec<_>>()
.join("\n"),
res.text().await.unwrap_or_default(),
)));
}
Ok(res
.json::<T>()
.await
.map_err(to_anyhow)

View File

@@ -8,7 +8,8 @@
use crate::{
db::{UserDB, DB},
users::Authed,
users::{require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookUtil},
};
use axum::{
extract::{Extension, Path, Query},
@@ -18,7 +19,7 @@ use axum::{
use hyper::StatusCode;
use serde::{Deserialize, Serialize};
use sql_builder::{bind::Bind, SqlBuilder};
use sqlx::FromRow;
use sqlx::{FromRow, Postgres, Transaction};
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{Error, JsonResult, Result},
@@ -32,6 +33,7 @@ pub fn workspaced_service() -> Router {
.route("/exists/*path", get(exists_resource))
.route("/get_value/*path", get(get_resource_value))
.route("/update/*path", post(update_resource))
.route("/update_value/*path", post(update_resource_value))
.route("/delete/*path", delete(delete_resource))
.route("/create", post(create_resource))
.route("/type/list", get(list_resource_types))
@@ -83,6 +85,7 @@ pub struct ListableResource {
pub resource_type: String,
pub extra_perms: serde_json::Value,
pub is_linked: Option<bool>,
pub is_refreshed: Option<bool>,
pub is_oauth: Option<bool>,
pub is_expired: Option<bool>,
pub refresh_error: Option<String>,
@@ -106,6 +109,7 @@ struct EditResource {
#[derive(Deserialize)]
pub struct ListResourceQuery {
resource_type: Option<String>,
resource_type_exclude: Option<String>,
}
async fn list_resources(
authed: Authed,
@@ -126,6 +130,7 @@ async fn list_resources(
"resource.extra_perms",
"(now() > account.expires_at) as is_expired",
"variable.path IS NOT NULL as is_linked",
"account.refresh_token != '' as is_refreshed",
"variable.is_oauth",
"variable.account",
"account.refresh_error",
@@ -143,7 +148,14 @@ async fn list_resources(
.clone();
if let Some(rt) = &lq.resource_type {
sqlb.and_where_eq("resource_type", "?".bind(rt));
for rt in rt.split(',') {
sqlb.and_where_eq("resource_type", "?".bind(&rt));
}
}
if let Some(rt) = &lq.resource_type_exclude {
for rt in rt.split(',') {
sqlb.and_where_ne("resource_type", "?".bind(&rt));
}
}
let sql = sqlb.sql().map_err(|e| Error::InternalErr(e.to_string()))?;
@@ -167,7 +179,8 @@ async fn get_resource(
let resource_o = sqlx::query_as!(
ListableResource,
"SELECT resource.*, (now() > account.expires_at) as is_expired, account.refresh_error,
"SELECT resource.*, (now() > account.expires_at) as is_expired, account.refresh_token != '' as is_refreshed,
account.refresh_error,
variable.path IS NOT NULL as is_linked,
variable.is_oauth as \"is_oauth?\",
variable.account
@@ -226,14 +239,38 @@ async fn get_resource_value(
Ok(Json(value))
}
async fn check_path_conflict<'c>(
tx: &mut Transaction<'c, Postgres>,
w_id: &str,
path: &str,
) -> Result<()> {
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM resource WHERE path = $1 AND workspace_id = $2)",
path,
w_id
)
.fetch_one(tx)
.await?
.unwrap_or(false);
if exists {
return Err(Error::BadRequest(format!(
"Resource {} already exists",
path
)));
}
return Ok(());
}
async fn create_resource(
authed: Authed,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
Path(w_id): Path<String>,
Json(resource): Json<CreateResource>,
) -> Result<(StatusCode, String)> {
let mut tx = user_db.begin(&authed).await?;
check_path_conflict(&mut tx, &w_id, &resource.path).await?;
sqlx::query!(
"INSERT INTO resource
(workspace_id, path, value, description, resource_type)
@@ -258,6 +295,11 @@ async fn create_resource(
.await?;
tx.commit().await?;
webhook.send_message(WebhookMessage::CreateResource {
workspace: w_id.clone(),
path: resource.path.clone(),
});
Ok((
StatusCode::CREATED,
format!("resource {} created", resource.path),
@@ -267,6 +309,7 @@ async fn create_resource(
async fn delete_resource(
authed: Authed,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
@@ -298,12 +341,19 @@ async fn delete_resource(
.await?;
tx.commit().await?;
webhook.send_message(WebhookMessage::DeleteResource {
workspace: w_id.clone(),
path: path.to_owned(),
});
Ok(format!("resource {} deleted", path))
}
async fn update_resource(
authed: Authed,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(ns): Json<EditResource>,
) -> Result<String> {
@@ -329,22 +379,29 @@ async fn update_resource(
let mut tx = user_db.begin(&authed).await?;
if let Some(npath) = ns.path {
if npath != path {
check_path_conflict(&mut tx, &w_id, &npath).await?;
if !authed.is_admin {
require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db).await?;
}
sqlx::query!(
"UPDATE variable SET path = $1 WHERE path = $2 AND workspace_id = $3",
npath,
path,
w_id
)
.execute(&mut tx)
.await?;
}
}
let sql = sqlb.sql().map_err(|e| Error::InternalErr(e.to_string()))?;
let npath_o: Option<String> = sqlx::query_scalar(&sql).fetch_optional(&mut tx).await?;
let npath = not_found_if_none(npath_o, "Resource", path)?;
if let Some(npath) = ns.path {
sqlx::query!(
"UPDATE variable SET path = $1 WHERE path = $2 AND workspace_id = $3",
npath,
path,
w_id
)
.execute(&mut tx)
.await?;
}
audit_log(
&mut tx,
&authed.username,
@@ -357,16 +414,65 @@ async fn update_resource(
.await?;
tx.commit().await?;
webhook.send_message(WebhookMessage::UpdateResource {
workspace: w_id.clone(),
old_path: path.to_owned(),
new_path: npath.clone(),
});
Ok(format!("resource {} updated (npath: {:?})", path, npath))
}
#[derive(FromRow, Serialize, Deserialize)]
struct UpdateResource {
value: Option<serde_json::Value>,
}
async fn update_resource_value(
authed: Authed,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
Path((w_id, path)): Path<(String, StripPath)>,
Json(nv): Json<UpdateResource>,
) -> Result<String> {
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
"UPDATE resource SET value = $1 WHERE path = $2 AND workspace_id = $3",
nv.value,
path,
w_id
)
.execute(&mut tx)
.await?;
audit_log(
&mut tx,
&authed.username,
"resources.update",
ActionKind::Update,
&w_id,
Some(path),
None,
)
.await?;
tx.commit().await?;
webhook.send_message(WebhookMessage::UpdateResource {
workspace: w_id.clone(),
old_path: path.to_owned(),
new_path: path.to_owned(),
});
Ok(format!("value of resource {} updated", path))
}
async fn list_resource_types(
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
) -> JsonResult<Vec<ResourceType>> {
let rows = sqlx::query_as!(
ResourceType,
"SELECT * from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter') ORDER \
"SELECT * from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter' OR workspace_id = 'admins') ORDER \
BY name",
&w_id
)
@@ -381,7 +487,7 @@ async fn list_resource_types_names(
Path(w_id): Path<String>,
) -> JsonResult<Vec<String>> {
let rows = sqlx::query_scalar!(
"SELECT name from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter') \
"SELECT name from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter' OR workspace_id = 'admins') \
ORDER BY name",
&w_id
)
@@ -401,7 +507,7 @@ async fn get_resource_type(
let resource_type_o = sqlx::query_as!(
ResourceType,
"SELECT * from resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = \
'starter')",
'starter' OR workspace_id = 'admins')",
&name,
&w_id
)
@@ -418,7 +524,8 @@ async fn exists_resource_type(
Path((w_id, name)): Path<(String, String)>,
) -> JsonResult<bool> {
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM resource_type WHERE name = $1 AND workspace_id = $2)",
"SELECT EXISTS(SELECT 1 FROM resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = \
'starter' OR workspace_id = 'admins'))",
name,
w_id
)
@@ -432,11 +539,14 @@ async fn exists_resource_type(
async fn create_resource_type(
authed: Authed,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
Path(w_id): Path<String>,
Json(resource_type): Json<CreateResourceType>,
) -> Result<(StatusCode, String)> {
let mut tx = user_db.begin(&authed).await?;
check_rt_path_conflict(&mut tx, &w_id, &resource_type.name).await?;
sqlx::query!(
"INSERT INTO resource_type
(workspace_id, name, schema, description)
@@ -460,15 +570,40 @@ async fn create_resource_type(
.await?;
tx.commit().await?;
webhook.send_message(WebhookMessage::CreateResourceType { name: resource_type.name.clone() });
Ok((
StatusCode::CREATED,
format!("resource_type {} created", resource_type.name),
))
}
async fn check_rt_path_conflict<'c>(
tx: &mut Transaction<'c, Postgres>,
w_id: &str,
name: &str,
) -> Result<()> {
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM resource_type WHERE name = $1 AND workspace_id = $2)",
name,
w_id
)
.fetch_one(tx)
.await?
.unwrap_or(false);
if exists {
return Err(Error::BadRequest(format!(
"Resource type {} already exists",
name
)));
}
return Ok(());
}
async fn delete_resource_type(
authed: Authed,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
Path((w_id, name)): Path<(String, String)>,
) -> Result<String> {
require_admin(authed.is_admin, &authed.username)?;
@@ -493,6 +628,7 @@ async fn delete_resource_type(
)
.await?;
tx.commit().await?;
webhook.send_message(WebhookMessage::DeleteResourceType { name: name.clone() });
Ok(format!("resource_type {} deleted", name))
}
@@ -500,6 +636,7 @@ async fn delete_resource_type(
async fn update_resource_type(
authed: Authed,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
Path((w_id, name)): Path<(String, String)>,
Json(ns): Json<EditResourceType>,
) -> Result<String> {
@@ -529,6 +666,7 @@ async fn update_resource_type(
)
.await?;
tx.commit().await?;
webhook.send_message(WebhookMessage::UpdateResourceType { name: name.clone() });
Ok(format!("resource_type {} updated", name))
}

View File

@@ -6,6 +6,8 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::str::FromStr;
use crate::{
db::{UserDB, DB},
users::Authed,
@@ -15,15 +17,16 @@ use axum::{
routing::{delete, get, post},
Json, Router,
};
use chrono::DateTime;
use chrono::{DateTime, FixedOffset};
use serde::Deserialize;
use sqlx::{Postgres, Transaction};
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{JsonResult, Result},
utils::{not_found_if_none, Pagination, StripPath},
};
use windmill_queue::{
self,
schedule::{EditSchedule, NewSchedule, PreviewPayload, Schedule, SetEnabled},
error::{Error, JsonResult, Result},
schedule::Schedule,
utils::{not_found_if_none, paginate, Pagination, StripPath},
};
use windmill_queue::{self, schedule::push_scheduled_job, JobKind};
pub fn workspaced_service() -> Router {
Router::new()
@@ -40,15 +43,94 @@ pub fn global_service() -> Router {
Router::new().route("/preview", post(preview_schedule))
}
#[derive(Deserialize)]
pub struct NewSchedule {
pub path: String,
pub schedule: String,
pub offset: i32,
pub script_path: String,
pub is_flow: bool,
pub args: Option<serde_json::Value>,
pub enabled: Option<bool>,
}
async fn check_path_conflict<'c>(
tx: &mut Transaction<'c, Postgres>,
w_id: &str,
path: &str,
) -> Result<()> {
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM schedule WHERE path = $1 AND workspace_id = $2)",
path,
w_id
)
.fetch_one(tx)
.await?
.unwrap_or(false);
if exists {
return Err(Error::BadRequest(format!(
"Schedule {} already exists",
path
)));
}
return Ok(());
}
async fn create_schedule(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(ns): Json<NewSchedule>,
) -> Result<String> {
let tx = user_db.begin(&authed).await?;
let res = windmill_queue::schedule::create_schedule(tx, w_id, ns, &authed.username).await?;
Ok(res)
let mut tx = user_db.begin(&authed).await?;
cron::Schedule::from_str(&ns.schedule).map_err(|e| Error::BadRequest(e.to_string()))?;
check_path_conflict(&mut tx, &w_id, &ns.path).await?;
check_flow_conflict(&mut tx, &w_id, &ns.path, ns.is_flow, &ns.script_path).await?;
let schedule = sqlx::query_as!(
Schedule,
"INSERT INTO schedule (workspace_id, path, schedule, offset_, edited_by, script_path, \
is_flow, args, enabled, email) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *",
w_id,
ns.path,
ns.schedule,
ns.offset,
&authed.username,
ns.script_path,
ns.is_flow,
ns.args,
ns.enabled.unwrap_or(false),
&authed.email
)
.fetch_one(&mut tx)
.await
.map_err(|e| Error::InternalErr(format!("inserting schedule in {w_id}: {e}")))?;
audit_log(
&mut tx,
&authed.username,
"schedule.create",
ActionKind::Create,
&w_id,
Some(&ns.path.to_string()),
Some(
[
Some(("schedule", ns.schedule.as_str())),
Some(("script_path", ns.script_path.as_str())),
]
.into_iter()
.flatten()
.collect(),
),
)
.await?;
if ns.enabled.unwrap_or(true) {
tx = push_scheduled_job(tx, schedule).await?
}
tx.commit().await?;
Ok(ns.path.to_string())
}
async fn edit_schedule(
@@ -57,9 +139,55 @@ async fn edit_schedule(
Path((w_id, path)): Path<(String, StripPath)>,
Json(es): Json<EditSchedule>,
) -> Result<String> {
let tx = user_db.begin(&authed).await?;
let res = windmill_queue::schedule::edit_schedule(tx, w_id, path, es, &authed.username).await?;
Ok(res)
let mut tx = user_db.begin(&authed).await?;
let path = path.to_path();
cron::Schedule::from_str(&es.schedule).map_err(|e| Error::BadRequest(e.to_string()))?;
let is_flow = sqlx::query_scalar!(
"SELECT is_flow FROM schedule WHERE path = $1 AND workspace_id = $2",
path,
w_id
)
.fetch_one(&mut tx)
.await?;
clear_schedule(&mut tx, path, is_flow).await?;
let schedule = sqlx::query_as!(
Schedule,
"UPDATE schedule SET schedule = $1, args = $2 WHERE path \
= $3 AND workspace_id = $4 RETURNING *",
es.schedule,
es.args,
path,
w_id,
)
.fetch_one(&mut tx)
.await
.map_err(|e| Error::InternalErr(format!("updating schedule in {w_id}: {e}")))?;
if schedule.enabled {
tx = push_scheduled_job(tx, schedule).await?;
}
audit_log(
&mut tx,
&authed.username,
"schedule.edit",
ActionKind::Update,
&w_id,
Some(&path.to_string()),
Some(
[Some(("schedule", es.schedule.as_str()))]
.into_iter()
.flatten()
.collect(),
),
)
.await?;
tx.commit().await?;
Ok(path.to_string())
}
async fn list_schedule(
@@ -68,9 +196,19 @@ async fn list_schedule(
Path(w_id): Path<String>,
Query(pagination): Query<Pagination>,
) -> JsonResult<Vec<Schedule>> {
let tx = user_db.begin(&authed).await?;
let res = windmill_queue::schedule::list_schedule(tx, w_id, pagination).await?;
Ok(Json(res))
let mut tx = user_db.begin(&authed).await?;
let (per_page, offset) = paginate(pagination);
let rows = sqlx::query_as!(
Schedule,
"SELECT * FROM schedule WHERE workspace_id = $1 ORDER BY edited_at desc LIMIT $2 OFFSET $3",
w_id,
per_page as i64,
offset as i64
)
.fetch_all(&mut tx)
.await?;
tx.commit().await?;
Ok(Json(rows))
}
async fn get_schedule(
@@ -100,7 +238,15 @@ async fn exists_schedule(
pub async fn preview_schedule(
Json(payload): Json<PreviewPayload>,
) -> JsonResult<Vec<DateTime<chrono::Utc>>> {
Ok(Json(windmill_queue::schedule::preview_schedule(payload)?))
let schedule = cron::Schedule::from_str(&payload.schedule)
.map_err(|e| Error::BadRequest(e.to_string()))?;
let upcoming: Vec<DateTime<chrono::Utc>> = schedule
.upcoming(get_offset(payload.offset))
.take(10)
.map(|x| x.into())
.collect();
Ok(Json(upcoming))
}
pub async fn set_enabled(
@@ -109,10 +255,41 @@ pub async fn set_enabled(
Path((w_id, path)): Path<(String, StripPath)>,
Json(payload): Json<SetEnabled>,
) -> Result<String> {
let tx = user_db.begin(&authed).await?;
let res =
windmill_queue::schedule::set_enabled(tx, w_id, path, payload, &authed.username).await?;
Ok(res)
let mut tx = user_db.begin(&authed).await?;
let path = path.to_path();
let schedule_o = sqlx::query_as!(
Schedule,
"UPDATE schedule SET enabled = $1, email = $2 WHERE path = $3 AND workspace_id = $4 RETURNING *",
&payload.enabled,
authed.email,
path,
w_id
)
.fetch_optional(&mut tx)
.await?;
let schedule = not_found_if_none(schedule_o, "Schedule", path)?;
clear_schedule(&mut tx, path, schedule.is_flow).await?;
if payload.enabled {
tx = push_scheduled_job(tx, schedule).await?;
}
audit_log(
&mut tx,
&authed.username,
"schedule.setenabled",
ActionKind::Update,
&w_id,
Some(path),
Some([("enabled", payload.enabled.to_string().as_ref())].into()),
)
.await?;
tx.commit().await?;
Ok(format!(
"succesfully updated schedule at path {} to status {}",
path, payload.enabled
))
}
async fn delete_schedule(
@@ -120,7 +297,96 @@ async fn delete_schedule(
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let tx = user_db.begin(&authed).await?;
let res = windmill_queue::schedule::delete_schedule(tx, w_id, path, &authed.username).await?;
Ok(res)
let mut tx = user_db.begin(&authed).await?;
let path = path.to_path();
sqlx::query!(
"DELETE FROM schedule WHERE path = $1 AND workspace_id = $2",
path,
w_id
)
.execute(&mut tx)
.await?;
audit_log(
&mut tx,
&authed.username,
"schedule.delete",
ActionKind::Delete,
&w_id,
Some(path),
None,
)
.await?;
tx.commit().await?;
Ok(format!("schedule {} deleted", path))
}
async fn check_flow_conflict<'c>(
tx: &mut Transaction<'c, Postgres>,
w_id: &str,
path: &str,
is_flow: bool,
script_path: &str,
) -> Result<()> {
if path != script_path || !is_flow {
let exists_flow = sqlx::query_scalar!(
"SELECT EXISTS (SELECT 1 FROM flow WHERE path = $1 AND workspace_id = $2)",
path,
w_id
)
.fetch_one(tx)
.await?
.unwrap_or(false);
if exists_flow {
return Err(Error::BadRequest(format!(
"The path is the same as a flow, it can only trigger that flow.
However the provided path is: {script_path} and is_flow is {is_flow}"
)));
};
}
Ok(())
}
#[derive(Deserialize)]
pub struct EditSchedule {
pub schedule: String,
pub args: Option<serde_json::Value>,
}
pub async fn clear_schedule<'c>(
db: &mut Transaction<'c, Postgres>,
path: &str,
is_flow: bool,
) -> Result<()> {
let job_kind = if is_flow {
JobKind::Flow
} else {
JobKind::Script
};
sqlx::query!(
"DELETE FROM queue WHERE schedule_path = $1 AND running = false AND job_kind = $2",
path,
job_kind: JobKind
)
.execute(db)
.await?;
Ok(())
}
#[derive(Deserialize)]
pub struct PreviewPayload {
pub schedule: String,
pub offset: Option<i32>,
}
fn get_offset(offset: Option<i32>) -> FixedOffset {
FixedOffset::west_opt(offset.unwrap_or(0) * 60).expect("Invalid offset")
}
#[derive(Deserialize)]
pub struct SetEnabled {
pub enabled: bool,
}

View File

@@ -12,10 +12,12 @@ use windmill_audit::{audit_log, ActionKind};
use crate::{
db::{UserDB, DB},
users::{truncate_token, Authed, Tokened},
schedule::clear_schedule,
users::{require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookUtil},
};
use axum::{
extract::{Extension, Host, Path, Query},
extract::{Extension, Path, Query},
routing::{get, post},
Json, Router,
};
@@ -30,16 +32,17 @@ use std::{
};
use windmill_common::{
error::{Error, JsonResult, Result},
schedule::Schedule,
scripts::{
to_i64, HubScript, ListScriptQuery, ListableScript, NewScript, Script, ScriptHash,
ScriptKind, ScriptLang,
},
users::owner_to_token_owner,
users::username_to_permissioned_as,
utils::{
list_elems_from_hub, not_found_if_none, paginate, require_admin, Pagination, StripPath,
},
};
use windmill_queue;
use windmill_queue::{self, schedule::push_scheduled_job};
const MAX_HASH_HISTORY_LENGTH_STORED: usize = 20;
@@ -70,6 +73,7 @@ pub fn workspaced_service() -> Router {
.route("/get/h/:hash", get(get_script_by_hash))
.route("/raw/h/:hash", get(raw_script_by_hash))
.route("/deployment_status/h/:hash", get(get_deployment_status))
.route("/list_paths", get(list_paths))
}
async fn list_scripts(
authed: Authed,
@@ -160,16 +164,13 @@ async fn list_scripts(
}
async fn list_hub_scripts(
Authed { email, username, .. }: Authed,
Authed { email, .. }: Authed,
Extension(http_client): Extension<Client>,
Host(host): Host,
) -> JsonResult<serde_json::Value> {
let asks = list_elems_from_hub(
http_client,
"https://hub.windmill.dev/searchData?approved=true",
email,
username,
host,
&email,
)
.await?;
Ok(Json(asks))
@@ -180,10 +181,12 @@ fn hash_script(ns: &NewScript) -> i64 {
ns.hash(&mut dh);
dh.finish() as i64
}
async fn create_script(
authed: Authed,
Tokened { token }: Tokened,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Json(ns): Json<NewScript>,
) -> Result<(StatusCode, String)> {
@@ -214,86 +217,99 @@ async fn create_script(
.fetch_optional(&mut tx)
.await?;
let parent_hashes_and_perms: Option<(Vec<i64>, serde_json::Value)> =
match (&ns.parent_hash, clashing_script) {
(None, None) => Ok(None),
(None, Some(s)) => Err(Error::BadRequest(format!(
"Path conflict for {} with non-archived hash {}",
&ns.path, &s.hash
))),
(Some(p_hash), o) => {
if sqlx::query_scalar!(
"SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2",
p_hash.0,
&w_id
)
.fetch_optional(&mut tx)
.await?
.is_none()
{
return Err(Error::BadRequest(
"The parent hash does not seem to exist".to_owned(),
));
};
struct ParentInfo {
p_hashes: Vec<i64>,
perms: serde_json::Value,
p_path: String,
}
let parent_hashes_and_perms: Option<ParentInfo> = match (&ns.parent_hash, clashing_script) {
(None, None) => Ok(None),
(None, Some(s)) => Err(Error::BadRequest(format!(
"Path conflict for {} with non-archived hash {}",
&ns.path, &s.hash
))),
(Some(p_hash), o) => {
if sqlx::query_scalar!(
"SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2",
p_hash.0,
&w_id
)
.fetch_optional(&mut tx)
.await?
.is_none()
{
return Err(Error::BadRequest(
"The parent hash does not seem to exist".to_owned(),
));
};
let clashing_hash_o = sqlx::query_scalar!(
"SELECT hash FROM script WHERE parent_hashes[1] = $1 AND workspace_id = $2",
p_hash.0,
&w_id
)
.fetch_optional(&mut tx)
.await?;
let clashing_hash_o = sqlx::query_scalar!(
"SELECT hash FROM script WHERE parent_hashes[1] = $1 AND workspace_id = $2",
p_hash.0,
&w_id
)
.fetch_optional(&mut tx)
.await?;
if let Some(clashing_hash) = clashing_hash_o {
return Err(Error::BadRequest(format!(
"A script with hash {} with same parent_hash has been found. However, the \
if let Some(clashing_hash) = clashing_hash_o {
return Err(Error::BadRequest(format!(
"A script with hash {} with same parent_hash has been found. However, the \
lineage must be linear: no 2 scripts can have the same parent",
ScriptHash(clashing_hash)
)));
};
ScriptHash(clashing_hash)
)));
};
let ps = get_script_by_hash_internal(&mut tx, &w_id, p_hash).await?;
let ps = get_script_by_hash_internal(&mut tx, &w_id, p_hash).await?;
let ph = {
let v = ps.parent_hashes.map(|x| x.0).unwrap_or_default();
let mut v: Vec<i64> = v
.into_iter()
.take(MAX_HASH_HISTORY_LENGTH_STORED - 1)
.collect();
v.insert(0, p_hash.0);
v
};
let r: Result<Option<(Vec<i64>, serde_json::Value)>> = match o {
Some(clashing_script)
if clashing_script.path == ns.path
&& clashing_script.hash.0 != p_hash.0 =>
{
Err(Error::BadRequest(format!(
"Path conflict for {} with non-archived hash {}",
&ns.path, &clashing_script.hash
)))
}
Some(_) => Ok(Some((ph, ps.extra_perms))),
None => Ok(Some((ph, ps.extra_perms))),
};
sqlx::query!(
"UPDATE script SET archived = true WHERE hash = $1 AND workspace_id = $2",
p_hash.0,
&w_id
)
.execute(&mut tx)
.await?;
r
if ps.path != ns.path {
if !authed.is_admin {
require_owner_of_path(&w_id, &authed.username, &authed.groups, &ps.path, &db)
.await?;
}
}
}?;
let p_hashes = parent_hashes_and_perms.as_ref().map(|v| &v.0[..]);
let ph = {
let v = ps.parent_hashes.map(|x| x.0).unwrap_or_default();
let mut v: Vec<i64> = v
.into_iter()
.take(MAX_HASH_HISTORY_LENGTH_STORED - 1)
.collect();
v.insert(0, p_hash.0);
v
};
let r: Result<Option<ParentInfo>> = match o {
Some(clashing_script)
if clashing_script.path == ns.path && clashing_script.hash.0 != p_hash.0 =>
{
Err(Error::BadRequest(format!(
"Path conflict for {} with non-archived hash {}",
&ns.path, &clashing_script.hash
)))
}
Some(_) | None => Ok(Some(ParentInfo {
p_hashes: ph,
perms: ps.extra_perms,
p_path: ps.path,
})),
};
sqlx::query!(
"UPDATE script SET archived = true WHERE hash = $1 AND workspace_id = $2",
p_hash.0,
&w_id
)
.execute(&mut tx)
.await?;
r
}
}?;
let p_hashes = parent_hashes_and_perms.as_ref().map(|v| &v.p_hashes[..]);
let extra_perms = parent_hashes_and_perms
.as_ref()
.map(|v| v.1.clone())
.map(|v| v.perms.clone())
.unwrap_or(json!({}));
let lock = if ns.language == ScriptLang::Bash {
let lock = if ns.language == ScriptLang::Bash || ns.language == ScriptLang::Deno {
Some(String::new())
} else {
ns.lock
@@ -326,6 +342,26 @@ async fn create_script(
.execute(&mut tx)
.await?;
if let Some(p_path) = parent_hashes_and_perms.as_ref().map(|x| x.p_path.clone()) {
let schedulables = sqlx::query_as!(
Schedule,
"UPDATE schedule SET script_path = $1 WHERE script_path = $2 AND workspace_id = $3 AND is_flow IS false RETURNING *",
ns.path,
p_path,
w_id,
)
.fetch_all(&mut tx)
.await?;
for schedule in schedulables {
clear_schedule(&mut tx, &schedule.path, false).await?;
if schedule.enabled {
tx = push_scheduled_job(tx, schedule).await?;
}
}
}
let mut tx = if needs_lock_gen {
let dependencies = match ns.language {
ScriptLang::Python3 => {
@@ -337,14 +373,17 @@ async fn create_script(
tx,
&w_id,
windmill_queue::JobPayload::Dependencies { hash, dependencies, language: ns.language },
None,
serde_json::Map::new(),
&authed.username,
owner_to_token_owner(&authed.username, false),
&authed.email,
username_to_permissioned_as(&authed.username),
None,
None,
None,
false,
false,
None,
true,
)
.await?;
tx
@@ -360,15 +399,14 @@ async fn create_script(
ActionKind::Update,
&w_id,
Some(&ns.path),
Some(
[
("hash", hash.to_string().as_str()),
("token", &truncate_token(&token)),
]
.into(),
),
Some([("hash", hash.to_string().as_str())].into()),
)
.await?;
webhook.send_message(WebhookMessage::UpdateScript {
workspace: w_id.clone(),
path: ns.path.clone(),
hash: hash.to_string(),
});
} else {
audit_log(
&mut tx,
@@ -381,12 +419,16 @@ async fn create_script(
[
("workspace", w_id.as_str()),
("hash", hash.to_string().as_str()),
("token", &truncate_token(&token)),
]
.into(),
),
)
.await?;
webhook.send_message(WebhookMessage::CreateScript {
workspace: w_id.clone(),
path: ns.path.clone(),
hash: hash.to_string(),
});
}
tx.commit().await?;
@@ -398,33 +440,17 @@ pub async fn get_hub_script_by_path(
authed: Authed,
Path(path): Path<StripPath>,
Extension(http_client): Extension<Client>,
Host(host): Host,
) -> Result<String> {
windmill_common::scripts::get_hub_script_by_path(
authed.email,
authed.username,
path,
http_client,
host,
)
.await
windmill_common::scripts::get_hub_script_by_path(&authed.email, path, http_client).await
}
pub async fn get_full_hub_script_by_path(
Authed { username, email, .. }: Authed,
Authed { email, .. }: Authed,
Path(path): Path<StripPath>,
Extension(http_client): Extension<Client>,
Host(host): Host,
) -> JsonResult<HubScript> {
Ok(Json(
windmill_common::scripts::get_full_hub_script_by_path(
email,
username,
path,
http_client,
host,
)
.await?,
windmill_common::scripts::get_full_hub_script_by_path(&email, path, http_client).await?,
))
}
@@ -451,15 +477,30 @@ async fn get_script_by_path(
Ok(Json(script))
}
async fn list_paths(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
) -> JsonResult<Vec<String>> {
let mut tx = user_db.begin(&authed).await?;
let scripts = sqlx::query_scalar!(
"SELECT distinct(path) FROM script WHERE workspace_id = $1",
w_id
)
.fetch_all(&mut tx)
.await?;
tx.commit().await?;
Ok(Json(scripts))
}
async fn raw_script_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path
.to_path()
.strip_suffix(".ts")
.ok_or_else(|| Error::BadRequest("Raw script path must end with .ts".to_string()))?;
let path = path.to_path().split(".").next().unwrap_or_default();
let mut tx = user_db.begin(&authed).await?;
let content_o = sqlx::query_scalar!(
@@ -571,6 +612,7 @@ async fn get_deployment_status(
async fn archive_script_by_path(
authed: Authed,
webhook: WebhookUtil,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
@@ -597,6 +639,10 @@ async fn archive_script_by_path(
)
.await?;
tx.commit().await?;
webhook.send_message(WebhookMessage::DeleteScript {
workspace: w_id.clone(),
hash: hash.to_string(),
});
Ok(())
}
@@ -604,6 +650,7 @@ async fn archive_script_by_path(
async fn archive_script_by_hash(
authed: Authed,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
Path((w_id, hash)): Path<(String, ScriptHash)>,
) -> JsonResult<Script> {
let mut tx = user_db.begin(&authed).await?;
@@ -628,12 +675,18 @@ async fn archive_script_by_hash(
.await?;
tx.commit().await?;
webhook.send_message(WebhookMessage::DeleteScript {
workspace: w_id.clone(),
hash: hash.to_string(),
});
Ok(Json(script))
}
async fn delete_script_by_hash(
authed: Authed,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
Extension(db): Extension<DB>,
Path((w_id, hash)): Path<(String, ScriptHash)>,
) -> JsonResult<Script> {
@@ -662,6 +715,11 @@ async fn delete_script_by_hash(
.await?;
tx.commit().await?;
webhook.send_message(WebhookMessage::DeleteScript {
workspace: w_id.clone(),
hash: hash.to_string(),
});
Ok(Json(script))
}

View File

@@ -8,7 +8,8 @@
use axum::{
body::{self, BoxBody},
http::{header, response::Builder, Response, Uri},
extract::OriginalUri,
http::{header, response::Builder, Response},
response::IntoResponse,
Extension,
};
@@ -20,26 +21,28 @@ use std::sync::Arc;
// static_handler is a handler that serves static files from the
pub async fn static_handler(
uri: Uri,
Extension(is_secure): Extension<Arc<IsSecure>>,
Extension(is_cloud_hosted): Extension<Arc<CloudHosted>>,
Extension(csp): Extension<Arc<ContentSecurityPolicy>>,
) -> impl IntoResponse {
let path = uri.path().trim_start_matches('/').to_string();
OriginalUri(original_uri): OriginalUri,
) -> StaticFile {
let path = original_uri.path().trim_start_matches('/').to_string();
StaticFile(path, is_secure.0, is_cloud_hosted.0, csp)
}
#[derive(RustEmbed)]
#[folder = "../../frontend/build/"]
struct Asset;
pub struct StaticFile<T>(pub T, pub bool, pub bool, pub Arc<ContentSecurityPolicy>);
pub struct StaticFile(
pub String,
pub bool,
pub bool,
pub Arc<ContentSecurityPolicy>,
);
impl<T> IntoResponse for StaticFile<T>
where
T: Into<String>,
{
impl IntoResponse for StaticFile {
fn into_response(self) -> Response<BoxBody> {
let path = self.0.into();
let path = self.0;
let can_set_security_headers = self.1 && self.2;
let csp = self.3;
serve_path(path, can_set_security_headers, csp)

View File

@@ -10,14 +10,16 @@ use std::{sync::Arc, time::Duration};
use crate::{
db::{UserDB, DB},
folders::get_folders_for_user,
utils::require_super_admin,
workspaces::invite_user_to_all_auto_invite_worspaces,
CookieDomain, IsSecure,
};
use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use axum::{
async_trait,
extract::{Extension, FromRequest, Path, Query, RequestParts},
http,
extract::{Extension, FromRequestParts, OriginalUri, Path, Query},
http::{self, request::Parts},
response::{IntoResponse, Response},
routing::{delete, get, post},
Json, Router,
@@ -29,14 +31,15 @@ use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use time::OffsetDateTime;
use tower_cookies::{Cookie, Cookies};
use tracing::Span;
use tracing::{Instrument, Span};
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{self, Error, JsonResult, Result},
utils::{not_found_if_none, rd_string, require_admin, Pagination},
utils::{not_found_if_none, rd_string, require_admin, Pagination, StripPath},
};
use windmill_queue::CLOUD_HOSTED;
const TTL_TOKEN_CACHE_S: u64 = 60 * 5; // 5 minutes
const TTL_TOKEN_CACHE_S: u64 = 60; // 60s
pub const TTL_TOKEN_DB_H: u32 = 72;
const COOKIE_NAME: &str = "token";
@@ -49,6 +52,7 @@ pub fn workspaced_service() -> Router {
.route("/exists", post(exists_username))
.route("/update/:user", post(update_workspace_user))
.route("/delete/:user", delete(delete_workspace_user))
.route("/is_owner/*path", get(is_owner_of_path))
.route("/whois/:email", get(whois))
.route("/whoami", get(whoami))
.route("/leave", post(leave_workspace))
@@ -69,6 +73,7 @@ pub fn global_service() -> Router {
.route("/tokens/create", post(create_token))
.route("/tokens/delete/:token_prefix", delete(delete_token))
.route("/tokens/list", get(list_tokens))
.route("/usage", get(get_usage))
// .route("/list_invite_codes", get(list_invite_codes))
// .route("/create_invite_code", post(create_invite_code))
// .route("/signup", post(signup))
@@ -86,11 +91,12 @@ pub fn make_unauthed_service() -> Router {
pub struct AuthCache {
cache: Cache<(String, String), Authed>,
db: DB,
superadmin_secret: Option<String>,
}
impl AuthCache {
pub fn new(db: DB) -> Self {
AuthCache { cache: Cache::new(), db }
pub fn new(db: DB, superadmin_secret: Option<String>) -> Self {
AuthCache { cache: Cache::new(), db, superadmin_secret }
}
pub async fn get_authed(&self, w_id: Option<String>, token: &str) -> Option<Authed> {
@@ -115,11 +121,79 @@ impl AuthCache {
if let Some(user) = user_o {
let authed_o = {
match user {
(Some(owner), email, super_admin) if w_id.is_some() => {
if let Some((prefix, name)) = owner.split_once('/') {
if prefix == "u" {
let is_admin = super_admin
|| sqlx::query_scalar!(
"SELECT is_admin FROM usr where username = $1 AND \
workspace_id = $2 AND disabled = false",
name,
&w_id.as_ref().unwrap()
)
.fetch_one(&self.db)
.await
.ok()
.unwrap_or(false);
let w_id = &w_id.unwrap();
let groups = get_groups_for_user(w_id, &name, &self.db)
.await
.ok()
.unwrap_or_default();
let folders =
get_folders_for_user(w_id, &name, &groups, &self.db)
.await
.ok()
.unwrap_or_default();
Some(Authed {
email: email
.unwrap_or_else(|| "missing@email.xyz".to_string()),
username: name.to_string(),
is_admin,
groups,
folders,
})
} else {
let groups = vec![name.to_string()];
let folders = get_folders_for_user(
&w_id.unwrap(),
"",
&groups,
&self.db,
)
.await
.ok()
.unwrap_or_default();
Some(Authed {
email: email
.unwrap_or_else(|| "missing@email.xyz".to_string()),
username: format!("group-{name}"),
is_admin: false,
groups,
folders,
})
}
} else {
let groups = vec![];
let folders = vec![];
Some(Authed {
email: email
.unwrap_or_else(|| "missing@email.xyz".to_string()),
username: owner,
is_admin: super_admin,
groups,
folders,
})
}
}
(_, Some(email), super_admin) => {
if w_id.is_some() {
let row_o = sqlx::query_as::<_, (String, bool)>(
"SELECT username, is_admin FROM usr where email = $1 AND \
workspace_id = $2",
workspace_id = $2 AND disabled = false",
)
.bind(&email)
.bind(&w_id.as_ref().unwrap())
@@ -138,71 +212,44 @@ impl AuthCache {
.ok()
.unwrap_or_default();
let folders = get_folders_for_user(
&w_id.unwrap(),
&username,
&groups,
&self.db,
)
.await
.ok()
.unwrap_or_default();
Some(Authed {
email: Some(email),
email,
username,
is_admin: is_admin || super_admin,
groups,
folders,
})
}
None if super_admin || w_id.unwrap() == "starter" => {
Some(Authed {
email: Some(email.to_string()),
email: email.clone(),
username: email,
is_admin: super_admin,
groups: vec![],
folders: vec![],
})
}
None => None,
}
} else {
Some(Authed {
email: Some(email.to_string()),
email: email.to_string(),
username: email,
is_admin: super_admin,
groups: Vec::new(),
folders: Vec::new(),
})
}
}
(Some(owner), _, super_admin) if w_id.is_some() => {
if let Some((prefix, name)) = owner.split_once('/') {
if prefix == "u" {
let is_admin = super_admin
|| sqlx::query_scalar!(
"SELECT is_admin FROM usr where username = $1 AND \
workspace_id = $2",
name,
&w_id.as_ref().unwrap()
)
.fetch_one(&self.db)
.await
.ok()
.unwrap_or(false);
let groups =
get_groups_for_user(&w_id.unwrap(), &name, &self.db)
.await
.ok()
.unwrap_or_default();
Some(Authed {
email: None,
username: name.to_string(),
is_admin,
groups,
})
} else {
Some(Authed {
email: None,
username: format!("group-{name}"),
is_admin: false,
groups: vec![name.to_string()],
})
}
} else {
None
}
}
_ => None,
}
};
@@ -212,6 +259,19 @@ impl AuthCache {
.await;
}
authed_o
} else if self
.superadmin_secret
.as_ref()
.map(|x| x == token)
.unwrap_or(false)
{
Some(Authed {
email: "superadmin_secret@windmill.dev".to_string(),
username: "superadmin_secret".to_string(),
is_admin: true,
groups: Vec::new(),
folders: Vec::new(),
})
} else {
None
}
@@ -224,16 +284,16 @@ impl AuthCache {
}
}
async fn extract_token<B: Send>(req: &mut RequestParts<B>) -> Option<String> {
let auth_header = req
.headers()
async fn extract_token<S: Send + Sync>(parts: &mut Parts, state: &S) -> Option<String> {
let auth_header = parts
.headers
.get(http::header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|s| s.strip_prefix("Bearer "));
let from_cookie = match auth_header {
Some(x) => Some(x.to_owned()),
None => Extension::<Cookies>::from_request(req)
None => Extension::<Cookies>::from_request_parts(parts, state)
.await
.ok()
.and_then(|cookies| cookies.get(COOKIE_NAME).map(|c| c.value().to_owned())),
@@ -245,7 +305,7 @@ async fn extract_token<B: Send>(req: &mut RequestParts<B>) -> Option<String> {
}
match from_cookie {
Some(token) => Some(token),
None => Query::<Token>::from_request(req)
None => Query::<Token>::from_request_parts(parts, state)
.await
.ok()
.and_then(|token| token.token.clone()),
@@ -258,21 +318,24 @@ pub struct Tokened {
}
#[async_trait]
impl<B> FromRequest<B> for Tokened
impl<S> FromRequestParts<S> for Tokened
where
B: Send,
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request(req: &mut RequestParts<B>) -> std::result::Result<Self, Self::Rejection> {
let already_tokened = req.extensions().get::<Tokened>();
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
let already_tokened = parts.extensions.get::<Tokened>();
if let Some(tokened) = already_tokened {
Ok(tokened.clone())
} else {
let token_o = extract_token(req).await;
let token_o = extract_token(parts, state).await;
if let Some(token) = token_o {
let tokened = Self { token };
req.extensions_mut().insert(tokened.clone());
parts.extensions.insert(tokened.clone());
Ok(tokened)
} else {
Err((StatusCode::UNAUTHORIZED, "Unauthorized".to_owned()))
@@ -283,44 +346,55 @@ where
#[derive(Clone, Debug)]
pub struct Authed {
pub email: Option<String>,
pub email: String,
pub username: String,
pub is_admin: bool,
pub groups: Vec<String>,
pub folders: Vec<(String, bool)>,
}
#[async_trait]
impl<B> FromRequest<B> for Authed
impl<S> FromRequestParts<S> for Authed
where
B: Send,
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request(req: &mut RequestParts<B>) -> std::result::Result<Self, Self::Rejection> {
let already_authed = req.extensions().get::<Authed>();
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
let already_authed = parts.extensions.get::<Authed>();
if let Some(authed) = already_authed {
Ok(authed.clone())
} else {
let already_tokened = req.extensions().get::<Tokened>();
let already_tokened = parts.extensions.get::<Tokened>();
let token_o = if let Some(token) = already_tokened {
Some(token.token.clone())
} else {
extract_token(req).await
extract_token(parts, state).await
};
let path_vec: Vec<&str> = req.uri().path().split("/").collect();
let workspace_id = if path_vec[0] == "" && path_vec[1] == "w" {
Some(path_vec[2].to_owned())
let original_uri = OriginalUri::from_request_parts(parts, state)
.await
.ok()
.map(|x| x.0)
.unwrap_or_default();
let path_vec: Vec<&str> = original_uri.path().split("/").collect();
let workspace_id = if path_vec.len() >= 4 && path_vec[0] == "" && path_vec[2] == "w" {
Some(path_vec[3].to_owned())
} else {
None
};
if let Some(token) = token_o {
if let Ok(Extension(cache)) = Extension::<Arc<AuthCache>>::from_request(req).await {
if let Ok(Extension(cache)) =
Extension::<Arc<AuthCache>>::from_request_parts(parts, state).await
{
if let Some(authed) = cache.get_authed(workspace_id.clone(), &token).await {
req.extensions_mut().insert(authed.clone());
parts.extensions.insert(authed.clone());
Span::current().record("username", &authed.username.as_str());
if let Some(email) = authed.email.clone() {
Span::current().record("email", &email.as_str());
}
Span::current().record("email", &authed.email);
if let Some(workspace_id) = workspace_id {
Span::current().record("workspace_id", &workspace_id);
}
@@ -337,14 +411,17 @@ where
pub struct OptAuthed(pub Option<Authed>);
#[async_trait]
impl<B> FromRequest<B> for OptAuthed
impl<S> FromRequestParts<S> for OptAuthed
where
B: Send,
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request(req: &mut RequestParts<B>) -> std::result::Result<Self, Self::Rejection> {
Authed::from_request(req)
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
Authed::from_request_parts(parts, state)
.await
.map(|authed| Self(Some(authed)))
.or_else(|_| Ok(Self(None)))
@@ -365,9 +442,7 @@ pub struct User {
#[derive(FromRow, Serialize)]
pub struct Usage {
pub duration_ms: i64,
pub jobs: i64,
pub flows: i64,
pub executions: i64,
}
#[derive(Serialize)]
@@ -377,7 +452,7 @@ pub struct UserWithUsage {
pub usage: Usage,
}
#[derive(FromRow, Serialize)]
#[derive(FromRow, Serialize, Debug)]
pub struct GlobalUserInfo {
email: String,
login_type: Option<String>,
@@ -399,6 +474,7 @@ pub struct UserInfo {
pub operator: bool,
pub disabled: bool,
pub role: Option<String>,
pub folders: Vec<String>,
}
#[derive(FromRow, Serialize)]
@@ -406,6 +482,7 @@ pub struct WorkspaceInvite {
pub workspace_id: String,
pub email: String,
pub is_admin: bool,
pub operator: bool,
}
#[derive(FromRow, Serialize)]
@@ -462,7 +539,8 @@ pub struct EditUser {
#[derive(Deserialize)]
pub struct EditWorkspaceUser {
pub is_admin: Option<bool>,
pub enabled: Option<bool>,
pub operator: Option<bool>,
pub disabled: Option<bool>,
}
#[derive(Deserialize)]
@@ -539,14 +617,12 @@ async fn list_users(
SELECT usr.*, usage.*
FROM usr
, LATERAL (
SELECT COALESCE(SUM(duration_ms), 0) duration_ms
, COALESCE(SUM(job_kind IN ('flow', 'flowpreview') ::int), 0) flows
, COALESCE(SUM(job_kind NOT IN ('flow', 'flowpreview') ::int), 0) jobs
SELECT COALESCE(SUM(duration_ms + 1000)/1000 , 0) executions
FROM completed_job
WHERE workspace_id = usr.workspace_id
AND created_by = usr.username
AND parent_job IS NULL
AND now() - '2 week'::interval < created_at
AND job_kind NOT IN ('flow', 'flowpreview')
AND email = usr.email
AND now() - '5 week'::interval < created_at
) usage
WHERE workspace_id = $1
",
@@ -568,7 +644,7 @@ async fn list_users_as_super_admin(
Query(pagination): Query<Pagination>,
) -> JsonResult<Vec<GlobalUserInfo>> {
let mut tx = db.begin().await?;
require_super_admin(&mut tx, authed.email).await?;
require_super_admin(&mut tx, &authed.email).await?;
let per_page = pagination.per_page.unwrap_or(10000).max(1);
let offset = (pagination.page.unwrap_or(1).max(1) - 1) * per_page;
@@ -606,6 +682,12 @@ async fn list_usernames(
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
) -> JsonResult<Vec<String>> {
if *CLOUD_HOSTED && w_id == "demo" {
return Ok(Json(vec![
authed.username,
"other_usernames_redacted_in_demo_workspace".to_string(),
]));
}
let mut tx = user_db.begin(&authed).await?;
let rows = sqlx::query_scalar!("SELECT username from usr WHERE workspace_id = $1", &w_id)
.fetch_all(&mut tx)
@@ -675,13 +757,12 @@ async fn logout(
async fn whoami(
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Authed { username, email, is_admin, groups }: Authed,
Authed { username, email, is_admin, groups, folders }: Authed,
) -> JsonResult<UserInfo> {
let user = get_user(&w_id, &username, &db).await?;
if let Some(user) = user {
Ok(Json(user))
} else {
let email = email.unwrap_or_else(|| "noemail".to_string());
Ok(Json(UserInfo {
workspace_id: w_id,
email: email.clone(),
@@ -693,6 +774,10 @@ async fn whoami(
operator: false,
disabled: false,
role: Some("superadmin".to_string()),
folders: folders
.into_iter()
.filter_map(|x| if x.1 { Some(x.0) } else { None })
.collect(),
}))
}
}
@@ -700,8 +785,9 @@ async fn whoami(
async fn global_whoami(
Extension(db): Extension<DB>,
Authed { email, .. }: Authed,
Tokened { token }: Tokened,
) -> JsonResult<GlobalUserInfo> {
let user: GlobalUserInfo = sqlx::query_as!(
let user = sqlx::query_as!(
GlobalUserInfo,
"SELECT email, login_type::TEXT, super_admin, verified, name, company FROM password WHERE \
email = $1",
@@ -709,18 +795,43 @@ async fn global_whoami(
)
.fetch_one(&db)
.await
.map_err(|e| Error::InternalErr(format!("fetching global identity: {e}")))?;
.map_err(|e| Error::InternalErr(format!("fetching global identity: {e}")));
Ok(Json(user))
if let Ok(user) = user {
Ok(Json(user))
} else if std::env::var("SUPERADMIN_SECRET").ok() == Some(token) {
Ok(Json(GlobalUserInfo {
email: email.clone(),
login_type: Some("superadmin_secret".to_string()),
super_admin: true,
verified: true,
name: None,
company: None,
}))
} else {
Err(user.unwrap_err())
}
}
async fn get_email(Authed { email, .. }: Authed) -> Result<String> {
let email = email.ok_or(Error::BadRequest(
"current session does not correspond to an user with email".to_string(),
))?;
Ok(email)
}
async fn get_usage(Extension(db): Extension<DB>, Authed { email, .. }: Authed) -> Result<String> {
let usage = sqlx::query_scalar!(
"
SELECT usage.usage FROM usage
WHERE is_workspace = false
AND month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)
AND id = $1",
email
)
.fetch_optional(&db)
.await?
.unwrap_or(0);
Ok(usage.to_string())
}
async fn get_user(w_id: &str, username: &str, db: &DB) -> Result<Option<UserInfo>> {
let user = sqlx::query_as!(
User,
@@ -738,6 +849,8 @@ async fn get_user(w_id: &str, username: &str, db: &DB) -> Result<Option<UserInfo
.await?
.unwrap_or(false);
let groups = get_groups_for_user(&w_id, username, db).await?;
let folders = get_folders_for_user(&w_id, username, &groups, db).await?;
Ok(user.map(|usr| UserInfo {
groups,
workspace_id: usr.workspace_id,
@@ -749,10 +862,14 @@ async fn get_user(w_id: &str, username: &str, db: &DB) -> Result<Option<UserInfo
operator: usr.operator,
disabled: usr.disabled,
role: usr.role,
folders: folders
.into_iter()
.filter_map(|x| if x.1 { Some(x.0) } else { None })
.collect(),
}))
}
async fn get_groups_for_user(w_id: &str, username: &str, db: &DB) -> Result<Vec<String>> {
pub async fn get_groups_for_user(w_id: &str, username: &str, db: &DB) -> Result<Vec<String>> {
let groups = sqlx::query_scalar!(
"SELECT group_ FROM usr_to_group where usr = $1 AND workspace_id = $2",
username,
@@ -762,6 +879,49 @@ async fn get_groups_for_user(w_id: &str, username: &str, db: &DB) -> Result<Vec<
.await?;
Ok(groups)
}
pub async fn is_owner_of_path(
Authed { username, is_admin, groups, .. }: Authed,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<bool> {
let path = path.to_path();
if is_admin {
Ok(Json(true))
} else {
Ok(Json(
require_owner_of_path(&w_id, &username, &groups, path, &db)
.await
.is_ok(),
))
}
}
pub async fn require_owner_of_path(
w_id: &str,
username: &str,
groups: &Vec<String>,
path: &str,
db: &DB,
) -> Result<()> {
if !path.is_empty() {
let splitted = path.split("/").collect::<Vec<&str>>();
if splitted[0] == "u" {
if splitted[1] == username {
return Ok(());
} else {
return Err(Error::BadRequest(format!(
"only the owner {} is authorized to perform this operation",
splitted[1]
)));
}
} else if splitted[0] == "g" {
return crate::groups::require_is_owner(splitted[1], username, groups, w_id, db).await;
} else if splitted[0] == "f" {
return crate::folders::require_is_owner(splitted[1], username, groups, w_id, db).await;
}
}
Err(Error::BadRequest(format!("not recognized owner kind")))
}
async fn whois(
Extension(db): Extension<DB>,
@@ -806,7 +966,6 @@ async fn decline_invite(
) -> Result<(StatusCode, String)> {
let mut tx = db.begin().await?;
let email = email.unwrap_or("".to_string());
let is_admin = sqlx::query_scalar!(
"DELETE FROM workspace_invite WHERE workspace_id = $1 AND email = $2 RETURNING is_admin",
nu.workspace_id,
@@ -850,32 +1009,40 @@ async fn accept_invite(
}
let mut tx = db.begin().await?;
let email = email.unwrap_or("".to_string());
let is_admin = sqlx::query_scalar!(
"DELETE FROM workspace_invite WHERE workspace_id = $1 AND email = $2 RETURNING is_admin",
let r = sqlx::query!(
"DELETE FROM workspace_invite WHERE workspace_id = $1 AND email = $2 RETURNING is_admin, operator",
nu.workspace_id,
email,
)
.fetch_optional(&mut tx)
.await?;
if let Some(is_admin) = is_admin {
tx = add_user_to_workspace(&nu.workspace_id, &email, &nu.username, is_admin, tx).await?;
let is_some = r.is_some();
if let Some(r) = r {
tx = add_user_to_workspace(
&nu.workspace_id,
&email,
&nu.username,
r.is_admin,
r.operator,
tx,
)
.await?;
audit_log(
&mut tx,
&nu.username,
"users.accept_invite",
ActionKind::Create,
&nu.workspace_id,
Some(&email),
None,
)
.await?;
tx.commit().await?;
}
audit_log(
&mut tx,
&nu.username,
"users.accept_invite",
ActionKind::Create,
&nu.workspace_id,
Some(&email),
None,
)
.await?;
tx.commit().await?;
if is_admin.is_some() {
if is_some {
Ok((
StatusCode::CREATED,
format!(
@@ -893,6 +1060,7 @@ async fn add_user_to_workspace<'c>(
email: &str,
username: &str,
is_admin: bool,
operator: bool,
mut tx: sqlx::Transaction<'c, sqlx::Postgres>,
) -> error::Result<sqlx::Transaction<'c, sqlx::Postgres>> {
let already_exists_username = sqlx::query_scalar!(
@@ -929,18 +1097,19 @@ async fn add_user_to_workspace<'c>(
sqlx::query!(
"INSERT INTO usr
(workspace_id, email, username, is_admin)
VALUES ($1, $2, $3, $4)",
(workspace_id, email, username, is_admin, operator)
VALUES ($1, $2, $3, $4, $5)",
&w_id,
email,
username,
is_admin
is_admin,
operator
)
.execute(&mut tx)
.await?;
sqlx::query_as!(
Group,
"INSERT INTO usr_to_group (workspace_id, usr, group_) VALUES ($1, $2, $3)",
"INSERT INTO usr_to_group (workspace_id, usr, group_) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
&w_id,
username,
"all",
@@ -981,6 +1150,28 @@ async fn update_workspace_user(
.await?;
}
if let Some(a) = eu.operator {
sqlx::query_scalar!(
"UPDATE usr SET operator = $1 WHERE username = $2 AND workspace_id = $3",
a,
&username_to_update,
&w_id
)
.execute(&mut tx)
.await?;
}
if let Some(a) = eu.disabled {
sqlx::query_scalar!(
"UPDATE usr SET disabled = $1 WHERE username = $2 AND workspace_id = $3",
a,
&username_to_update,
&w_id
)
.execute(&mut tx)
.await?;
}
audit_log(
&mut tx,
&username,
@@ -1003,7 +1194,7 @@ async fn update_user(
) -> Result<String> {
let mut tx = db.begin().await?;
require_super_admin(&mut tx, email.clone()).await?;
require_super_admin(&mut tx, &email).await?;
if let Some(sa) = eu.is_super_admin {
sqlx::query_scalar!(
@@ -1017,7 +1208,7 @@ async fn update_user(
audit_log(
&mut tx,
&email.unwrap(),
&email,
"users.update",
ActionKind::Update,
"global",
@@ -1036,33 +1227,38 @@ async fn delete_user(
) -> Result<String> {
let mut tx = db.begin().await?;
require_super_admin(&mut tx, email.clone()).await?;
let username = sqlx::query_scalar!(
"DELETE FROM usr WHERE email = $1 RETURNING username",
&email_to_delete
)
.fetch_one(&mut tx)
.await?;
require_super_admin(&mut tx, &email).await?;
sqlx::query!("DELETE FROM password WHERE email = $1", &email_to_delete)
.execute(&mut tx)
.await?;
sqlx::query!("DELETE FROM usr_to_group WHERE usr = $1", &username)
.execute(&mut tx)
.await?;
sqlx::query!(
"DELETE FROM workspace_invite WHERE email = $1",
let usernames = sqlx::query_scalar!(
"DELETE FROM usr WHERE email = $1 RETURNING username",
&email_to_delete
)
.execute(&mut tx)
.fetch_all(&mut tx)
.await?;
for username in usernames {
sqlx::query!("DELETE FROM password WHERE email = $1", &email_to_delete)
.execute(&mut tx)
.await?;
sqlx::query!("DELETE FROM usr_to_group WHERE usr = $1", &username)
.execute(&mut tx)
.await?;
sqlx::query!(
"DELETE FROM workspace_invite WHERE email = $1",
&email_to_delete
)
.execute(&mut tx)
.await?;
}
audit_log(
&mut tx,
&email.unwrap(),
&email,
"users.delete",
ActionKind::Delete,
"global",
@@ -1082,7 +1278,7 @@ async fn create_user(
) -> Result<(StatusCode, String)> {
let mut tx = db.begin().await?;
require_super_admin(&mut tx, email.clone()).await?;
require_super_admin(&mut tx, &email).await?;
sqlx::query!(
"INSERT INTO password(email, verified, password_hash, login_type, super_admin, name, \
@@ -1100,15 +1296,17 @@ async fn create_user(
audit_log(
&mut tx,
&email.unwrap(),
"users.update",
ActionKind::Update,
&email,
"users.add_global",
ActionKind::Create,
"global",
Some(&nu.email),
None,
)
.await?;
tx.commit().await?;
invite_user_to_all_auto_invite_worspaces(&db, &nu.email).await?;
Ok((StatusCode::CREATED, format!("email {} created", nu.email)))
}
@@ -1163,9 +1361,6 @@ async fn set_password(
Json(EditPassword { password }): Json<EditPassword>,
) -> Result<String> {
let mut tx = db.begin().await?;
let email = email
.ok_or("no_email")
.map_err(|e| Error::NotAuthorized(e.to_string()))?;
let custom_type = sqlx::query_scalar!(
"SELECT login_type::TEXT FROM password WHERE email = $1",
@@ -1437,9 +1632,7 @@ async fn create_token(
) -> Result<(StatusCode, String)> {
let token = rd_string(30);
let mut tx = db.begin().await?;
let email = email.ok_or_else(|| {
error::Error::BadRequest(format!("Only users with email can create tokens"))
})?;
let is_super_admin =
sqlx::query_scalar!("SELECT super_admin FROM password WHERE email = $1", email)
.fetch_optional(&mut tx)
@@ -1467,6 +1660,7 @@ async fn create_token(
Some(&token[0..10]),
None,
)
.instrument(tracing::info_span!("token", email = &email))
.await?;
tx.commit().await?;
Ok((StatusCode::CREATED, token))
@@ -1494,9 +1688,7 @@ async fn delete_token(
Path(token_prefix): Path<String>,
) -> Result<String> {
let mut tx = db.begin().await?;
let email = email.ok_or_else(|| {
error::Error::BadRequest(format!("Only users with email can create tokens"))
})?;
let tokens_deleted: Vec<String> = sqlx::query_scalar(
"DELETE FROM token
WHERE email = $1

View File

@@ -11,15 +11,12 @@ use windmill_common::error::{self, Error};
pub async fn require_super_admin<'c>(
db: &mut Transaction<'c, Postgres>,
email: Option<String>,
email: &str,
) -> error::Result<()> {
let is_admin = sqlx::query_scalar!(
"SELECT super_admin FROM password WHERE email = $1",
email.as_ref()
)
.fetch_one(db)
.await
.map_err(|e| Error::InternalErr(format!("fetching super admin: {e}")))?;
let is_admin = sqlx::query_scalar!("SELECT super_admin FROM password WHERE email = $1", email)
.fetch_one(db)
.await
.map_err(|e| Error::InternalErr(format!("fetching super admin: {e}")))?;
if !is_admin {
Err(Error::NotAuthorized(
"This endpoint require caller to be a super admin".to_owned(),

View File

@@ -11,7 +11,8 @@ use std::sync::Arc;
use crate::{
db::{UserDB, DB},
oauth2::{AllClients, _refresh_token},
users::Authed,
users::{require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookUtil},
BaseUrl,
};
/*
@@ -60,7 +61,7 @@ async fn list_contextual_variables(
get_reserved_variables(
&w_id,
"q1A0qcPuO00yxioll7iph76N9CJDqn",
&email.unwrap_or_else(|| "no email".to_string()),
&email,
&username,
"017e0ad5-f499-73b6-5488-92a61c5196dd",
format!("u/{username}").as_str(),
@@ -85,7 +86,8 @@ async fn list_variables(
"SELECT variable.workspace_id, variable.path, CASE WHEN is_secret IS TRUE THEN null ELSE variable.value::text END as value,
is_secret, variable.description, variable.extra_perms, account, is_oauth, (now() > account.expires_at) as is_expired,
account.refresh_error,
resource.path IS NOT NULL as is_linked
resource.path IS NOT NULL as is_linked,
account.refresh_token != '' as is_refreshed
from variable
LEFT JOIN account ON variable.account = account.id AND account.workspace_id = variable.workspace_id
LEFT JOIN resource ON resource.path = variable.path AND resource.workspace_id = variable.workspace_id
@@ -117,7 +119,8 @@ async fn get_variable(
let variable_o = sqlx::query_as::<_, ListableVariable>(
"SELECT variable.*, (now() > account.expires_at) as is_expired, account.refresh_error,
resource.path IS NOT NULL as is_linked
resource.path IS NOT NULL as is_linked,
account.refresh_token != '' as is_refreshed
from variable
LEFT JOIN account ON variable.account = account.id
LEFT JOIN resource ON resource.path = variable.path AND resource.workspace_id = variable.workspace_id
@@ -197,14 +200,38 @@ async fn exists_variable(
Ok(Json(exists))
}
async fn check_path_conflict<'c>(
tx: &mut Transaction<'c, Postgres>,
w_id: &str,
path: &str,
) -> Result<()> {
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM variable WHERE path = $1 AND workspace_id = $2)",
path,
w_id
)
.fetch_one(tx)
.await?
.unwrap_or(false);
if exists {
return Err(Error::BadRequest(format!(
"Variable {} already exists",
path
)));
}
return Ok(());
}
async fn create_variable(
authed: Authed,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
Path(w_id): Path<String>,
Json(variable): Json<CreateVariable>,
) -> Result<(StatusCode, String)> {
let mut tx = user_db.begin(&authed).await?;
check_path_conflict(&mut tx, &w_id, &variable.path).await?;
let value = if variable.is_secret {
let mc = build_crypt(&mut tx, &w_id).await?;
encrypt(&mc, &variable.value)
@@ -240,6 +267,11 @@ async fn create_variable(
tx.commit().await?;
webhook.send_message(WebhookMessage::CreateVariable {
workspace: w_id.clone(),
path: variable.path.clone(),
});
Ok((
StatusCode::CREATED,
format!("variable {} created", variable.path),
@@ -249,6 +281,7 @@ async fn create_variable(
async fn delete_variable(
authed: Authed,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
@@ -281,6 +314,11 @@ async fn delete_variable(
tx.commit().await?;
webhook.send_message(WebhookMessage::DeleteVariable {
workspace: w_id.clone(),
path: path.to_owned(),
});
Ok(format!("variable {} deleted", path))
}
@@ -295,6 +333,8 @@ struct EditVariable {
async fn update_variable(
authed: Authed,
Extension(user_db): Extension<UserDB>,
webhook: WebhookUtil,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(ns): Json<EditVariable>,
) -> Result<String> {
@@ -344,21 +384,27 @@ async fn update_variable(
}
sqlb.returning("path");
if let Some(npath) = ns.path {
if npath != path {
check_path_conflict(&mut tx, &w_id, &npath).await?;
if !authed.is_admin {
require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db).await?;
}
sqlx::query!(
"UPDATE resource SET path = $1 WHERE path = $2 AND workspace_id = $3",
npath,
path,
w_id
)
.execute(&mut tx)
.await?;
}
}
let sql = sqlb.sql().map_err(|e| Error::InternalErr(e.to_string()))?;
let npath_o: Option<String> = sqlx::query_scalar(&sql).fetch_optional(&mut tx).await?;
if let Some(npath) = ns.path {
sqlx::query!(
"UPDATE resource SET path = $1 WHERE path = $2 AND workspace_id = $3",
npath,
path,
w_id
)
.execute(&mut tx)
.await?;
}
let npath = not_found_if_none(npath_o, "Variable", path)?;
audit_log(
@@ -373,6 +419,12 @@ async fn update_variable(
.await?;
tx.commit().await?;
webhook.send_message(WebhookMessage::UpdateVariable {
workspace: w_id.clone(),
old_path: path.to_owned(),
new_path: npath.clone(),
});
Ok(format!("variable {} updated (npath: {:?})", path, npath))
}

View File

@@ -0,0 +1,156 @@
use std::time::Duration;
use axum::{
async_trait,
extract::{FromRequestParts, OriginalUri},
http::request::Parts,
Extension,
};
use hyper::StatusCode;
use serde::Serialize;
use tokio::{select, sync::mpsc};
use crate::db::DB;
lazy_static::lazy_static! {
// TODO: these aren't synced, they should be moved into the queue abstraction once/if that happens.
static ref WEBHOOK_REQUEST_COUNT: prometheus::Histogram = prometheus::register_histogram!(
"webhook_request",
"Histogram of webhook requests made"
)
.unwrap();
}
#[derive(Serialize)]
#[serde(tag = "type")]
pub enum WebhookMessage {
// See https://serde.rs/enum-representations.html#internally-tagged for how this looks in JSON
CreateApp { workspace: String, path: String },
DeleteApp { workspace: String, path: String },
UpdateApp { workspace: String, old_path: String, new_path: String },
CreateFlow { workspace: String, path: String },
UpdateFlow { workspace: String, old_path: String, new_path: String },
ArchiveFlow { workspace: String, path: String },
CreateFolder { workspace: String, name: String },
UpdateFolder { workspace: String, name: String },
DeleteFolder { workspace: String, name: String },
DeleteResource { workspace: String, path: String },
CreateResource { workspace: String, path: String },
UpdateResource { workspace: String, old_path: String, new_path: String },
CreateResourceType { name: String },
DeleteResourceType { name: String },
UpdateResourceType { name: String },
CreateScript { workspace: String, path: String, hash: String },
UpdateScript { workspace: String, path: String, hash: String },
DeleteScript { workspace: String, hash: String },
CreateVariable { workspace: String, path: String },
UpdateVariable { workspace: String, old_path: String, new_path: String },
DeleteVariable { workspace: String, path: String },
}
#[derive(Clone)]
pub struct WebhookShared {
pub channel: mpsc::UnboundedSender<(String, WebhookMessage)>,
}
impl WebhookShared {
pub fn new(mut shutdown_rx: tokio::sync::broadcast::Receiver<()>) -> Self {
let (tx, mut rx) = mpsc::unbounded_channel::<(String, WebhookMessage)>();
let _process = tokio::spawn(async move {
let client = reqwest::Client::builder()
// TODO: investigate pool timeouts and such if TCP load is high
.timeout(Duration::from_secs(5))
.build()
.unwrap();
loop {
select! {
biased;
_ = shutdown_rx.recv() => break,
r = rx.recv() => match r {
Some((url, message)) => {
let timer = WEBHOOK_REQUEST_COUNT.start_timer();
let _ = client.post(url).json(&message).send().await;
timer.stop_and_record();
},
None => break,
}
}
}
});
Self { channel: tx }
}
}
#[derive(Clone)]
pub struct WebhookUtil {
webhook: Option<String>,
shared: Extension<WebhookShared>,
}
impl WebhookUtil {
pub fn send_message(&self, message: WebhookMessage) {
let Some(webhook) = &self.webhook else {
return;
};
let _ = self.shared.channel.send((webhook.clone(), message));
}
}
#[async_trait]
impl<S> FromRequestParts<S> for WebhookUtil
where
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> std::result::Result<Self, Self::Rejection> {
let original_uri = OriginalUri::from_request_parts(parts, state)
.await
.ok()
.map(|x| x.0)
.unwrap_or_default();
let path_vec: Vec<&str> = original_uri.path().split("/").collect();
let workspace_id = if path_vec.len() >= 4 && path_vec[0] == "" && path_vec[2] == "w" {
Some(path_vec[3].to_owned())
} else {
None
};
let webhook = sqlx::query_scalar!(
"SELECT webhook FROM workspace_settings WHERE workspace_id = $1",
workspace_id
)
.fetch_one(
&Extension::<DB>::from_request_parts(parts, state)
.await
.map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"Could not aquire DB while retrieving webhook".to_owned(),
)
})?
.0,
)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Could not execute DB query {:?}", e),
)
})?;
let shared = Extension::<WebhookShared>::from_request_parts(parts, state)
.await
.map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"Could not aquire shared process while retrieving webhook".to_owned(),
)
})?;
Ok(Self { webhook, shared })
}
}

View File

@@ -6,23 +6,28 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::{str::FromStr, sync::Arc};
use crate::{
db::{UserDB, DB},
folders::Folder,
resources::{Resource, ResourceType},
users::{Authed, WorkspaceInvite},
utils::require_super_admin,
BaseUrl,
};
use axum::{
body::StreamBody,
extract::{Extension, Path, Query},
headers,
response::IntoResponse,
response::{IntoResponse, Redirect},
routing::{delete, get, post},
Json, Router,
};
use stripe::CustomerId;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{Error, JsonResult, Result},
error::{to_anyhow, Error, JsonResult, Result},
flows::Flow,
scripts::{Schema, Script, ScriptLang},
utils::{paginate, rd_string, require_admin, Pagination},
@@ -31,7 +36,7 @@ use windmill_common::{
use hyper::{header, StatusCode};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use sqlx::{FromRow, Postgres, Transaction};
use tempfile::TempDir;
use tokio::fs::File;
use tokio_util::io::ReaderStream;
@@ -40,14 +45,19 @@ pub fn workspaced_service() -> Router {
Router::new()
.route("/list_pending_invites", get(list_pending_invites))
.route("/update", post(edit_workspace))
.route("/delete", delete(delete_workspace))
.route("/archive", post(archive_workspace))
.route("/invite_user", post(invite_user))
.route("/add_user", post(add_user))
.route("/delete_invite", post(delete_invite))
.route("/get_settings", get(get_settings))
.route("/edit_slack_command", post(edit_slack_command))
.route("/edit_webhook", post(edit_webhook))
.route("/edit_auto_invite", post(edit_auto_invite))
.route("/tarball", get(tarball_workspace))
.route("/premium_info", get(premium_info))
.route("/checkout", get(stripe_checkout))
.route("/billing_portal", get(stripe_portal))
}
pub fn global_service() -> Router {
Router::new()
.route("/list_as_superadmin", get(list_workspaces_as_super_admin))
@@ -56,6 +66,9 @@ pub fn global_service() -> Router {
.route("/create", post(create_workspace))
.route("/exists", post(exists_workspace))
.route("/exists_username", post(exists_username))
.route("/allowed_domain_auto_invite", get(is_allowed_auto_domain))
.route("/unarchive/:workspace", post(unarchive_workspace))
.route("/delete/:workspace", delete(delete_workspace))
}
#[derive(FromRow, Serialize)]
@@ -63,7 +76,6 @@ struct Workspace {
id: String,
name: String,
owner: String,
domain: Option<String>,
deleted: bool,
premium: bool,
}
@@ -74,6 +86,21 @@ pub struct WorkspaceSettings {
pub slack_team_id: Option<String>,
pub slack_name: Option<String>,
pub slack_command_script: Option<String>,
pub slack_email: String,
pub auto_invite_domain: Option<String>,
pub auto_invite_operator: Option<bool>,
pub customer_id: Option<String>,
pub plan: Option<String>,
pub webhook: Option<String>,
}
#[derive(FromRow, Serialize, Debug)]
pub struct Usage {
pub workspace_id: String,
pub slack_team_id: Option<String>,
pub slack_name: Option<String>,
pub slack_command_script: Option<String>,
pub slack_email: String,
}
#[derive(sqlx::Type, Serialize, Deserialize, Debug)]
@@ -86,19 +113,28 @@ pub enum WorkspaceKeyKind {
struct EditCommandScript {
slack_command_script: Option<String>,
}
#[derive(Deserialize)]
struct EditAutoInvite {
operator: Option<bool>,
}
#[derive(Deserialize)]
struct EditWebhook {
webhook: Option<String>,
}
#[derive(Deserialize)]
struct CreateWorkspace {
id: String,
name: String,
username: String,
domain: Option<String>,
}
#[derive(Deserialize)]
struct EditWorkspace {
name: String,
owner: String,
domain: Option<String>,
}
#[derive(Serialize)]
@@ -129,6 +165,15 @@ struct ValidateUsername {
pub struct NewWorkspaceInvite {
pub email: String,
pub is_admin: bool,
pub operator: bool,
}
#[derive(Deserialize)]
pub struct NewWorkspaceUser {
pub email: String,
pub username: String,
pub is_admin: bool,
pub operator: bool,
}
async fn list_pending_invites(
@@ -149,6 +194,160 @@ async fn list_pending_invites(
Ok(Json(rows))
}
#[derive(Serialize, FromRow)]
pub struct PremiumWorkspaceInfo {
pub premium: bool,
pub usage: Option<i32>,
}
async fn premium_info(
authed: Authed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
) -> JsonResult<PremiumWorkspaceInfo> {
require_admin(authed.is_admin, &authed.username)?;
let mut tx = db.begin().await?;
let row = sqlx::query_as::<_, PremiumWorkspaceInfo>(
"SELECT premium, usage.usage FROM workspace LEFT JOIN usage ON workspace.id = usage.id AND usage.is_workspace IS true WHERE workspace.id = $1",
)
.bind(w_id)
.fetch_one(&mut tx)
.await?;
tx.commit().await?;
Ok(Json(row))
}
#[derive(Deserialize)]
struct PlanQuery {
plan: String,
}
async fn stripe_checkout(
authed: Authed,
Path(w_id): Path<String>,
Query(plan): Query<PlanQuery>,
Extension(base_url): Extension<Arc<BaseUrl>>,
) -> Result<Redirect> {
// #[cfg(feature = "enterprise")]
{
require_admin(authed.is_admin, &authed.username)?;
let client = stripe::Client::new(std::env::var("STRIPE_KEY").expect("STRIPE_KEY"));
let success_rd = format!("{}/workspace_settings/checkout?success=true", base_url.0);
let failure_rd = format!("{}/workspace_settings/checkout?success=false", base_url.0);
let checkout_session = {
let mut params = stripe::CreateCheckoutSession::new(&failure_rd, &success_rd);
params.mode = Some(stripe::CheckoutSessionMode::Subscription);
params.line_items = match plan.plan.as_str() {
"team" => Some(vec![
stripe::CreateCheckoutSessionLineItems {
quantity: None,
price: Some("price_1MSdSyGU3NdFi9eLMdV6cS6F".to_string()),
..Default::default()
},
stripe::CreateCheckoutSessionLineItems {
quantity: None,
price: Some("price_1MShsNGU3NdFi9eLJMEZUW8b".to_string()),
..Default::default()
},
]),
"enterprise" => Some(vec![
stripe::CreateCheckoutSessionLineItems {
quantity: None,
price: Some("price_1MSdf6GU3NdFi9eLJFRkntlx".to_string()),
..Default::default()
},
stripe::CreateCheckoutSessionLineItems {
quantity: None,
price: Some("price_1MShsNGU3NdFi9eLJMEZUW8b".to_string()),
..Default::default()
},
]),
_ => Err(Error::BadRequest("invalid plan".to_string()))?,
};
params.customer_email = Some(&authed.email);
params.client_reference_id = Some(&w_id);
stripe::CheckoutSession::create(&client, params)
.await
.unwrap()
};
let uri = checkout_session
.url
.ok_or_else(|| Error::InternalErr(format!("stripe checkout redirect issue")))?;
Ok(Redirect::to(&uri))
}
}
async fn stripe_portal(
authed: Authed,
Path(w_id): Path<String>,
Extension(db): Extension<DB>,
Extension(base_url): Extension<Arc<BaseUrl>>,
) -> Result<Redirect> {
require_admin(authed.is_admin, &authed.username)?;
let customer_id = sqlx::query_scalar!(
"SELECT customer_id FROM workspace_settings WHERE workspace_id = $1",
w_id
)
.fetch_one(&db)
.await?
.ok_or_else(|| Error::InternalErr(format!("no customer id for workspace {}", w_id)))?;
let client = stripe::Client::new(std::env::var("STRIPE_KEY").expect("STRIPE_KEY"));
let success_rd = format!("{}/workspace_settings?tab=premium", base_url.0);
let portal_session = {
let customer_id = CustomerId::from_str(&customer_id).unwrap();
let mut params = stripe::CreateBillingPortalSession::new(customer_id);
params.return_url = Some(&success_rd);
stripe::BillingPortalSession::create(&client, params)
.await
.map_err(to_anyhow)?
};
Ok(Redirect::to(&portal_session.url))
}
// async fn stripe_usage(
// authed: Authed,
// Path(w_id): Path<String>,
// Extension(db): Extension<DB>,
// Extension(base_url): Extension<Arc<BaseUrl>>,
// ) -> Result<Redirect> {
// require_admin(authed.is_admin, &authed.username)?;
// let customer_id = sqlx::query_scalar!(
// "SELECT customer_id FROM workspace_settings WHERE workspace_id = $1",
// w_id
// )
// .fetch_one(&db)
// .await?
// .ok_or_else(|| Error::InternalErr(format!("no customer id for workspace {}", w_id)))?;
// let client = stripe::Client::new(std::env::var("STRIPE_KEY").expect("STRIPE_KEY"));
// let success_rd = format!("{}/workspace_settings?tab=premium", base_url.0);
// let portal_session = {
// let customer_id = CustomerId::from_str(&customer_id).unwrap();
// let subscriptions = stripe::Subscription::list(
// &client,
// stripe::ListSubscriptions { customer: Some(customer_id), ..Default::default() },
// )
// .await
// .map_err(to_anyhow)?
// .data[0];
// let getUsage =
// stripe::SubscriptionItem::list(
// &client,
// stripe::ListSubscriptionItems {
// subscription: subscription.id,
// ..Default::default()
// },
// )
// .await
// .map_err(to_anyhow)
// };
// let mut params = stripe::ListSubscriptionItems::new(customer_id);
// params.return_url = Some(&success_rd);
// stripe::BillingPortalSession::create(&client, params)
// .await
// .map_err(to_anyhow)?
// };
// }
async fn exists_workspace(
authed: Authed,
Extension(user_db): Extension<UserDB>,
@@ -175,7 +374,7 @@ async fn list_workspaces(
Workspace,
"SELECT workspace.* FROM workspace, usr WHERE usr.workspace_id = workspace.id AND \
usr.email = $1 AND deleted = false",
authed.email.as_ref()
authed.email
)
.fetch_all(&mut tx)
.await?;
@@ -225,7 +424,7 @@ async fn edit_slack_command(
"workspaces.edit_command_script",
ActionKind::Update,
&w_id,
Some(&authed.email.unwrap()),
Some(&authed.email),
Some(
[(
"script",
@@ -242,6 +441,123 @@ async fn edit_slack_command(
Ok(format!("Edit command script {}", &w_id))
}
const BANNED_DOMAINS: &str = include_str!("../banned_domains.txt");
async fn is_allowed_auto_domain(Authed { email, .. }: Authed) -> JsonResult<bool> {
let domain = email.split('@').last().unwrap();
return Ok(Json(!BANNED_DOMAINS.contains(domain)));
}
async fn edit_auto_invite(
authed: Authed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Authed { is_admin, email, username, .. }: Authed,
Json(ea): Json<EditAutoInvite>,
) -> Result<String> {
require_admin(is_admin, &username)?;
let domain = email.split('@').last().unwrap();
let mut tx = db.begin().await?;
if let Some(operator) = ea.operator {
if BANNED_DOMAINS.contains(domain) {
return Err(Error::BadRequest(format!(
"Domain {} is not allowed",
domain
)));
}
sqlx::query!(
"UPDATE workspace_settings SET auto_invite_domain = $1, auto_invite_operator = $2 WHERE workspace_id = $3",
domain,
operator,
&w_id
)
.execute(&mut tx)
.await?;
sqlx::query!(
"INSERT INTO workspace_invite
(workspace_id, email, is_admin, operator)
SELECT $1::text, email, false, $3 FROM password WHERE email LIKE CONCAT('%', $2::text) AND NOT EXISTS (
SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email
)
ON CONFLICT DO NOTHING",
&w_id,
&domain,
operator
)
.execute(&mut tx)
.await?;
} else {
sqlx::query!(
"UPDATE workspace_settings SET auto_invite_domain = NULL, auto_invite_operator = NULL WHERE workspace_id = $1",
&w_id,
)
.execute(&mut tx)
.await?;
}
audit_log(
&mut tx,
&authed.username,
"workspaces.edit_auto_invite_domain",
ActionKind::Update,
&w_id,
Some(&authed.email),
Some([("operator", &format!("{:?}", ea.operator)[..])].into()),
)
.await?;
tx.commit().await?;
Ok(format!(
"Edit auto-invite for workspace {} to {}",
&w_id, domain
))
}
async fn edit_webhook(
authed: Authed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Authed { is_admin, username, .. }: Authed,
Json(ew): Json<EditWebhook>,
) -> Result<String> {
require_admin(is_admin, &username)?;
let mut tx = db.begin().await?;
if let Some(webhook) = &ew.webhook {
sqlx::query!(
"UPDATE workspace_settings SET webhook = $1 WHERE workspace_id = $2",
webhook,
&w_id
)
.execute(&mut tx)
.await?;
} else {
sqlx::query!(
"UPDATE workspace_settings SET webhook = NULL WHERE workspace_id = $1",
&w_id,
)
.execute(&mut tx)
.await?;
}
audit_log(
&mut tx,
&authed.username,
"workspaces.edit_webhook",
ActionKind::Update,
&w_id,
Some(&authed.email),
Some([("webhook", &format!("{:?}", ew.webhook)[..])].into()),
)
.await?;
tx.commit().await?;
Ok(format!("Edit webhook for workspace {}", &w_id))
}
async fn list_workspaces_as_super_admin(
authed: Authed,
Extension(user_db): Extension<UserDB>,
@@ -249,7 +565,7 @@ async fn list_workspaces_as_super_admin(
Authed { email, .. }: Authed,
) -> JsonResult<Vec<Workspace>> {
let mut tx = user_db.begin(&authed).await?;
require_super_admin(&mut tx, email).await?;
require_super_admin(&mut tx, &email).await?;
let (per_page, offset) = paginate(pagination);
let workspaces = sqlx::query_as!(
@@ -268,9 +584,6 @@ async fn user_workspaces(
Extension(db): Extension<DB>,
Authed { email, .. }: Authed,
) -> JsonResult<WorkspaceList> {
let email = email
.ok_or("not a personal token")
.map_err(|x| Error::NotAuthorized(x.to_string()))?;
let mut tx = db.begin().await?;
let workspaces = sqlx::query_as!(
UserWorkspace,
@@ -285,6 +598,20 @@ async fn user_workspaces(
Ok(Json(WorkspaceList { email, workspaces }))
}
async fn check_name_conflict<'c>(tx: &mut Transaction<'c, Postgres>, w_id: &str) -> Result<()> {
let exists = sqlx::query_scalar!("SELECT EXISTS(SELECT 1 FROM workspace WHERE id = $1)", w_id)
.fetch_one(tx)
.await?
.unwrap_or(false);
if exists {
return Err(windmill_common::error::Error::BadRequest(format!(
"Workspace {} already exists",
w_id
)));
}
return Ok(());
}
async fn create_workspace(
authed: Authed,
Extension(db): Extension<DB>,
@@ -294,14 +621,14 @@ async fn create_workspace(
return Err(Error::BadRequest("bot is a reserved username".to_string()));
}
let mut tx = db.begin().await?;
check_name_conflict(&mut tx, &nw.id).await?;
sqlx::query!(
"INSERT INTO workspace
(id, name, owner, domain)
VALUES ($1, $2, $3, $4)",
(id, name, owner)
VALUES ($1, $2, $3)",
nw.id,
nw.name,
authed.email,
nw.domain
)
.execute(&mut tx)
.await?;
@@ -377,7 +704,6 @@ async fn create_workspace(
)
.await?;
tx.commit().await?;
Ok(format!("Created workspace {}", &nw.id))
}
@@ -391,10 +717,9 @@ async fn edit_workspace(
require_admin(is_admin, &username)?;
let mut tx = db.begin().await?;
sqlx::query!(
"UPDATE workspace SET name = $1, owner = $2, domain = $3 WHERE id = $4",
"UPDATE workspace SET name = $1, owner = $2 WHERE id = $3",
ew.name,
ew.owner,
ew.domain,
&w_id
)
.execute(&mut tx)
@@ -406,14 +731,8 @@ async fn edit_workspace(
"workspaces.update",
ActionKind::Update,
&w_id,
Some(&authed.email.unwrap()),
Some(
[(
"domain",
ew.domain.unwrap_or("NO_DOMAIN".to_string()).as_str(),
)]
.into(),
),
Some(&authed.email),
None,
)
.await?;
tx.commit().await?;
@@ -421,7 +740,7 @@ async fn edit_workspace(
Ok(format!("Updated workspace {}", &w_id))
}
async fn delete_workspace(
async fn archive_workspace(
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Authed { is_admin, username, email, .. }: Authed,
@@ -435,10 +754,130 @@ async fn delete_workspace(
audit_log(
&mut tx,
&username,
"workspaces.delete",
"workspaces.archive",
ActionKind::Update,
&w_id,
Some(&email.unwrap_or("noemail".to_string())),
Some(&email),
None,
)
.await?;
tx.commit().await?;
Ok(format!("Archived workspace {}", &w_id))
}
async fn unarchive_workspace(
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Authed { is_admin, username, email, .. }: Authed,
) -> Result<String> {
require_admin(is_admin, &username)?;
let mut tx = db.begin().await?;
sqlx::query!("UPDATE workspace SET deleted = false WHERE id = $1", &w_id)
.execute(&mut tx)
.await?;
audit_log(
&mut tx,
&username,
"workspaces.unarchive",
ActionKind::Update,
&w_id,
Some(&email),
None,
)
.await?;
tx.commit().await?;
Ok(format!("Unarchived workspace {}", &w_id))
}
async fn delete_workspace(
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Authed { username, email, .. }: Authed,
) -> Result<String> {
let w_id = match w_id.as_str() {
"starter" => Err(Error::BadRequest(
"starter workspace cannot be deleted".to_string(),
)),
"admins" => Err(Error::BadRequest(
"admins workspace cannot be deleted".to_string(),
)),
_ => Ok(w_id),
}?;
let mut tx = db.begin().await?;
require_super_admin(&mut tx, &email).await?;
sqlx::query!("DELETE FROM script WHERE workspace_id = $1", &w_id)
.execute(&mut tx)
.await?;
sqlx::query!("DELETE FROM flow WHERE workspace_id = $1", &w_id)
.execute(&mut tx)
.await?;
sqlx::query!("DELETE FROM app WHERE workspace_id = $1", &w_id)
.execute(&mut tx)
.await?;
sqlx::query!("DELETE FROM variable WHERE workspace_id = $1", &w_id)
.execute(&mut tx)
.await?;
sqlx::query!("DELETE FROM resource WHERE workspace_id = $1", &w_id)
.execute(&mut tx)
.await?;
sqlx::query!("DELETE FROM schedule WHERE workspace_id = $1", &w_id)
.execute(&mut tx)
.await?;
sqlx::query!("DELETE FROM completed_job WHERE workspace_id = $1", &w_id)
.execute(&mut tx)
.await?;
sqlx::query!("DELETE FROM usr WHERE workspace_id = $1", &w_id)
.execute(&mut tx)
.await?;
sqlx::query!(
"DELETE FROM workspace_invite WHERE workspace_id = $1",
&w_id
)
.execute(&mut tx)
.await?;
sqlx::query!("DELETE FROM usr_to_group WHERE workspace_id = $1", &w_id)
.execute(&mut tx)
.await?;
sqlx::query!("DELETE FROM group_ WHERE workspace_id = $1", &w_id)
.execute(&mut tx)
.await?;
sqlx::query!("DELETE FROM folder WHERE workspace_id = $1", &w_id)
.execute(&mut tx)
.await?;
sqlx::query!("DELETE FROM workspace_key WHERE workspace_id = $1", &w_id)
.execute(&mut tx)
.await?;
sqlx::query!(
"DELETE FROM workspace_settings WHERE workspace_id = $1",
&w_id
)
.execute(&mut tx)
.await?;
sqlx::query!("DELETE FROM workspace WHERE id = $1", &w_id)
.execute(&mut tx)
.await?;
audit_log(
&mut tx,
&username,
"workspaces.delete",
ActionKind::Delete,
&w_id,
Some(&email),
None,
)
.await?;
@@ -447,6 +886,32 @@ async fn delete_workspace(
Ok(format!("Deleted workspace {}", &w_id))
}
pub async fn invite_user_to_all_auto_invite_worspaces(db: &DB, email: &str) -> Result<()> {
let mut tx = db.begin().await?;
let domain = email.split('@').last().unwrap();
let workspaces = sqlx::query!(
"SELECT workspace_id, auto_invite_operator FROM workspace_settings WHERE auto_invite_domain = $1",
domain
)
.fetch_all(&mut tx)
.await?;
for r in workspaces {
sqlx::query!(
"INSERT INTO workspace_invite
(workspace_id, email, is_admin, operator)
VALUES ($1, $2, false, $3)
ON CONFLICT DO NOTHING",
r.workspace_id,
email,
r.auto_invite_operator
)
.execute(&mut tx)
.await?;
}
tx.commit().await?;
Ok(())
}
async fn invite_user(
Authed { username, is_admin, .. }: Authed,
Extension(db): Extension<DB>,
@@ -459,11 +924,12 @@ async fn invite_user(
sqlx::query!(
"INSERT INTO workspace_invite
(workspace_id, email, is_admin)
VALUES ($1, $2, $3)",
(workspace_id, email, is_admin, operator)
VALUES ($1, $2, $3, $4)",
&w_id,
nu.email,
nu.is_admin
nu.is_admin,
nu.operator
)
.execute(&mut tx)
.await?;
@@ -476,6 +942,37 @@ async fn invite_user(
))
}
async fn add_user(
Authed { username, is_admin, .. }: Authed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Json(nu): Json<NewWorkspaceUser>,
) -> Result<(StatusCode, String)> {
require_admin(is_admin, &username)?;
let mut tx = db.begin().await?;
sqlx::query!(
"INSERT INTO usr
(workspace_id, email, username, is_admin, operator)
VALUES ($1, $2, $3, $4, $5)",
&w_id,
nu.email,
nu.username,
nu.is_admin,
nu.operator
)
.execute(&mut tx)
.await?;
tx.commit().await?;
Ok((
StatusCode::CREATED,
format!("user with email {} added", nu.email),
))
}
async fn delete_invite(
Authed { username, is_admin, .. }: Authed,
Extension(db): Extension<DB>,
@@ -488,10 +985,11 @@ async fn delete_invite(
sqlx::query!(
"DELETE FROM workspace_invite WHERE
workspace_id = $1 AND email = $2 AND is_admin = $3",
workspace_id = $1 AND email = $2 AND is_admin = $3 AND operator = $4",
&w_id,
nu.email,
nu.is_admin
nu.is_admin,
nu.operator
)
.execute(&mut tx)
.await?;
@@ -547,6 +1045,22 @@ async fn tarball_workspace(
let file = File::create(&file_path).await?;
let mut a = tokio_tar::Builder::new(file);
{
let folders = sqlx::query_as::<_, Folder>("SELECT * FROM folder WHERE workspace_id = $1")
.bind(&w_id)
.fetch_all(&db)
.await?;
for folder in folders {
write_to_archive(
serde_json::to_string_pretty(&folder).unwrap(),
format!("f/{}/folder.meta.json", folder.name),
&mut a,
)
.await?;
}
}
{
let scripts = sqlx::query_as::<_, Script>(
"SELECT * FROM script as o WHERE workspace_id = $1 AND archived = false
@@ -564,12 +1078,7 @@ async fn tarball_workspace(
ScriptLang::Go => "go",
ScriptLang::Bash => "sh",
};
write_to_archive(
script.content,
format!("scripts/{}.{}", script.path, ext),
&mut a,
)
.await?;
write_to_archive(script.content, format!("{}.{}", script.path, ext), &mut a).await?;
let lock = script
.lock
@@ -585,12 +1094,7 @@ async fn tarball_workspace(
lock,
};
let metadata_str = serde_json::to_string_pretty(&metadata).unwrap();
write_to_archive(
metadata_str,
format!("scripts/{}.json", script.path),
&mut a,
)
.await?;
write_to_archive(metadata_str, format!("{}.script.json", script.path), &mut a).await?;
}
}
@@ -607,7 +1111,7 @@ async fn tarball_workspace(
let resource_str = serde_json::to_string_pretty(&resource).unwrap();
write_to_archive(
resource_str,
format!("resources/{}.json", resource.path),
format!("{}.resource.json", resource.path),
&mut a,
)
.await?;
@@ -627,7 +1131,7 @@ async fn tarball_workspace(
let resource_str = serde_json::to_string_pretty(&resource_type).unwrap();
write_to_archive(
resource_str,
format!("resource_types/{}.json", resource_type.name),
format!("{}.resource-type.json", resource_type.name),
&mut a,
)
.await?;
@@ -644,7 +1148,7 @@ async fn tarball_workspace(
for flow in flows {
let flow_str = serde_json::to_string_pretty(&flow).unwrap();
write_to_archive(flow_str, format!("flows/{}.json", flow.path), &mut a).await?;
write_to_archive(flow_str, format!("{}.flow.json", flow.path), &mut a).await?;
}
}
@@ -658,7 +1162,7 @@ async fn tarball_workspace(
for var in variables {
let flow_str = serde_json::to_string_pretty(&var).unwrap();
write_to_archive(flow_str, format!("variables/{}.json", var.path), &mut a).await?;
write_to_archive(flow_str, format!("{}.variable.json", var.path), &mut a).await?;
}
}
a.into_inner().await?;

View File

@@ -0,0 +1,14 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2022
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
use serde::Deserialize;
#[derive(Deserialize)]
pub struct ListAppQuery {
pub starred_only: Option<bool>,
}

View File

@@ -10,7 +10,7 @@
use axum::{
body::{self, BoxBody},
response::IntoResponse,
Json,
response::Json,
};
#[cfg(feature = "sqlx")]
@@ -58,6 +58,8 @@ pub enum Error {
ExitStatus(i32),
#[error(transparent)]
Anyhow(#[from] anyhow::Error),
#[error("Error: {0:#?}")]
JsonErr(serde_json::Value),
}
impl Error {

View File

@@ -24,7 +24,7 @@ pub const MAX_RETRY_INTERVAL: Duration = HOURS.saturating_mul(6);
pub struct FlowStatus {
pub step: i32,
pub modules: Vec<FlowStatusModule>,
pub failure_module: FlowStatusModule,
pub failure_module: FlowStatusModuleWParent,
#[serde(default)]
#[serde(skip_serializing_if = "is_default")]
pub retry: RetryStatus,
@@ -67,6 +67,14 @@ pub struct Approval {
pub approver: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct FlowStatusModuleWParent {
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_module: Option<String>,
#[serde(flatten)]
pub module_status: FlowStatusModule,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum FlowStatusModule {
@@ -171,12 +179,15 @@ impl FlowStatus {
.iter()
.map(|m| FlowStatusModule::WaitingForPriorSteps { id: m.id.clone() })
.collect(),
failure_module: FlowStatusModule::WaitingForPriorSteps {
id: f
.failure_module
.as_ref()
.map(|x| x.id.clone())
.unwrap_or_else(|| "failure".to_string()),
failure_module: FlowStatusModuleWParent {
parent_module: None,
module_status: FlowStatusModule::WaitingForPriorSteps {
id: f
.failure_module
.as_ref()
.map(|x| x.id.clone())
.unwrap_or_else(|| "failure".to_string()),
},
},
retry: RetryStatus { fail_count: 0, previous_result: None, failed_jobs: vec![] },
}

View File

@@ -11,8 +11,10 @@ use std::{collections::HashMap, time::Duration};
use serde::{self, Deserialize, Serialize};
use crate::{
more_serde::{default_false, default_id, default_true, is_default},
scripts::{Schema, ScriptLang},
more_serde::{
default_empty_string, default_false, default_id, default_null, default_true, is_default,
},
scripts::{Schema, ScriptHash, ScriptLang},
};
#[derive(Serialize)]
@@ -160,14 +162,26 @@ pub struct FlowModule {
pub sleep: Option<InputTransform>,
}
impl FlowModule {
pub fn id_append(&mut self, s: &str) {
self.id = format!("{}-{}", self.id, s);
}
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
#[serde(
tag = "type",
rename_all(serialize = "lowercase", deserialize = "lowercase")
)]
pub enum InputTransform {
Static { value: serde_json::Value },
Javascript { expr: String },
Static {
#[serde(default = "default_null")]
value: serde_json::Value,
},
Javascript {
#[serde(default = "default_empty_string")]
expr: String,
},
}
#[derive(Serialize, Deserialize, Debug, Clone)]
@@ -200,6 +214,14 @@ pub enum FlowModuleValue {
#[serde(alias = "input_transform")]
input_transforms: HashMap<String, InputTransform>,
path: String,
#[serde(skip_serializing_if = "Option::is_none")]
hash: Option<ScriptHash>,
},
Flow {
#[serde(default)]
#[serde(alias = "input_transform")]
input_transforms: HashMap<String, InputTransform>,
path: String,
},
ForloopFlow {
iterator: InputTransform,

View File

@@ -10,12 +10,14 @@ use std::net::SocketAddr;
use error::Error;
pub mod apps;
pub mod error;
pub mod external_ip;
pub mod flow_status;
pub mod flows;
pub mod more_serde;
pub mod oauth2;
pub mod schedule;
pub mod scripts;
pub mod users;
pub mod utils;
@@ -27,7 +29,8 @@ pub mod tracing_init;
pub const DEFAULT_NUM_WORKERS: usize = 3;
pub const DEFAULT_TIMEOUT: i32 = 300;
pub const DEFAULT_SLEEP_QUEUE: u64 = 50;
pub const DEFAULT_MAX_CONNECTIONS: u32 = 100;
pub const DEFAULT_MAX_CONNECTIONS_SERVER: u32 = 50;
pub const DEFAULT_MAX_CONNECTIONS_WORKER: u32 = 3;
#[cfg(feature = "tokio")]
pub async fn shutdown_signal(tx: tokio::sync::broadcast::Sender<()>) -> anyhow::Result<()> {
@@ -77,7 +80,7 @@ async fn metrics() -> Result<String, Error> {
}
#[cfg(feature = "sqlx")]
pub async fn connect_db() -> anyhow::Result<sqlx::Pool<sqlx::Postgres>> {
pub async fn connect_db(server_mode: bool) -> anyhow::Result<sqlx::Pool<sqlx::Postgres>> {
use anyhow::Context;
let database_url = std::env::var("DATABASE_URL")
@@ -85,7 +88,13 @@ pub async fn connect_db() -> anyhow::Result<sqlx::Pool<sqlx::Postgres>> {
let max_connections = match std::env::var("DATABASE_CONNECTIONS") {
Ok(n) => n.parse::<u32>().context("invalid DATABASE_CONNECTIONS")?,
Err(_) => DEFAULT_MAX_CONNECTIONS,
Err(_) => {
if server_mode {
DEFAULT_MAX_CONNECTIONS_SERVER
} else {
DEFAULT_MAX_CONNECTIONS_WORKER
}
}
};
Ok(connect(&database_url, max_connections).await?)
@@ -99,6 +108,7 @@ pub async fn connect(
use std::time::Duration;
sqlx::postgres::PgPoolOptions::new()
.min_connections(3)
.max_connections(max_connections)
.max_lifetime(Duration::from_secs(30 * 60)) // 30 mins
.connect(database_url)

View File

@@ -18,6 +18,14 @@ pub fn default_false() -> bool {
false
}
pub fn default_null() -> serde_json::Value {
serde_json::Value::Null
}
pub fn default_empty_string() -> String {
String::new()
}
pub fn default_id() -> String {
rd_string(6)
}

View File

@@ -0,0 +1,28 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2022
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
use chrono::DateTime;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
#[derive(FromRow, Serialize, Deserialize, Debug)]
pub struct Schedule {
pub workspace_id: String,
pub path: String,
pub edited_by: String,
pub edited_at: DateTime<chrono::Utc>,
pub schedule: String,
pub offset_: i32,
pub enabled: bool,
pub script_path: String,
pub is_flow: bool,
pub args: Option<serde_json::Value>,
pub extra_perms: serde_json::Value,
pub email: String,
pub error: Option<String>,
}

View File

@@ -208,11 +208,9 @@ pub fn to_hex_string(i: &i64) -> String {
#[cfg(feature = "reqwest")]
pub async fn get_hub_script_by_path(
email: Option<String>,
username: String,
email: &str,
path: StripPath,
http_client: reqwest::Client,
host: String,
) -> crate::error::Result<String> {
use crate::{
error::{to_anyhow, Error},
@@ -228,8 +226,6 @@ pub async fn get_hub_script_by_path(
http_client,
&format!("https://hub.windmill.dev/raw/{path}.ts"),
email,
username,
host,
true,
)
.await?
@@ -241,11 +237,9 @@ pub async fn get_hub_script_by_path(
#[cfg(feature = "reqwest")]
pub async fn get_full_hub_script_by_path(
email: Option<String>,
username: String,
email: &str,
path: StripPath,
http_client: reqwest::Client,
host: String,
) -> crate::error::Result<HubScript> {
use crate::{
error::{to_anyhow, Error},
@@ -261,8 +255,6 @@ pub async fn get_full_hub_script_by_path(
http_client,
&format!("https://hub.windmill.dev/raw2/{path}"),
email,
username,
host,
true,
)
.await?

View File

@@ -31,6 +31,7 @@ fn filter_metadata(meta: &Metadata) -> bool {
}
pub fn initialize_tracing() {
let style = std::env::var("RUST_LOG_STYLE").unwrap_or_else(|_| "auto".into());
let json_fmt = std::env::var("JSON_FMT")
.map(|x| x == "true")
.unwrap_or(false);
@@ -41,10 +42,18 @@ pub fn initialize_tracing() {
match json_fmt {
true => ts_base
.with(json_layer().with_filter(filter_fn(filter_metadata)))
.with(
json_layer()
.flatten_event(true)
.with_filter(filter_fn(filter_metadata)),
)
.init(),
false => ts_base
.with(compact_layer().with_filter(filter_fn(filter_metadata)))
.with(
compact_layer()
.with_ansi(style.to_lowercase() != "never")
.with_filter(filter_fn(filter_metadata)),
)
.init(),
}
}

View File

@@ -6,7 +6,10 @@
* LICENSE-AGPL for a copy of the license.
*/
pub fn owner_to_token_owner(user: &str, is_group: bool) -> String {
let prefix = if is_group { 'g' } else { 'u' };
format!("{}/{}", prefix, user)
pub fn username_to_permissioned_as(user: &str) -> String {
if user.contains('@') {
user.to_string()
} else {
format!("u/{}", user)
}
}

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